Merge pull request #3555 from Tibsfox/fix/captures-executed-timestamp

fix(gsd): stamp defer and milestone captures as executed after triage
This commit is contained in:
Jeremy McSpadden 2026-04-07 07:19:43 -05:00 committed by GitHub
commit b38db63c96
2 changed files with 49 additions and 10 deletions

View file

@ -0,0 +1,30 @@
/**
* 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, "defer", "Deferred to a future UX-polish milestone", "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 });
}
});

View file

@ -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 as string) === "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<string, CaptureEntry[]>();
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);
}
}
}
// Mark note captures as executed — they're informational only, no action