diff --git a/src/resources/extensions/gsd/bootstrap/register-extension.ts b/src/resources/extensions/gsd/bootstrap/register-extension.ts index 3368b75eb..024d4a72d 100644 --- a/src/resources/extensions/gsd/bootstrap/register-extension.ts +++ b/src/resources/extensions/gsd/bootstrap/register-extension.ts @@ -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); } diff --git a/src/resources/extensions/gsd/bootstrap/register-hooks.ts b/src/resources/extensions/gsd/bootstrap/register-hooks.ts index 58cef8c50..7d82b956f 100644 --- a/src/resources/extensions/gsd/bootstrap/register-hooks.ts +++ b/src/resources/extensions/gsd/bootstrap/register-hooks.ts @@ -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(); diff --git a/src/resources/extensions/gsd/prompts/complete-milestone.md b/src/resources/extensions/gsd/prompts/complete-milestone.md index 21c35a39a..82aea198c 100644 --- a/src/resources/extensions/gsd/prompts/complete-milestone.md +++ b/src/resources/extensions/gsd/prompts/complete-milestone.md @@ -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 9–13): -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. diff --git a/src/resources/extensions/gsd/prompts/complete-slice.md b/src/resources/extensions/gsd/prompts/complete-slice.md index 2a3d0ca7b..2779ebc5d 100644 --- a/src/resources/extensions/gsd/prompts/complete-slice.md +++ b/src/resources/extensions/gsd/prompts/complete-slice.md @@ -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. diff --git a/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts b/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts new file mode 100644 index 000000000..9be886664 --- /dev/null +++ b/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts @@ -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', + ); + }); +});