gsd-pi 2.82.0-dev.3709f22a5 → 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 (193) hide show
  1. package/README.md +2 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +14 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +53 -29
  6. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  7. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +3 -0
  8. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +13 -19
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +13 -6
  11. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  12. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  13. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  14. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  15. package/dist/resources/extensions/gsd/auto.js +14 -8
  16. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -1
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +1 -1
  18. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  19. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  20. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  21. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  22. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  23. package/dist/resources/extensions/gsd/commands-verdict.js +139 -0
  24. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  25. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  26. package/dist/resources/extensions/gsd/forensics.js +3 -3
  27. package/dist/resources/extensions/gsd/git-service.js +6 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  30. package/dist/resources/extensions/gsd/guided-flow.js +8 -5
  31. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
  32. package/dist/resources/extensions/gsd/paths.js +4 -0
  33. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  34. package/dist/resources/extensions/gsd/state.js +3 -3
  35. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  36. package/dist/resources/extensions/gsd/templates/plan.md +1 -0
  37. package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
  38. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
  39. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  40. package/dist/resources/extensions/gsd/worktree-manager.js +1 -1
  41. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
  42. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  45. package/dist/web/standalone/.next/build-manifest.json +3 -3
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  73. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
  79. package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
  80. package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  84. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  85. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  86. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  88. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  89. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  90. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  91. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +44 -3
  93. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
  95. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +71 -97
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +25 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +24 -10
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +53 -3
  106. package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
  107. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +75 -102
  108. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +30 -1
  109. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +29 -10
  110. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  111. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  112. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  113. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  114. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  115. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  116. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  117. package/packages/pi-tui/dist/terminal.js +12 -0
  118. package/packages/pi-tui/dist/terminal.js.map +1 -1
  119. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  120. package/packages/pi-tui/src/terminal.ts +11 -0
  121. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  122. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  123. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  124. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  125. package/src/resources/extensions/gsd/auto/phases.ts +60 -36
  126. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  127. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
  128. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  129. package/src/resources/extensions/gsd/auto-dispatch.ts +13 -19
  130. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -6
  131. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  132. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  133. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  134. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  135. package/src/resources/extensions/gsd/auto.ts +13 -8
  136. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +9 -1
  137. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -1
  138. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  139. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  140. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  141. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  142. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  143. package/src/resources/extensions/gsd/commands-verdict.ts +202 -0
  144. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  145. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  146. package/src/resources/extensions/gsd/forensics.ts +3 -3
  147. package/src/resources/extensions/gsd/git-service.ts +6 -3
  148. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  149. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  150. package/src/resources/extensions/gsd/guided-flow.ts +8 -5
  151. package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
  152. package/src/resources/extensions/gsd/paths.ts +5 -0
  153. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  154. package/src/resources/extensions/gsd/state.ts +3 -3
  155. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  156. package/src/resources/extensions/gsd/templates/plan.md +1 -0
  157. package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
  158. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  159. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +139 -1
  160. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
  161. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  162. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  163. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +378 -0
  164. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  165. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  166. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  167. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  168. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
  169. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  170. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  171. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  172. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +2 -1
  173. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
  174. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
  175. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  176. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  177. package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
  178. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  179. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  180. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  181. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  182. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +17 -0
  183. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  184. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  185. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  186. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
  187. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  188. package/src/resources/extensions/gsd/worktree-manager.ts +1 -1
  189. package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
  190. package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
  191. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
  192. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → 78uanrILNOKG-Jpi4itAE}/_buildManifest.js +0 -0
  193. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → 78uanrILNOKG-Jpi4itAE}/_ssgManifest.js +0 -0
@@ -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
+ });
@@ -225,6 +225,25 @@ test("clearLock removes the session_file row for the active worker", (t) => {
225
225
  "session_file row deleted by clearLock");
226
226
  });
227
227
 
228
+ test("clearLock marks stale worker crashed when no current-process worker matches", (t) => {
229
+ const base = makeBase();
230
+ t.after(() => cleanup(base));
231
+ openDatabase(join(base, ".gsd", "gsd.db"));
232
+ const projectRoot = normalizeRealPath(base);
233
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
234
+
235
+ setRuntimeKv("worker", workerId, "session_file", "/tmp/stale-session.jsonl");
236
+ setWorkerPid(workerId, 99999);
237
+ expireWorker(workerId);
238
+ assert.ok(readCrashLock(base), "stale worker is detected before clearLock");
239
+
240
+ clearLock(base);
241
+
242
+ assert.equal(getAutoWorker(workerId)?.status, "crashed");
243
+ assert.equal(getRuntimeKv("worker", workerId, "session_file"), null);
244
+ assert.equal(readCrashLock(base), null);
245
+ });
246
+
228
247
  test("clearStaleWorkerLock crashes stale worker and cancels latest active dispatch", (t) => {
229
248
  const base = makeBase();
230
249
  t.after(() => cleanup(base));
@@ -264,3 +283,27 @@ test("clearStaleWorkerLock crashes stale worker and cancels latest active dispat
264
283
  assert.equal(getRuntimeKv("worker", workerId, "session_file"), null);
265
284
  assert.equal(readCrashLock(base), null);
266
285
  });
286
+
287
+ test("clearLock marks stale worker crashed and releases held milestone lease", (t) => {
288
+ const base = makeBase();
289
+ t.after(() => cleanup(base));
290
+ openDatabase(join(base, ".gsd", "gsd.db"));
291
+ insertMilestone({ id: "M001", title: "T", status: "active" });
292
+ const projectRoot = normalizeRealPath(base);
293
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
294
+ const lease = claimMilestoneLease(workerId, "M001");
295
+ assert.equal(lease.ok, true);
296
+ if (!lease.ok) return;
297
+
298
+ setWorkerPid(workerId, 99999);
299
+ expireWorker(workerId);
300
+ assert.ok(readCrashLock(base), "stale worker is detected before clearLock");
301
+
302
+ clearLock(base);
303
+
304
+ assert.equal(getAutoWorker(workerId)?.status, "crashed");
305
+ const leaseRow = _getAdapter()!.prepare(
306
+ `SELECT status FROM milestone_leases WHERE milestone_id = :m`,
307
+ ).get({ ":m": "M001" }) as { status: string } | undefined;
308
+ assert.equal(leaseRow?.status, "released");
309
+ });
@@ -98,6 +98,8 @@ function makeMockPi() {
98
98
  sendMessage: (...args: unknown[]) => {
99
99
  calls.push(args);
100
100
  },
101
+ getThinkingLevel: () => "off",
102
+ setThinkingLevel: () => {},
101
103
  calls,
102
104
  } as any;
103
105
  }
@@ -10,6 +10,7 @@ import assert from "node:assert/strict";
10
10
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
+ import { execFileSync } from "node:child_process";
13
14
 
14
15
  import { DISPATCH_RULES, type DispatchContext } from "../auto-dispatch.ts";
15
16
  import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
@@ -23,6 +24,14 @@ function makeBase(): string {
23
24
  return base;
24
25
  }
25
26
 
27
+ function initGitRepo(base: string): void {
28
+ execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
29
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base, stdio: "ignore" });
30
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: base, stdio: "ignore" });
31
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
32
+ execFileSync("git", ["commit", "-m", "initial"], { cwd: base, stdio: "ignore" });
33
+ }
34
+
26
35
  function buildDispatchCtx(basePath: string): DispatchContext {
27
36
  return {
28
37
  basePath,
@@ -76,6 +85,24 @@ describe("completing-milestone dispatch guard (#4324)", () => {
76
85
  assert.equal(result?.unitType, "complete-milestone");
77
86
  assert.equal(result?.unitId, "M001");
78
87
  });
88
+
89
+ test("dispatches complete-milestone when only .gsd/ files exist in git history (#5097)", async () => {
90
+ base = makeBase();
91
+ rmSync(join(base, "implementation.txt"), { force: true });
92
+ initGitRepo(base);
93
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\n");
94
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
95
+ execFileSync("git", ["commit", "-m", "chore: planning artifacts only"], { cwd: base, stdio: "ignore" });
96
+
97
+ openDatabase(join(base, ".gsd", "gsd.db"));
98
+ insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
99
+
100
+ const result = await rule.match(buildDispatchCtx(base));
101
+
102
+ assert.equal(result?.action, "dispatch");
103
+ assert.equal(result?.unitType, "complete-milestone");
104
+ assert.equal(result?.unitId, "M001");
105
+ });
79
106
  });
80
107
 
81
108
  describe("complete phase dispatch guard (#5683)", () => {
@@ -37,6 +37,7 @@ import {
37
37
  checkpointDatabase,
38
38
  refreshOpenDatabaseFromDisk,
39
39
  tryCreateMemoriesFts,
40
+ _isLikelyWslDrvFsPathForTest,
40
41
  } from '../gsd-db.ts';
41
42
  import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
42
43
 
@@ -345,6 +346,16 @@ describe('gsd-db', () => {
345
346
  });
346
347
  });
347
348
 
349
+ test('gsd-db: detects WSL DrvFs mount paths for conservative pragmas', () => {
350
+ withPlatform('linux', () => {
351
+ assert.equal(_isLikelyWslDrvFsPathForTest('/mnt/d/code/project/.gsd/gsd.db'), true);
352
+ assert.equal(_isLikelyWslDrvFsPathForTest('/tmp/gsd.db'), false);
353
+ });
354
+ withPlatform('darwin', () => {
355
+ assert.equal(_isLikelyWslDrvFsPathForTest('/mnt/d/code/project/.gsd/gsd.db'), false);
356
+ });
357
+ });
358
+
348
359
  test('gsd-db: transaction rollback on error', () => {
349
360
  openDatabase(':memory:');
350
361
 
@@ -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], {
@@ -1168,6 +1168,15 @@ describe('git-service', async () => {
1168
1168
  rmSync(repo, { recursive: true, force: true });
1169
1169
  });
1170
1170
 
1171
+ test('Integration branch: rejects milestone branches', () => {
1172
+ const repo = initBranchTestRepo();
1173
+
1174
+ writeIntegrationBranch(repo, "M001", "milestone/M001");
1175
+ assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "milestone branches are not recorded as integration branch");
1176
+
1177
+ rmSync(repo, { recursive: true, force: true });
1178
+ });
1179
+
1171
1180
  // ─── writeIntegrationBranch: still records legitimate branches ────────
1172
1181
 
1173
1182
  test('Integration branch: records non-ephemeral gsd branches', () => {
@@ -22,6 +22,7 @@ import type { IterationContext, LoopState, PreDispatchData, IterationData } from
22
22
  import type { SessionLockStatus } from "../session-lock.js";
23
23
  import { runDispatch, runUnitPhase, runPreDispatch, runFinalize } from "../auto/phases.js";
24
24
  import { readUnitRuntimeRecord } from "../unit-runtime.js";
25
+ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js";
25
26
  import {
26
27
  closeDatabase,
27
28
  insertMilestone,
@@ -160,6 +161,8 @@ function makeIC(
160
161
  pi: {
161
162
  sendMessage: () => {},
162
163
  setModel: async () => true,
164
+ getThinkingLevel: () => "off",
165
+ setThinkingLevel: () => {},
163
166
  } as any,
164
167
  s: makeSession(),
165
168
  deps,
@@ -868,6 +871,49 @@ test("runUnitPhase increments unitDispatchCount for repeated artifact-missing re
868
871
  assert.equal(ic.s.unitDispatchCount.get("execute-task/M001/S01/T01"), 2);
869
872
  });
870
873
 
874
+ test("runUnitPhase pre-dispatch model validation failures do not emit unit-start or dispatch runtime state", async (t) => {
875
+ const capture = createEventCapture();
876
+ const base = mkdtempSync(join(tmpdir(), `gsd-pre-dispatch-block-${randomUUID()}`));
877
+ t.after(() => rmSync(base, { recursive: true, force: true }));
878
+
879
+ const deps = makeMockDeps(capture, {
880
+ selectAndApplyModel: async () => {
881
+ throw new ModelPolicyDispatchBlockedError("execute-task", "M001/S01/T01", []);
882
+ },
883
+ });
884
+ const ic = makeIC(deps, {
885
+ s: {
886
+ ...makeSession(),
887
+ basePath: base,
888
+ } as any,
889
+ });
890
+ const iterData: IterationData = {
891
+ unitType: "execute-task",
892
+ unitId: "M001/S01/T01",
893
+ prompt: "do stuff",
894
+ finalPrompt: "do stuff",
895
+ pauseAfterUatDispatch: false,
896
+ state: { phase: "executing", activeMilestone: { id: "M001" }, activeSlice: { id: "S01" }, registry: [], blockers: [] } as any,
897
+ mid: "M001",
898
+ midTitle: "Test",
899
+ isRetry: false,
900
+ previousTier: undefined,
901
+ };
902
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
903
+
904
+ await assert.rejects(() => runUnitPhase(ic, iterData, loopState), ModelPolicyDispatchBlockedError);
905
+ await assert.rejects(() => runUnitPhase(ic, iterData, loopState), ModelPolicyDispatchBlockedError);
906
+
907
+ const startEvents = capture.events.filter(e => e.eventType === "unit-start");
908
+ assert.equal(startEvents.length, 0, "pre-dispatch validation failures must not emit unit-start");
909
+ assert.equal(ic.s.unitDispatchCount.get("execute-task/M001/S01/T01") ?? 0, 0, "dispatch count must not increment on pre-dispatch validation failure");
910
+ assert.equal(
911
+ readUnitRuntimeRecord(base, "execute-task", "M001/S01/T01"),
912
+ null,
913
+ "pre-dispatch validation failures must not persist a dispatched runtime record",
914
+ );
915
+ });
916
+
871
917
  test("all events from a mock iteration have monotonically increasing seq and same flowId", async () => {
872
918
  const capture = createEventCapture();
873
919
  const { resolveAgentEnd, _resetPendingResolve } = await import("../auto/resolve.js");