fix(gsd): fail-closed stop guard, harden backtrack parsing, fix prompt params
- Stop/backtrack guard now calls pauseAuto before marking captures executed, and returns break on any exception to prevent silently dropping user halt intent - Backtrack target parsing excludes current milestone ID and rejects ambiguous multi-target strings instead of guessing first match - Fixed gsd_skip_slice parameter names in rethink prompt (milestone_id → milestoneId)
This commit is contained in:
parent
4f896cc561
commit
abe887de10
3 changed files with 25 additions and 11 deletions
|
|
@ -721,6 +721,8 @@ export async function runGuards(
|
|||
// ── Stop/Backtrack directive guard (#3487) ──
|
||||
// Check for unexecuted stop or backtrack captures BEFORE dispatching any unit.
|
||||
// This ensures user "halt" directives are honored immediately.
|
||||
// IMPORTANT: Fail-closed — any exception during stop handling still breaks the loop
|
||||
// to ensure user halt intent is never silently dropped.
|
||||
try {
|
||||
const { loadStopCaptures, markCaptureExecuted } = await import("../captures.js");
|
||||
const stopCaptures = loadStopCaptures(s.basePath);
|
||||
|
|
@ -737,12 +739,10 @@ export async function runGuards(
|
|||
basename(s.originalBasePath || s.basePath),
|
||||
);
|
||||
|
||||
// Mark all stop/backtrack captures as executed so they don't re-fire
|
||||
for (const cap of stopCaptures) {
|
||||
markCaptureExecuted(s.basePath, cap.id);
|
||||
}
|
||||
// Pause first — ensures auto-mode stops even if later steps fail
|
||||
await deps.pauseAuto(ctx, pi);
|
||||
|
||||
// For backtrack captures, write the backtrack trigger before pausing
|
||||
// For backtrack captures, write the backtrack trigger after pausing
|
||||
if (isBacktrack) {
|
||||
try {
|
||||
const { executeBacktrack } = await import("../triage-resolution.js");
|
||||
|
|
@ -752,12 +752,19 @@ export async function runGuards(
|
|||
}
|
||||
}
|
||||
|
||||
await deps.pauseAuto(ctx, pi);
|
||||
// Mark captures as executed only after successful pause/transition
|
||||
for (const cap of stopCaptures) {
|
||||
markCaptureExecuted(s.basePath, cap.id);
|
||||
}
|
||||
|
||||
debugLog("autoLoop", { phase: "exit", reason: isBacktrack ? "user-backtrack" : "user-stop" });
|
||||
return { action: "break", reason: isBacktrack ? "user-backtrack" : "user-stop" };
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail-closed: if anything in the stop guard throws, break the loop
|
||||
// rather than silently continuing and dropping user halt intent
|
||||
debugLog("guards", { phase: "stop-guard-error", error: String(e) });
|
||||
return { action: "break", reason: "stop-guard-error" };
|
||||
}
|
||||
|
||||
// Budget ceiling guard
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it.
|
|||
### Skip a slice
|
||||
Mark a slice as skipped so auto-mode advances past it without executing. Use the `gsd_skip_slice` tool:
|
||||
```
|
||||
gsd_skip_slice({ milestone_id: "M003", slice_id: "S02", reason: "Descoped — feature moved to M005" })
|
||||
gsd_skip_slice({ milestoneId: "M003", sliceId: "S02", reason: "Descoped — feature moved to M005" })
|
||||
```
|
||||
Skipped slices are treated as closed by the state machine (like "complete" but distinct). Use when a slice is no longer needed or has been superseded. The slice data is preserved for reference.
|
||||
|
||||
|
|
|
|||
|
|
@ -148,10 +148,17 @@ export function executeBacktrack(
|
|||
capture: CaptureEntry,
|
||||
): string | null {
|
||||
try {
|
||||
// Extract target milestone from capture text or resolution
|
||||
const targetMatch = (capture.resolution ?? capture.text)
|
||||
.match(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/);
|
||||
const targetMilestoneId = targetMatch?.[1] ?? null;
|
||||
// Extract target milestone from capture text or resolution.
|
||||
// Filter out the current milestone ID to avoid picking it as the backtrack target
|
||||
// when the text mentions both current and target milestones (e.g. "backtrack from M004 to M003").
|
||||
const sourceText = capture.resolution ?? capture.text;
|
||||
const allMatches = [...sourceText.matchAll(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/g)]
|
||||
.map(m => m[1])
|
||||
.filter(id => id !== currentMilestoneId);
|
||||
// Reject ambiguous multi-target strings — if more than one distinct target remains,
|
||||
// don't guess; let the user clarify.
|
||||
const uniqueTargets = [...new Set(allMatches)];
|
||||
const targetMilestoneId = uniqueTargets.length === 1 ? uniqueTargets[0] : null;
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue