From b46b113360f078ec9483ffe5b29a3cf4e467d98b Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 5 Apr 2026 10:38:29 -0700 Subject: [PATCH 1/4] fix(gsd): stamp defer and milestone captures as executed after triage --- .../extensions/gsd/triage-resolution.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/resources/extensions/gsd/triage-resolution.ts b/src/resources/extensions/gsd/triage-resolution.ts index 256091edf..7255a34f6 100644 --- a/src/resources/extensions/gsd/triage-resolution.ts +++ b/src/resources/extensions/gsd/triage-resolution.ts @@ -479,15 +479,18 @@ export function executeTriageResolutions( } } - // Also process deferred captures that target milestone IDs — create - // milestone directories so deriveState() discovers them. - const deferred = loadAllCaptures(basePath).filter( - c => c.status === "resolved" && !c.executed && c.classification === "defer", + // Also process deferred and milestone-class captures (#3542). + // A defer/milestone capture's "action" is the triage decision itself — + // once classified and resolved, the capture is done. The target milestone + // picks up the work naturally from its planning context. + const deferrable = loadAllCaptures(basePath).filter( + c => c.status === "resolved" && !c.executed && + (c.classification === "defer" || c.classification === "milestone"), ); - if (deferred.length > 0) { - // Group deferred captures by target milestone + if (deferrable.length > 0) { + // Group captures that reference a specific milestone — create dirs as needed. const byMilestone = new Map(); - for (const cap of deferred) { + for (const cap of deferrable) { const target = cap.resolution?.match(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/)?.[1]; if (target) { const list = byMilestone.get(target) ?? []; @@ -502,12 +505,18 @@ export function executeTriageResolutions( if (created) { result.deferredMilestones++; result.actions.push(`Created milestone ${milestoneId} for ${captures.length} deferred capture(s)`); - for (const cap of captures) { - markCaptureExecuted(basePath, cap.id); - } } } } + // Stamp ALL defer/milestone captures as executed (#3542 gaps 1-3). + // Previously only captures that triggered dir creation were stamped. + // Captures without a milestone ID in resolution text, or targeting an + // existing directory, were silently dropped — never stamped. + for (const cap of deferrable) { + if (!cap.executed) { + markCaptureExecuted(basePath, cap.id); + } + } } if (actionable.length === 0) return result; From 5cb04f54cad13c76867fe6176830ea15577114f6 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 5 Apr 2026 11:55:07 -0700 Subject: [PATCH 2/4] test(gsd): add defer capture stamp regression test --- .../gsd/tests/defer-milestone-stamp.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts diff --git a/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts b/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts new file mode 100644 index 000000000..d60cbafcc --- /dev/null +++ b/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts @@ -0,0 +1,34 @@ +/** + * Regression test for #3542: defer and milestone captures must be stamped + * as executed after triage resolution, regardless of directory state. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { executeTriageResolutions } from "../triage-resolution.ts"; +import { appendCapture, markCaptureResolved, loadAllCaptures } from "../captures.ts"; + +test("defer captures without milestone ID are stamped as executed (#3542)", async () => { + const base = mkdtempSync(join(tmpdir(), "gsd-stamp-")); + mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true }); + try { + appendCapture(base, "Improve error messages"); + const captures = loadAllCaptures(base); + const id = captures[0].id; + markCaptureResolved(base, id, { + classification: "defer", + resolution: "Deferred to a future UX-polish milestone", + reason: "Not urgent", + }); + + executeTriageResolutions(base, "M001", "S01"); + + const after = loadAllCaptures(base); + const cap = after.find(c => c.id === id); + assert.ok(cap?.executed, "Defer capture should be stamped as executed"); + } finally { + rmSync(base, { recursive: true, force: true }); + } +}); From db90607378f2d149041413f1ce9c6661aed588ed Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 5 Apr 2026 12:05:02 -0700 Subject: [PATCH 3/4] fix(gsd): cast milestone classification to string for type safety --- src/resources/extensions/gsd/triage-resolution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/triage-resolution.ts b/src/resources/extensions/gsd/triage-resolution.ts index 7255a34f6..270a176fc 100644 --- a/src/resources/extensions/gsd/triage-resolution.ts +++ b/src/resources/extensions/gsd/triage-resolution.ts @@ -485,7 +485,7 @@ export function executeTriageResolutions( // picks up the work naturally from its planning context. const deferrable = loadAllCaptures(basePath).filter( c => c.status === "resolved" && !c.executed && - (c.classification === "defer" || c.classification === "milestone"), + (c.classification === "defer" || (c.classification as string) === "milestone"), ); if (deferrable.length > 0) { // Group captures that reference a specific milestone — create dirs as needed. From f953e5d9c76f4998a820ef15e402dcca92efa4c3 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 5 Apr 2026 13:14:37 -0700 Subject: [PATCH 4/4] fix(gsd): pass required arguments in defer-milestone-stamp test Co-Authored-By: Claude Opus 4.6 (1M context) --- .../extensions/gsd/tests/defer-milestone-stamp.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts b/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts index d60cbafcc..22a7d7670 100644 --- a/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts +++ b/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts @@ -17,11 +17,7 @@ test("defer captures without milestone ID are stamped as executed (#3542)", asyn appendCapture(base, "Improve error messages"); const captures = loadAllCaptures(base); const id = captures[0].id; - markCaptureResolved(base, id, { - classification: "defer", - resolution: "Deferred to a future UX-polish milestone", - reason: "Not urgent", - }); + markCaptureResolved(base, id, "defer", "Deferred to a future UX-polish milestone", "Not urgent"); executeTriageResolutions(base, "M001", "S01");