gsd-pi 2.82.0-dev.3a3c6509d → 2.82.0-dev.4285182e8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +14 -1
  4. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  5. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +3 -0
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-post-unit.js +12 -5
  8. package/dist/resources/extensions/gsd/auto.js +14 -7
  9. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  11. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  12. package/dist/resources/extensions/gsd/commands-verdict.js +139 -0
  13. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
  14. package/dist/resources/extensions/gsd/paths.js +4 -0
  15. package/dist/resources/extensions/gsd/state.js +2 -2
  16. package/dist/resources/extensions/gsd/templates/plan.md +1 -0
  17. package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
  18. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
  19. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
  20. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.html +1 -1
  43. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  50. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  53. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  54. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  55. package/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
  56. package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
  57. package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
  58. package/package.json +1 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +13 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  62. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +5 -2
  64. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  65. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +16 -1
  66. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -2
  67. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  68. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  69. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  70. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
  71. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  72. package/src/resources/extensions/gsd/auto-post-unit.ts +13 -5
  73. package/src/resources/extensions/gsd/auto.ts +13 -7
  74. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  75. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  76. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  77. package/src/resources/extensions/gsd/commands-verdict.ts +202 -0
  78. package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
  79. package/src/resources/extensions/gsd/paths.ts +5 -0
  80. package/src/resources/extensions/gsd/state.ts +2 -2
  81. package/src/resources/extensions/gsd/templates/plan.md +1 -0
  82. package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
  83. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +110 -0
  84. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
  85. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +378 -0
  86. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
  87. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
  88. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
  89. package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
  90. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  91. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
  92. package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
  93. package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
  94. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
  95. /package/dist/web/standalone/.next/static/{O6femb9LLl3nlgsDaYwS- → 78uanrILNOKG-Jpi4itAE}/_buildManifest.js +0 -0
  96. /package/dist/web/standalone/.next/static/{O6femb9LLl3nlgsDaYwS- → 78uanrILNOKG-Jpi4itAE}/_ssgManifest.js +0 -0
@@ -1,4 +1,5 @@
1
- // GSD-2 — Tests for step-mode completion messages in auto-post-unit
1
+ // Project/App: GSD-2
2
+ // File Purpose: Tests for step-mode completion messages in auto-post-unit.
2
3
 
3
4
  import test from "node:test";
4
5
  import assert from "node:assert/strict";
@@ -31,7 +32,7 @@ test("buildStepCompleteMessage: milestone complete surfaces review guidance", ()
31
32
  assert.doesNotMatch(msg, /Next:/);
32
33
  });
33
34
 
34
- test("buildStepCompleteMessage: mid-flight step includes next unit label and /clear hint", () => {
35
+ test("buildStepCompleteMessage: mid-flight step includes next unit label and /gsd next hint", () => {
35
36
  const state = makeState({
36
37
  phase: "executing",
37
38
  activeSlice: { id: "S01", title: "Core" },
@@ -40,7 +41,7 @@ test("buildStepCompleteMessage: mid-flight step includes next unit label and /cl
40
41
  const msg = buildStepCompleteMessage(state);
41
42
  assert.match(msg, /Next: Execute T03: Wire notify/);
42
43
  assert.match(msg, /\/clear/);
43
- assert.match(msg, /\/gsd to continue/);
44
+ assert.match(msg, /\/gsd next to continue one step/);
44
45
  });
45
46
 
46
47
  test("buildStepCompleteMessage: unknown phase falls back to generic continue label", () => {
@@ -51,9 +52,9 @@ test("buildStepCompleteMessage: unknown phase falls back to generic continue lab
51
52
  assert.match(msg, /\/clear/);
52
53
  });
53
54
 
54
- test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points users at /clear + /gsd", () => {
55
+ test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points users at /clear + /gsd next", () => {
55
56
  assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/clear/);
56
- assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd/);
57
+ assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd next/);
57
58
  });
58
59
 
59
60
  test("shouldReturnStepWizardAfterUnit: terminal milestone completion continues to merge-back path", () => {
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Tests for /gsd verdict — manual override of milestone validation verdict.
3
+ *
4
+ * Covers parseValidationFile section extraction and handleVerdict end-to-end:
5
+ * pass override, needs-remediation override with rationale, missing rationale
6
+ * rejection, active-milestone fallback, missing VALIDATION rejection.
7
+ *
8
+ * Also asserts the three paused-state messages reference /gsd verdict so the
9
+ * user has a discoverable recovery path.
10
+ */
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ import { randomUUID } from "node:crypto";
17
+
18
+ import { handleVerdict, parseValidationFile } from "../commands-verdict.ts";
19
+ import { openDatabase, closeDatabase, _getAdapter } from "../gsd-db.ts";
20
+ import { invalidateStateCache } from "../state.ts";
21
+
22
+ interface NotifyCall {
23
+ message: string;
24
+ kind: string;
25
+ }
26
+
27
+ function makeMockCtx(): { ctx: any; calls: NotifyCall[] } {
28
+ const calls: NotifyCall[] = [];
29
+ const ctx = {
30
+ ui: {
31
+ notify: (message: string, kind: string) => {
32
+ calls.push({ message, kind });
33
+ },
34
+ },
35
+ };
36
+ return { ctx, calls };
37
+ }
38
+
39
+ function makeBase(): string {
40
+ const base = mkdtempSync(join(tmpdir(), `gsd-verdict-${randomUUID()}-`));
41
+ mkdirSync(join(base, ".gsd"), { recursive: true });
42
+ return base;
43
+ }
44
+
45
+ function cleanup(base: string): void {
46
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* swallow */ }
47
+ }
48
+
49
+ function openTestDb(base: string): void {
50
+ openDatabase(join(base, ".gsd", "gsd.db"));
51
+ }
52
+
53
+ function seedMilestone(milestoneId: string, title: string, status = "active"): void {
54
+ const db = _getAdapter();
55
+ if (!db) throw new Error("DB not open");
56
+ db.prepare(
57
+ "INSERT OR REPLACE INTO milestones (id, title, status, created_at) VALUES (?, ?, ?, ?)",
58
+ ).run(milestoneId, title, status, new Date().toISOString());
59
+ }
60
+
61
+ function seedSlice(milestoneId: string, sliceId: string, status: string): void {
62
+ const db = _getAdapter();
63
+ if (!db) throw new Error("DB not open");
64
+ db.prepare(
65
+ "INSERT OR REPLACE INTO slices (milestone_id, id, title, status, created_at) VALUES (?, ?, ?, ?, ?)",
66
+ ).run(milestoneId, sliceId, `Slice ${sliceId}`, status, new Date().toISOString());
67
+ }
68
+
69
+ function writeValidation(base: string, milestoneId: string, verdict: string, round = 0): string {
70
+ const milestoneDir = join(base, ".gsd", "milestones", milestoneId);
71
+ mkdirSync(milestoneDir, { recursive: true });
72
+ const path = join(milestoneDir, `${milestoneId}-VALIDATION.md`);
73
+ const md = [
74
+ "---",
75
+ `verdict: ${verdict}`,
76
+ `remediation_round: ${round}`,
77
+ "---",
78
+ "",
79
+ `# Milestone Validation: ${milestoneId}`,
80
+ "",
81
+ "## Success Criteria Checklist",
82
+ "- [x] Criterion A met",
83
+ "- [x] Criterion B met",
84
+ "",
85
+ "## Slice Delivery Audit",
86
+ "| Slice | Result |",
87
+ "| --- | --- |",
88
+ "| S01 | delivered |",
89
+ "",
90
+ "## Cross-Slice Integration",
91
+ "No cross-slice mismatches detected.",
92
+ "",
93
+ "## Requirement Coverage",
94
+ "All requirements covered.",
95
+ "",
96
+ "## Verdict Rationale",
97
+ `Initial verdict was ${verdict}.`,
98
+ "",
99
+ ].join("\n");
100
+ writeFileSync(path, md);
101
+ return path;
102
+ }
103
+
104
+ // ─── parseValidationFile ────────────────────────────────────────────────
105
+
106
+ test("parseValidationFile extracts all standard sections and frontmatter", () => {
107
+ const md = [
108
+ "---",
109
+ "verdict: needs-attention",
110
+ "remediation_round: 2",
111
+ "---",
112
+ "",
113
+ "# Milestone Validation: M001",
114
+ "",
115
+ "## Success Criteria Checklist",
116
+ "- [x] First",
117
+ "- [ ] Second",
118
+ "",
119
+ "## Slice Delivery Audit",
120
+ "| Slice | Delivered |",
121
+ "",
122
+ "## Cross-Slice Integration",
123
+ "Boundary intact.",
124
+ "",
125
+ "## Requirement Coverage",
126
+ "R001 covered.",
127
+ "",
128
+ "## Verification Class Compliance",
129
+ "Operational: MET",
130
+ "",
131
+ "## Verdict Rationale",
132
+ "Acceptance proof incomplete.",
133
+ "",
134
+ "## Remediation Plan",
135
+ "Address acceptance proof.",
136
+ "",
137
+ ].join("\n");
138
+
139
+ const parsed = parseValidationFile(md);
140
+
141
+ assert.equal(parsed.verdict, "needs-attention");
142
+ assert.equal(parsed.remediationRound, 2);
143
+ assert.match(parsed.successCriteriaChecklist, /First/);
144
+ assert.match(parsed.successCriteriaChecklist, /Second/);
145
+ assert.match(parsed.sliceDeliveryAudit, /Delivered/);
146
+ assert.match(parsed.crossSliceIntegration, /Boundary intact/);
147
+ assert.match(parsed.requirementCoverage, /R001 covered/);
148
+ assert.match(parsed.verificationClasses ?? "", /Operational: MET/);
149
+ assert.match(parsed.verdictRationale, /Acceptance proof incomplete/);
150
+ assert.match(parsed.remediationPlan ?? "", /Address acceptance proof/);
151
+ });
152
+
153
+ test("parseValidationFile omits optional sections when absent", () => {
154
+ const md = [
155
+ "---",
156
+ "verdict: pass",
157
+ "remediation_round: 0",
158
+ "---",
159
+ "",
160
+ "## Success Criteria Checklist",
161
+ "- [x] All met",
162
+ "",
163
+ "## Slice Delivery Audit",
164
+ "Done.",
165
+ "",
166
+ "## Cross-Slice Integration",
167
+ "Clean.",
168
+ "",
169
+ "## Requirement Coverage",
170
+ "Complete.",
171
+ "",
172
+ "## Verdict Rationale",
173
+ "All criteria met.",
174
+ ].join("\n");
175
+
176
+ const parsed = parseValidationFile(md);
177
+
178
+ assert.equal(parsed.verdict, "pass");
179
+ assert.equal(parsed.remediationRound, 0);
180
+ assert.equal(parsed.verificationClasses, undefined);
181
+ assert.equal(parsed.remediationPlan, undefined);
182
+ });
183
+
184
+ // ─── handleVerdict — argument validation ────────────────────────────────
185
+
186
+ test("handleVerdict rejects missing verdict", async () => {
187
+ const { ctx, calls } = makeMockCtx();
188
+ await handleVerdict("", ctx, "/tmp/unused");
189
+ assert.equal(calls.length, 1);
190
+ assert.match(calls[0].message, /Usage: \/gsd verdict/);
191
+ assert.equal(calls[0].kind, "warning");
192
+ });
193
+
194
+ test("handleVerdict rejects invalid verdict", async () => {
195
+ const { ctx, calls } = makeMockCtx();
196
+ await handleVerdict("yolo", ctx, "/tmp/unused");
197
+ assert.equal(calls.length, 1);
198
+ assert.match(calls[0].message, /Invalid verdict "yolo"/);
199
+ assert.equal(calls[0].kind, "warning");
200
+ });
201
+
202
+ test("handleVerdict rejects needs-remediation without --rationale", async () => {
203
+ const base = makeBase();
204
+ try {
205
+ openTestDb(base);
206
+ seedMilestone("M001", "Test Milestone");
207
+ seedSlice("M001", "S01", "complete");
208
+ writeValidation(base, "M001", "pass");
209
+
210
+ const { ctx, calls } = makeMockCtx();
211
+ await handleVerdict("needs-remediation --milestone M001", ctx, base);
212
+
213
+ assert.ok(
214
+ calls.some((c) => /--rationale is required/.test(c.message)),
215
+ `expected rationale-required warning, got: ${JSON.stringify(calls)}`,
216
+ );
217
+ } finally {
218
+ closeDatabase();
219
+ invalidateStateCache();
220
+ cleanup(base);
221
+ }
222
+ });
223
+
224
+ test("handleVerdict rejects when VALIDATION file is missing", async () => {
225
+ const base = makeBase();
226
+ try {
227
+ openTestDb(base);
228
+ seedMilestone("M001", "Test Milestone");
229
+ seedSlice("M001", "S01", "complete");
230
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
231
+
232
+ const { ctx, calls } = makeMockCtx();
233
+ await handleVerdict("pass --milestone M001", ctx, base);
234
+
235
+ assert.ok(
236
+ calls.some((c) => /No VALIDATION file found/.test(c.message)),
237
+ `expected missing-VALIDATION warning, got: ${JSON.stringify(calls)}`,
238
+ );
239
+ } finally {
240
+ closeDatabase();
241
+ invalidateStateCache();
242
+ cleanup(base);
243
+ }
244
+ });
245
+
246
+ // ─── handleVerdict — pass override flow ─────────────────────────────────
247
+
248
+ test("handleVerdict pass override flips verdict and preserves sections", async () => {
249
+ const base = makeBase();
250
+ try {
251
+ openTestDb(base);
252
+ seedMilestone("M001", "Test Milestone");
253
+ seedSlice("M001", "S01", "complete");
254
+ const validationPath = writeValidation(base, "M001", "needs-attention");
255
+
256
+ const { ctx, calls } = makeMockCtx();
257
+ await handleVerdict("pass --milestone M001", ctx, base);
258
+
259
+ const rewritten = readFileSync(validationPath, "utf-8");
260
+ assert.match(rewritten, /^verdict: pass$/m, "verdict should flip to pass");
261
+ assert.match(rewritten, /Criterion A met/, "success criteria preserved");
262
+ assert.match(rewritten, /S01 \| delivered/, "slice audit preserved");
263
+ assert.match(rewritten, /Manually overridden via \/gsd verdict/, "default rationale applied");
264
+
265
+ assert.ok(
266
+ calls.some((c) => c.kind === "success" && /needs-attention.*->.*pass/.test(c.message)),
267
+ `expected success notification, got: ${JSON.stringify(calls)}`,
268
+ );
269
+ } finally {
270
+ closeDatabase();
271
+ invalidateStateCache();
272
+ cleanup(base);
273
+ }
274
+ });
275
+
276
+ test("handleVerdict needs-remediation override with --rationale rewrites verdict", async () => {
277
+ const base = makeBase();
278
+ try {
279
+ openTestDb(base);
280
+ seedMilestone("M001", "Test Milestone");
281
+ seedSlice("M001", "S01", "complete");
282
+ const validationPath = writeValidation(base, "M001", "pass");
283
+
284
+ const { ctx, calls } = makeMockCtx();
285
+ await handleVerdict(
286
+ 'needs-remediation --milestone M001 --rationale "found missing slice"',
287
+ ctx,
288
+ base,
289
+ );
290
+
291
+ const rewritten = readFileSync(validationPath, "utf-8");
292
+ assert.match(rewritten, /^verdict: needs-remediation$/m);
293
+ assert.match(rewritten, /found missing slice/);
294
+
295
+ assert.ok(
296
+ calls.some((c) => /gsd_reassess_roadmap/.test(c.message)),
297
+ "needs-remediation override should suggest gsd_reassess_roadmap follow-up",
298
+ );
299
+ } finally {
300
+ closeDatabase();
301
+ invalidateStateCache();
302
+ cleanup(base);
303
+ }
304
+ });
305
+
306
+ test("handleVerdict resolves active milestone when --milestone omitted", async () => {
307
+ const base = makeBase();
308
+ try {
309
+ openTestDb(base);
310
+ seedMilestone("M042", "Active Milestone");
311
+ seedSlice("M042", "S01", "complete");
312
+ const validationPath = writeValidation(base, "M042", "needs-attention");
313
+
314
+ const { ctx, calls } = makeMockCtx();
315
+ invalidateStateCache();
316
+ await handleVerdict("pass", ctx, base);
317
+
318
+ const rewritten = readFileSync(validationPath, "utf-8");
319
+ assert.match(rewritten, /^verdict: pass$/m);
320
+ assert.ok(
321
+ calls.some((c) => c.kind === "success" && /M042/.test(c.message)),
322
+ `expected success notification naming M042, got: ${JSON.stringify(calls)}`,
323
+ );
324
+ } finally {
325
+ closeDatabase();
326
+ invalidateStateCache();
327
+ cleanup(base);
328
+ }
329
+ });
330
+
331
+ // ─── Pause messages reference /gsd verdict ─────────────────────────────
332
+
333
+ test("auto-dispatch needs-attention pause message references /gsd verdict", async () => {
334
+ const { DISPATCH_RULES } = await import("../auto-dispatch.ts");
335
+ const rule = DISPATCH_RULES.find((r) => r.name === "completing-milestone → complete-milestone");
336
+ assert.ok(rule, "completing-milestone rule should exist");
337
+
338
+ const base = mkdtempSync(join(tmpdir(), "gsd-verdict-paused-"));
339
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
340
+ try {
341
+ writeFileSync(
342
+ join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md"),
343
+ "---\nverdict: needs-attention\nremediation_round: 0\n---\n\n# Validation\nNeeds work.\n",
344
+ );
345
+
346
+ const result = await rule!.match({
347
+ mid: "M001",
348
+ midTitle: "Test",
349
+ basePath: base,
350
+ state: { phase: "completing-milestone" } as any,
351
+ prefs: {} as any,
352
+ session: undefined,
353
+ } as any);
354
+
355
+ assert.ok(result !== null);
356
+ assert.equal(result!.action, "stop");
357
+ if (result!.action === "stop") {
358
+ assert.match(result!.reason, /\/gsd verdict/);
359
+ }
360
+ } finally {
361
+ cleanup(base);
362
+ }
363
+ });
364
+
365
+ test("state.ts needs-remediation blocker messages reference /gsd verdict", async () => {
366
+ // We don't need to invoke deriveState — just assert the substring is in the
367
+ // source. The blocker strings are constructed inline and shipped to the user
368
+ // verbatim, so a static check is sufficient and avoids fragile DB setup.
369
+ const stateSource = readFileSync(
370
+ new URL("../state.ts", import.meta.url).pathname,
371
+ "utf-8",
372
+ );
373
+ const occurrences = stateSource.match(/`\/gsd verdict /g) ?? [];
374
+ assert.ok(
375
+ occurrences.length >= 2,
376
+ `expected at least 2 references to /gsd verdict in state.ts blockers, found ${occurrences.length}`,
377
+ );
378
+ });
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * gsdRoot() must return the canonical project .gsd directory when basePath
5
5
  * is inside a .gsd/worktrees/<name>/ structure. Worktree-local .gsd folders
6
- * are legacy projection roots only; runtime state is DB-authoritative at the
6
+ * are projection roots; runtime/control state stays DB-authoritative at the
7
7
  * project .gsd.
8
8
  *
9
9
  * The bug: when a git worktree lives at /project/.gsd/worktrees/M008/,
@@ -21,7 +21,7 @@ import { mkdtempSync, realpathSync } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import { spawnSync } from "node:child_process";
23
23
 
24
- import { gsdRoot, resolveGsdPathContract, _clearGsdRootCache } from "../paths.ts";
24
+ import { gsdProjectionRoot, gsdRoot, resolveGsdPathContract, _clearGsdRootCache } from "../paths.ts";
25
25
 
26
26
  describe("gsdRoot() worktree detection (#2594)", () => {
27
27
  let projectRoot: string;
@@ -76,6 +76,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
76
76
  `Expected canonical project .gsd (${projectGsd}), got ${result}.`,
77
77
  );
78
78
  assert.equal(resolveGsdPathContract(worktreeBase).worktreeGsd, worktreeGsd);
79
+ assert.equal(gsdProjectionRoot(worktreeBase), worktreeGsd);
79
80
  });
80
81
 
81
82
  test("returns project .gsd when worktree .gsd does not exist yet", () => {
@@ -89,6 +90,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
89
90
  projectGsd,
90
91
  `Expected canonical project .gsd (${projectGsd}), got ${result}.`,
91
92
  );
93
+ assert.equal(gsdProjectionRoot(worktreeBase), join(worktreeBase, ".gsd"));
92
94
  });
93
95
 
94
96
  test("returns project .gsd when basePath is a real git worktree inside .gsd/worktrees/", () => {
@@ -116,6 +118,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
116
118
  projectGsd,
117
119
  `Expected canonical project .gsd (${projectGsd}), got ${gsdResult}`,
118
120
  );
121
+ assert.equal(gsdProjectionRoot(worktreeBase), join(worktreeBase, ".gsd"));
119
122
 
120
123
  // Cleanup worktree
121
124
  spawnSync("git", ["worktree", "remove", "--force", worktreeBase], {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import test from 'node:test';
4
4
  import assert from 'node:assert/strict';
5
- import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
5
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, realpathSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { tmpdir } from 'node:os';
8
8
 
@@ -105,6 +105,31 @@ test('handlePlanSlice writes slice/task planning state and renders plan artifact
105
105
  }
106
106
  });
107
107
 
108
+ test('handlePlanSlice renders plan artifacts under worktree-local .gsd while using project DB', async () => {
109
+ const base = makeTmpBase();
110
+ const worktree = join(base, '.gsd', 'worktrees', 'M001');
111
+ mkdirSync(join(worktree, '.gsd'), { recursive: true });
112
+ mkdirSync(join(worktree, 'src', 'resources', 'extensions', 'gsd', 'tools'), { recursive: true });
113
+ writeFileSync(join(worktree, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-milestone.ts'), '// fixture\n', 'utf-8');
114
+ writeFileSync(join(worktree, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-task.ts'), '// fixture\n', 'utf-8');
115
+ openDatabase(join(base, '.gsd', 'gsd.db'));
116
+
117
+ try {
118
+ seedParentSlice();
119
+
120
+ const result = await handlePlanSlice(validParams(), worktree);
121
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
122
+
123
+ const worktreePlan = join(worktree, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md');
124
+ const projectPlan = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md');
125
+ assert.ok(existsSync(worktreePlan), 'slice plan should be rendered to worktree-local .gsd');
126
+ assert.ok(!existsSync(projectPlan), 'slice plan should not be rendered to project .gsd');
127
+ assert.equal(result.planPath, realpathSync(worktreePlan));
128
+ } finally {
129
+ cleanup(base);
130
+ }
131
+ });
132
+
108
133
  test('handlePlanSlice advances DB-derived state out of planning immediately', async () => {
109
134
  const base = makeTmpBase();
110
135
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -8,8 +8,32 @@ import assert from "node:assert/strict";
8
8
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { tmpdir } from "node:os";
11
+ import { createRequire } from "node:module";
11
12
  import { AutoSession } from "../auto/session.ts";
12
13
  import { postUnitPreVerification } from "../auto-post-unit.ts";
14
+ import {
15
+ _getAdapter,
16
+ closeDatabase,
17
+ getTask,
18
+ insertMilestone,
19
+ insertSlice,
20
+ insertTask,
21
+ openDatabase,
22
+ } from "../gsd-db.ts";
23
+
24
+ const _require = createRequire(import.meta.url);
25
+
26
+ function openRawSqliteForTest(dbPath: string): { exec(sql: string): void; close(): void } {
27
+ try {
28
+ const mod = _require("node:sqlite") as { DatabaseSync: new (path: string) => { exec(sql: string): void; close(): void } };
29
+ return new mod.DatabaseSync(dbPath);
30
+ } catch {
31
+ type SqliteCtor = new (path: string) => { exec(sql: string): void; close(): void };
32
+ const mod = _require("better-sqlite3") as SqliteCtor | { default: SqliteCtor };
33
+ const DatabaseCtor: SqliteCtor = typeof mod === "function" ? mod : mod.default;
34
+ return new DatabaseCtor(dbPath);
35
+ }
36
+ }
13
37
 
14
38
  test("postUnitPreVerification rebuilds STATE.md after a completed unit", async () => {
15
39
  const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-state-"));
@@ -47,3 +71,63 @@ test("postUnitPreVerification rebuilds STATE.md after a completed unit", async (
47
71
  rmSync(base, { recursive: true, force: true });
48
72
  }
49
73
  });
74
+
75
+ test("postUnitPreVerification refreshes DB before checking execute-task completion", async () => {
76
+ const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-db-refresh-"));
77
+ try {
78
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
79
+ const tasksDir = join(sliceDir, "tasks");
80
+ mkdirSync(tasksDir, { recursive: true });
81
+ writeFileSync(
82
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
83
+ "# Roadmap\n\n## Slices\n\n- [ ] **S01: Slice** `risk:low` `depends:[]`\n",
84
+ );
85
+ writeFileSync(
86
+ join(sliceDir, "S01-PLAN.md"),
87
+ "# S01: Slice\n\n## Tasks\n\n- [ ] **T01: Do work** `est:30m`\n",
88
+ );
89
+ writeFileSync(
90
+ join(tasksDir, "T01-SUMMARY.md"),
91
+ "---\nid: T01\nparent: S01\nmilestone: M001\n---\n# T01\nDone.\n",
92
+ );
93
+
94
+ const dbPath = join(base, ".gsd", "gsd.db");
95
+ openDatabase(dbPath);
96
+ insertMilestone({ id: "M001", title: "Milestone", status: "active" });
97
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
98
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Do work", status: "pending" });
99
+ const adapterBefore = _getAdapter();
100
+
101
+ const externalDb = openRawSqliteForTest(dbPath);
102
+ try {
103
+ externalDb.exec("UPDATE tasks SET status = 'complete', completed_at = '2026-05-14T00:00:00.000Z' WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'");
104
+ } finally {
105
+ externalDb.close();
106
+ }
107
+
108
+ const s = new AutoSession();
109
+ s.basePath = base;
110
+ s.originalBasePath = base;
111
+ s.currentMilestoneId = "M001";
112
+ s.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
113
+
114
+ const result = await postUnitPreVerification({
115
+ s,
116
+ ctx: { ui: { notify() {} } } as any,
117
+ pi: {} as any,
118
+ buildSnapshotOpts: () => ({}),
119
+ lockBase: () => base,
120
+ stopAuto: async () => {},
121
+ pauseAuto: async () => {},
122
+ updateProgressWidget: () => {},
123
+ }, { skipSettleDelay: true, skipWorktreeSync: true });
124
+
125
+ assert.equal(result, "continue");
126
+ assert.notEqual(_getAdapter(), adapterBefore, "post-unit flow must reopen the DB before deriving state");
127
+ assert.equal(getTask("M001", "S01", "T01")?.status, "complete");
128
+ assert.equal(s.pendingVerificationRetry, null);
129
+ } finally {
130
+ closeDatabase();
131
+ rmSync(base, { recursive: true, force: true });
132
+ }
133
+ });
@@ -1,3 +1,6 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Validates planning and task template quality-gate content.
3
+
1
4
  import { readFileSync } from "node:fs";
2
5
  import { join, dirname } from "node:path";
3
6
  import { fileURLToPath } from "node:url";
@@ -27,11 +30,14 @@ console.log("\n=== Level 1: Templates contain quality gate headings ===");
27
30
  const plan = loadTemplate("plan");
28
31
  assertTrue(plan.includes("## Threat Surface"), "plan.md contains ## Threat Surface");
29
32
  assertTrue(plan.includes("## Requirement Impact"), "plan.md contains ## Requirement Impact");
33
+ assertTrue(plan.includes("node --test"), "plan.md instructs using node --test for verification");
30
34
 
31
35
  const taskPlan = loadTemplate("task-plan");
32
36
  assertTrue(taskPlan.includes("## Failure Modes"), "task-plan.md contains ## Failure Modes");
33
37
  assertTrue(taskPlan.includes("## Load Profile"), "task-plan.md contains ## Load Profile");
34
38
  assertTrue(taskPlan.includes("## Negative Tests"), "task-plan.md contains ## Negative Tests");
39
+ assertTrue(taskPlan.includes("node --test"), "task-plan.md instructs using node --test for verification");
40
+ assertTrue(taskPlan.includes("node -e"), "task-plan.md mentions inline node -e as disallowed guidance");
35
41
 
36
42
  const sliceSummary = loadTemplate("slice-summary");
37
43
  assertTrue(sliceSummary.includes("## Operational Readiness"), "slice-summary.md contains ## Operational Readiness");
@@ -165,6 +165,13 @@ test("decideFinalizeResult maps break results to stop decisions", () => {
165
165
  );
166
166
  });
167
167
 
168
+ test("decideFinalizeResult maps step-wizard breaks to completed step exits", () => {
169
+ assert.deepEqual(
170
+ decideFinalizeResult({ action: "break", reason: "step-wizard" }),
171
+ { action: "complete-and-break" },
172
+ );
173
+ });
174
+
168
175
  test("decideFinalizeResult maps continue and next results", () => {
169
176
  assert.deepEqual(
170
177
  decideFinalizeResult({ action: "continue" }),
@@ -27,7 +27,7 @@ import { logWarning } from "../workflow-logger.js";
27
27
  import { validatePlanningPathScope } from "../planning-path-scope.js";
28
28
  import { checkFilePathConsistency, checkTaskOrdering } from "../pre-execution-checks.js";
29
29
  import type { TaskRow } from "../db-task-slice-rows.js";
30
- import { buildTaskFileName, gsdRoot, resolveTasksDir } from "../paths.js";
30
+ import { buildTaskFileName, gsdProjectionRoot } from "../paths.js";
31
31
 
32
32
  export interface PlanSliceTaskInput {
33
33
  taskId: string;
@@ -314,12 +314,11 @@ export async function handlePlanSlice(
314
314
  }
315
315
 
316
316
  try {
317
- const tasksDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
317
+ const tasksDir = join(gsdProjectionRoot(basePath), "milestones", params.milestoneId, "slices", params.sliceId, "tasks");
318
318
  for (const taskId of omittedTaskIds) {
319
- if (!tasksDir) continue;
320
319
  const taskPlanPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
321
320
  if (existsSync(taskPlanPath)) rmSync(taskPlanPath, { force: true });
322
- const artifactPath = relative(gsdRoot(basePath), taskPlanPath).replace(/\\/g, "/");
321
+ const artifactPath = relative(gsdProjectionRoot(basePath), taskPlanPath).replace(/\\/g, "/");
323
322
  deleteArtifactByPath(artifactPath);
324
323
  }
325
324
 
@@ -109,6 +109,7 @@ const MAX_BUFFER_BYTES = 512 * 1024;
109
109
  * Prevents CPU spinning when deltas arrive faster than regex evaluation (#468).
110
110
  */
111
111
  const JS_FALLBACK_CHECK_INTERVAL_MS = 50;
112
+ const JS_FALLBACK_THROTTLE_MIN_BUFFER_BYTES = 4 * 1024;
112
113
 
113
114
  const DEFAULT_SCOPE: TtsrScope = {
114
115
  allowText: true,
@@ -383,7 +384,10 @@ export class TtsrManager {
383
384
  // streams — regex on a growing buffer is O(rules × buffer_size) (#468).
384
385
  const now = Date.now();
385
386
  const lastCheck = this.#lastJsCheckAt.get(bufferKey) ?? 0;
386
- if (now - lastCheck < JS_FALLBACK_CHECK_INTERVAL_MS) {
387
+ if (
388
+ nextBuffer.length >= JS_FALLBACK_THROTTLE_MIN_BUFFER_BYTES &&
389
+ now - lastCheck < JS_FALLBACK_CHECK_INTERVAL_MS
390
+ ) {
387
391
  stopTimer({ bufferSize: nextBuffer.length, throttled: true });
388
392
  return [];
389
393
  }