Merge pull request #3696 from Tibsfox/fix/prompt-step-ordering-and-tools

fix(gsd): prompt ordering, correct tools, crash guard, compaction fix
This commit is contained in:
Jeremy McSpadden 2026-04-07 06:57:28 -05:00 committed by GitHub
commit 0339317d10
5 changed files with 97 additions and 6 deletions

View file

@ -36,7 +36,10 @@ function installEpipeGuard(): void {
if (handleRecoverableExtensionProcessError(err)) {
return;
}
throw err;
// Log unhandled errors instead of re-throwing — throwing inside an
// uncaughtException handler is a fatal double-fault in Node.js (#3163).
process.stderr.write(`[gsd] uncaught extension error (non-fatal): ${err.message}\n`);
if (err.stack) process.stderr.write(`${err.stack}\n`);
};
process.on("uncaughtException", _gsdEpipeGuard);
}

View file

@ -108,7 +108,10 @@ export function registerHooks(pi: ExtensionAPI): void {
});
pi.on("session_before_compact", async () => {
if (isAutoActive() || isAutoPaused()) {
// Only cancel compaction while auto-mode is actively running.
// Paused auto-mode should allow compaction — the user may be doing
// interactive work (#3165).
if (isAutoActive()) {
return { cancel: true };
}
const basePath = process.cwd();

View file

@ -28,7 +28,7 @@ Then:
### Verification Gate — STOP if verification failed
**If ANY verification failure was recorded in steps 3, 4, or 5, you MUST follow the failure path below. Do NOT proceed to step 9.**
**If ANY verification failure was recorded in steps 3, 4, or 5, you MUST follow the failure path below. Do NOT proceed to step 10.**
**Failure path** (verification failed):
- Do NOT call `gsd_complete_milestone` — the milestone must not be marked as complete.
@ -39,7 +39,8 @@ Then:
**Success path** (all verifications passed — continue with steps 913):
9. **Persist completion through `gsd_complete_milestone`.** Call it with the parameters below. The tool updates the milestone status in the DB, renders `{{milestoneSummaryPath}}`, and validates all slices are complete before proceeding.
9. For each requirement whose status changed in step 8, call `gsd_requirement_update` with the requirement ID and updated `status` and `validation` fields — the tool regenerates `.gsd/REQUIREMENTS.md` automatically. Do this BEFORE completing the milestone so requirement updates are persisted.
10. **Persist completion through `gsd_complete_milestone`.** Call it with the parameters below. The tool updates the milestone status in the DB, renders `{{milestoneSummaryPath}}`, and validates all slices are complete before proceeding.
**Required parameters:**
- `milestoneId` (string) — Milestone ID (e.g. M001)
@ -57,7 +58,6 @@ Then:
**Optional parameters:**
- `followUps` (string) — Follow-up items for future milestones
- `deviations` (string) — Deviations from the original plan
10. For each requirement whose status changed in step 8, call `gsd_requirement_update` with the requirement ID and updated `status` and `validation` fields — the tool regenerates `.gsd/REQUIREMENTS.md` automatically.
11. Update `.gsd/PROJECT.md`: use the `write` tool with `path: ".gsd/PROJECT.md"` and `content` containing the full updated document reflecting milestone completion and current project state. Do NOT use the `edit` tool for this — PROJECT.md is a full-document refresh.
12. Review all slice summaries for cross-cutting lessons, patterns, or gotchas that emerged during this milestone. Append any non-obvious, reusable insights to `.gsd/KNOWLEDGE.md`.
13. Do not commit manually — the system auto-commits your changes after this unit completes.

View file

@ -24,7 +24,7 @@ Then:
3. Run all slice-level verification checks defined in the slice plan. All must pass before marking the slice done. If any fail, fix them first.
4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections.
5. If the slice involved runtime behavior, fill the **Operational Readiness** section (Q8) in the slice summary: health signal, failure signal, recovery procedure, and monitoring gaps. Omit entirely for simple slices with no runtime concerns.
6. If this slice produced evidence that a requirement changed status (Active → Validated, Active → Deferred, etc.), call `gsd_save_decision` with scope="requirement", decision="{requirement-id}", choice="{new-status}", rationale="{evidence}". Do NOT write `.gsd/REQUIREMENTS.md` directly — the engine renders it from the database.
6. If this slice produced evidence that a requirement changed status (Active → Validated, Active → Deferred, etc.), call `gsd_requirement_update` with the requirement ID, updated `status`, and `validation` evidence. Do NOT write `.gsd/REQUIREMENTS.md` directly — the engine renders it from the database.
7. Write `{{sliceSummaryPath}}` (compress all task summaries).
8. Write `{{sliceUatPath}}` — a concrete UAT script with real test cases derived from the slice plan and task summaries. Include preconditions, numbered steps with expected outcomes, and edge cases. This must NOT be a placeholder or generic template — tailor every test case to what this slice actually built.
9. Review task summaries for `key_decisions`. Append any significant decisions to `.gsd/DECISIONS.md` if missing.

View file

@ -0,0 +1,85 @@
/**
* Regression test for #3696 prompt step ordering and runtime fixes
*
* 1. complete-milestone.md: gsd_requirement_update (step 9) before
* gsd_complete_milestone (step 10)
* 2. complete-slice.md: uses gsd_requirement_update
* 3. register-extension.ts: _gsdEpipeGuard logs instead of re-throwing
* 4. register-hooks.ts: session_before_compact only checks isAutoActive
*/
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const completeMilestoneMd = readFileSync(
join(__dirname, '..', 'prompts', 'complete-milestone.md'),
'utf-8',
);
const completeSliceMd = readFileSync(
join(__dirname, '..', 'prompts', 'complete-slice.md'),
'utf-8',
);
const registerExtSrc = readFileSync(
join(__dirname, '..', 'bootstrap', 'register-extension.ts'),
'utf-8',
);
const registerHooksSrc = readFileSync(
join(__dirname, '..', 'bootstrap', 'register-hooks.ts'),
'utf-8',
);
describe('prompt step ordering (#3696)', () => {
test('gsd_requirement_update step appears before gsd_complete_milestone step', () => {
// Search for the numbered step definitions, not early "Do NOT call" warnings
const reqUpdateMatch = completeMilestoneMd.match(/^\d+\.\s.*gsd_requirement_update/m);
const completeMilestoneMatch = completeMilestoneMd.match(/^\d+\.\s.*gsd_complete_milestone/m);
assert.ok(reqUpdateMatch, 'gsd_requirement_update should appear in a numbered step');
assert.ok(completeMilestoneMatch, 'gsd_complete_milestone should appear in a numbered step');
const reqUpdateIdx = completeMilestoneMd.indexOf(reqUpdateMatch![0]);
const completeMilestoneIdx = completeMilestoneMd.indexOf(completeMilestoneMatch![0]);
assert.ok(
reqUpdateIdx < completeMilestoneIdx,
'gsd_requirement_update step must come before gsd_complete_milestone step',
);
});
test('complete-slice.md uses gsd_requirement_update', () => {
assert.match(completeSliceMd, /gsd_requirement_update/,
'complete-slice.md should reference gsd_requirement_update');
});
});
describe('register-extension _gsdEpipeGuard (#3696)', () => {
test('_gsdEpipeGuard exists and does not re-throw', () => {
assert.match(registerExtSrc, /_gsdEpipeGuard/,
'_gsdEpipeGuard should be defined in register-extension.ts');
// After the fix, the handler logs instead of throwing
assert.ok(
!registerExtSrc.includes('throw err'),
'_gsdEpipeGuard should NOT contain "throw err"',
);
});
});
describe('register-hooks session_before_compact (#3696)', () => {
test('session_before_compact only checks isAutoActive', () => {
// Extract the session_before_compact handler
const compactIdx = registerHooksSrc.indexOf('session_before_compact');
assert.ok(compactIdx > -1, 'session_before_compact hook should exist');
// The first check in the handler should be isAutoActive(), not isAutoPaused()
const afterCompact = registerHooksSrc.slice(compactIdx, compactIdx + 300);
assert.match(afterCompact, /isAutoActive\(\)/,
'session_before_compact should check isAutoActive()');
// Should NOT block compaction when paused
assert.ok(
!afterCompact.includes('isAutoPaused()'),
'session_before_compact should not check isAutoPaused',
);
});
});