gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5

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 (182) hide show
  1. package/README.md +1 -1
  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/phases.js +73 -30
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
  8. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  10. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  11. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  12. package/dist/resources/extensions/gsd/auto.js +7 -2
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  17. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  18. package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
  19. package/dist/resources/extensions/gsd/git-service.js +6 -2
  20. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  21. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  22. package/dist/resources/extensions/gsd/guided-flow.js +95 -116
  23. package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
  24. package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
  25. package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  29. package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  31. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  32. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
  34. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  36. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  37. package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
  40. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -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 +9 -9
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +1 -1
  77. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  79. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  80. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  84. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  85. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  86. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  90. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
  91. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  93. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  94. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  95. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  96. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  97. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  98. package/packages/pi-tui/dist/terminal.js +12 -0
  99. package/packages/pi-tui/dist/terminal.js.map +1 -1
  100. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  101. package/packages/pi-tui/src/terminal.ts +11 -0
  102. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  103. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  104. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  105. package/src/resources/extensions/gsd/auto/phases.ts +83 -37
  106. package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
  107. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  108. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
  109. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  110. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  111. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  112. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  113. package/src/resources/extensions/gsd/auto.ts +8 -2
  114. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
  115. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
  116. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  117. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  118. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  119. package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
  120. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  121. package/src/resources/extensions/gsd/git-service.ts +6 -3
  122. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  124. package/src/resources/extensions/gsd/guided-flow.ts +128 -133
  125. package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
  126. package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
  127. package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
  128. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
  131. package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
  134. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  135. package/src/resources/extensions/gsd/prompts/queue.md +4 -4
  136. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  137. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  139. package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
  140. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
  141. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
  142. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  143. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  145. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
  146. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
  147. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  148. package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
  149. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  150. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
  151. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
  152. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  153. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  154. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
  155. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  156. package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
  159. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
  160. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
  161. package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
  162. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
  163. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  164. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  165. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  166. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
  167. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
  168. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
  171. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  172. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  173. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  174. package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
  175. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
  176. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
  177. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  178. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  179. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  180. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  181. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
@@ -110,6 +110,49 @@ test("completing-milestone blocks when VALIDATION verdict is needs-attention (#5
110
110
  }
111
111
  });
112
112
 
113
+ test("completing-milestone blocks when VALIDATION verdict is fail (#5920)", async () => {
114
+ const base = mkdtempSync(join(tmpdir(), "gsd-fail-verdict-"));
115
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
116
+
117
+ try {
118
+ writeFileSync(
119
+ join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md"),
120
+ [
121
+ "---",
122
+ "verdict: fail",
123
+ "---",
124
+ "",
125
+ "# Validation Report",
126
+ "",
127
+ "Blocking failures remain unresolved.",
128
+ ].join("\n"),
129
+ );
130
+
131
+ const ctx = {
132
+ mid: "M001",
133
+ midTitle: "Test Milestone",
134
+ basePath: base,
135
+ state: { phase: "completing-milestone" } as any,
136
+ prefs: {} as any,
137
+ session: undefined,
138
+ };
139
+
140
+ const result = await completingRule!.match(ctx);
141
+
142
+ assert.ok(result !== null, "rule should match");
143
+ assert.equal(result!.action, "stop", "should return stop action");
144
+ if (result!.action === "stop") {
145
+ assert.equal(result!.level, "warning", "should be warning level (pausable)");
146
+ assert.ok(
147
+ result!.reason.includes('verdict is "fail"'),
148
+ "reason should mention fail verdict",
149
+ );
150
+ }
151
+ } finally {
152
+ rmSync(base, { recursive: true, force: true });
153
+ }
154
+ });
155
+
113
156
  test("completing-milestone proceeds normally when VALIDATION verdict is pass (#2675 guard)", async () => {
114
157
  const base = mkdtempSync(join(tmpdir(), "gsd-remediation-"));
115
158
  mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
@@ -36,7 +36,7 @@ function makeUatProject(): string {
36
36
  return base;
37
37
  }
38
38
 
39
- test("run-uat dispatch stops after three attempts without a verdict", async () => {
39
+ test("run-uat dispatch skips after three attempts without a verdict", async () => {
40
40
  const basePath = makeUatProject();
41
41
  const rule = DISPATCH_RULES.find((r) => r.name === "run-uat (post-completion)");
42
42
  assert.ok(rule, "run-uat dispatch rule is registered");
@@ -58,8 +58,7 @@ test("run-uat dispatch stops after three attempts without a verdict", async () =
58
58
  }
59
59
 
60
60
  const capped = await rule.match(ctx as any);
61
- assert.equal(capped?.action, "stop");
62
- assert.match(capped?.reason ?? "", /dispatched 3 times/);
61
+ assert.equal(capped?.action, "skip");
63
62
  assert.equal(getUatCount(basePath, "M001", "S01"), 4);
64
63
  } finally {
65
64
  rmSync(basePath, { recursive: true, force: true });
@@ -0,0 +1,113 @@
1
+ // GSD-2 — Smart entry routing behavior tests.
2
+ // Verifies guided wizard choices resolve to the correct execution path.
3
+
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+
7
+ import {
8
+ resolveActiveTaskChoiceRoute,
9
+ resolveGuidedExecuteLaunchMode,
10
+ type ActiveTaskChoice,
11
+ type ActiveTaskRoute,
12
+ type SmartEntryIsolationMode,
13
+ } from "../smart-entry-routing.ts";
14
+
15
+ test("guided execute route enters auto step bootstrap only for worktree isolation", () => {
16
+ const cases: Array<{
17
+ isolationMode: SmartEntryIsolationMode;
18
+ expectedRoute: ActiveTaskRoute;
19
+ }> = [
20
+ {
21
+ isolationMode: "worktree",
22
+ expectedRoute: {
23
+ kind: "auto-bootstrap",
24
+ verboseMode: false,
25
+ options: {
26
+ step: true,
27
+ milestoneLock: "M001",
28
+ },
29
+ },
30
+ },
31
+ {
32
+ isolationMode: "none",
33
+ expectedRoute: {
34
+ kind: "guided-dispatch",
35
+ unitType: "execute-task",
36
+ },
37
+ },
38
+ {
39
+ isolationMode: "branch",
40
+ expectedRoute: {
41
+ kind: "guided-dispatch",
42
+ unitType: "execute-task",
43
+ },
44
+ },
45
+ ];
46
+
47
+ for (const testCase of cases) {
48
+ assert.deepEqual(
49
+ resolveActiveTaskChoiceRoute({
50
+ choice: "execute",
51
+ isolationMode: testCase.isolationMode,
52
+ milestoneId: "M001",
53
+ }),
54
+ testCase.expectedRoute,
55
+ );
56
+ }
57
+ });
58
+
59
+ test("active task smart entry choices resolve to explicit routes", () => {
60
+ const cases: Array<{
61
+ choice: ActiveTaskChoice;
62
+ expectedRoute: ActiveTaskRoute;
63
+ }> = [
64
+ {
65
+ choice: "auto",
66
+ expectedRoute: {
67
+ kind: "auto-bootstrap",
68
+ verboseMode: false,
69
+ },
70
+ },
71
+ {
72
+ choice: "status",
73
+ expectedRoute: {
74
+ kind: "status",
75
+ },
76
+ },
77
+ {
78
+ choice: "milestone_actions",
79
+ expectedRoute: {
80
+ kind: "milestone-actions",
81
+ },
82
+ },
83
+ ];
84
+
85
+ for (const testCase of cases) {
86
+ assert.deepEqual(
87
+ resolveActiveTaskChoiceRoute({
88
+ choice: testCase.choice,
89
+ isolationMode: "worktree",
90
+ milestoneId: "M001",
91
+ }),
92
+ testCase.expectedRoute,
93
+ );
94
+ }
95
+ });
96
+
97
+ test("active task route rejects invalid choices from untyped callers", () => {
98
+ assert.throws(
99
+ () =>
100
+ resolveActiveTaskChoiceRoute({
101
+ choice: "not_yet" as ActiveTaskChoice,
102
+ isolationMode: "worktree",
103
+ milestoneId: "M001",
104
+ }),
105
+ /Invalid ActiveTaskChoice: not_yet/,
106
+ );
107
+ });
108
+
109
+ test("guided execute launch mode remains a small compatibility helper", () => {
110
+ assert.equal(resolveGuidedExecuteLaunchMode("worktree"), "auto-step");
111
+ assert.equal(resolveGuidedExecuteLaunchMode("none"), "guided-dispatch");
112
+ assert.equal(resolveGuidedExecuteLaunchMode("branch"), "guided-dispatch");
113
+ });
@@ -4,7 +4,10 @@ import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
 
6
6
  import { _withDetachedAutoKeepaliveForTest } from "../auto.ts";
7
- import { _scheduleAutoStartAfterIdleForTest } from "../guided-flow.ts";
7
+ import {
8
+ _scheduleAutoStartAfterIdleForTest,
9
+ resolveGuidedExecuteLaunchMode,
10
+ } from "../guided-flow.ts";
8
11
 
9
12
  const gsdDir = resolve(import.meta.dirname, "..");
10
13
 
@@ -64,6 +67,24 @@ test("bare /gsd stays in the foreground smart-entry flow (#5125 regression)", ()
64
67
  );
65
68
  });
66
69
 
70
+ test("guided execute uses auto step bootstrap when worktree isolation is enabled", () => {
71
+ assert.equal(
72
+ resolveGuidedExecuteLaunchMode("worktree"),
73
+ "auto-step",
74
+ "guided execute must enter auto bootstrap so the milestone worktree is created before execution",
75
+ );
76
+ assert.equal(
77
+ resolveGuidedExecuteLaunchMode("none"),
78
+ "guided-dispatch",
79
+ "non-isolated projects can keep the foreground guided dispatch path",
80
+ );
81
+ assert.equal(
82
+ resolveGuidedExecuteLaunchMode("branch"),
83
+ "guided-dispatch",
84
+ "this regression fix is scoped to worktree isolation",
85
+ );
86
+ });
87
+
67
88
  test("auto bootstrap validates blocked directories before touching .gsd migration state", () => {
68
89
  const autoSrc = readGsdFile("auto.ts");
69
90
  const autoStartSrc = readGsdFile("auto-start.ts");
@@ -544,7 +544,7 @@ test("ADR-017 (#5703): live worker lock is not cleared", async (t) => {
544
544
 
545
545
  // ─── #5704: unregistered-milestone drift ────────────────────────────────────
546
546
 
547
- test("ADR-017 (#5704): unregistered-milestone drift detected and DB row inserted", async (t) => {
547
+ test("ADR-017 (#5704): unregistered-milestone drift fails closed without importing markdown", async (t) => {
548
548
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-projmd-"));
549
549
  const milestoneDir = join(base, ".gsd", "milestones", "M042");
550
550
  mkdirSync(milestoneDir, { recursive: true });
@@ -571,20 +571,22 @@ test("ADR-017 (#5704): unregistered-milestone drift detected and DB row inserted
571
571
  // Pre-condition: filesystem has the milestone, DB does NOT.
572
572
  assert.equal(getMilestone("M042"), null, "pre: DB has no row for M042");
573
573
 
574
- const result = await reconcileBeforeDispatch(base, {
575
- invalidateStateCache: () => {},
576
- deriveState: async () => makeState(),
577
- });
578
-
579
- assert.equal(result.ok, true);
580
- assert.ok(getMilestone("M042"), "post: DB row inserted for M042");
581
- const milestoneRepaired = result.repaired.find(
582
- (d) => d.kind === "unregistered-milestone",
574
+ await assert.rejects(
575
+ reconcileBeforeDispatch(base, {
576
+ invalidateStateCache: () => {},
577
+ deriveState: async () => makeState(),
578
+ }),
579
+ (err: unknown) => {
580
+ assert.ok(err instanceof ReconciliationFailedError);
581
+ assert.match(String(err.message), /unregistered-milestone/);
582
+ assert.equal(err.failures[0]?.drift.kind, "unregistered-milestone");
583
+ assert.match(String(err.failures[0]?.cause), /M042/);
584
+ assert.match(String(err.failures[0]?.cause), /markdown projection/);
585
+ assert.match(String(err.failures[0]?.cause), /recovery\/migration/);
586
+ return true;
587
+ },
583
588
  );
584
- assert.ok(milestoneRepaired, "repaired list should include the unregistered-milestone drift");
585
- if (milestoneRepaired?.kind === "unregistered-milestone") {
586
- assert.equal(milestoneRepaired.milestoneId, "M042");
587
- }
589
+ assert.equal(getMilestone("M042"), null, "post: DB still has no row for M042");
588
590
  });
589
591
 
590
592
  test("ADR-017 (#5704): registered milestone (DB row present) → no drift", async (t) => {
@@ -627,13 +629,14 @@ test("ADR-017 (#5704): registered milestone (DB row present) → no drift", asyn
627
629
 
628
630
  // ─── #5705: roadmap-divergence drift ─────────────────────────────────────────
629
631
 
630
- test("ADR-017 (#5705): roadmap-divergence drift detected and DB depends synced", async (t) => {
632
+ test("ADR-017 (#5705): roadmap-divergence re-renders projection without syncing depends into DB", async (t) => {
631
633
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-roadmap-"));
632
634
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
635
+ const roadmapPath = join(milestoneDir, "M001-ROADMAP.md");
633
636
  mkdirSync(milestoneDir, { recursive: true });
634
637
  // ROADMAP.md declares S02 depends on [S01]
635
638
  writeFileSync(
636
- join(milestoneDir, "M001-ROADMAP.md"),
639
+ roadmapPath,
637
640
  [
638
641
  "# M001: Test",
639
642
  "",
@@ -665,7 +668,12 @@ test("ADR-017 (#5705): roadmap-divergence drift detected and DB depends synced",
665
668
  });
666
669
 
667
670
  assert.equal(result.ok, true);
668
- assert.deepEqual(getSlice("M001", "S02")?.depends, ["S01"], "post: DB depends matches ROADMAP.md");
671
+ assert.deepEqual(getSlice("M001", "S02")?.depends, [], "post: DB depends remains authoritative");
672
+ assert.match(
673
+ readFileSync(roadmapPath, "utf-8"),
674
+ /- \[ \] \*\*S02: Feature\*\* `risk:medium` `depends:\[\]`/,
675
+ "post: ROADMAP projection is regenerated from DB depends",
676
+ );
669
677
  const roadmapRepaired = result.repaired.find((d) => d.kind === "roadmap-divergence");
670
678
  assert.ok(roadmapRepaired, "repaired list should include the roadmap-divergence drift");
671
679
  if (roadmapRepaired?.kind === "roadmap-divergence") {
@@ -673,13 +681,14 @@ test("ADR-017 (#5705): roadmap-divergence drift detected and DB depends synced",
673
681
  }
674
682
  });
675
683
 
676
- test("ADR-017 (#5705): ROADMAP declares slice missing from DB slice inserted and drift reported", async (t) => {
684
+ test("ADR-017 (#5705): ROADMAP-only slice is removed from projection and not inserted into DB", async (t) => {
677
685
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-roadmap-newslice-"));
678
686
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
687
+ const roadmapPath = join(milestoneDir, "M001-ROADMAP.md");
679
688
  mkdirSync(milestoneDir, { recursive: true });
680
689
  // ROADMAP.md declares S01 and S02; DB will only have S01.
681
690
  writeFileSync(
682
- join(milestoneDir, "M001-ROADMAP.md"),
691
+ roadmapPath,
683
692
  [
684
693
  "# M001: Test",
685
694
  "",
@@ -710,10 +719,10 @@ test("ADR-017 (#5705): ROADMAP declares slice missing from DB → slice inserted
710
719
  });
711
720
 
712
721
  assert.equal(result.ok, true);
713
- const s02 = getSlice("M001", "S02");
714
- assert.ok(s02, "post: S02 inserted into DB after repair");
715
- assert.equal(s02?.sequence, 2, "post: S02 sequence matches ROADMAP order");
716
- assert.deepEqual(s02?.depends, ["S01"], "post: S02 depends matches ROADMAP");
722
+ assert.equal(getSlice("M001", "S02"), null, "post: S02 still has no DB row");
723
+ const rendered = readFileSync(roadmapPath, "utf-8");
724
+ assert.match(rendered, /- \[ \] \*\*S01: Foundation\*\*/);
725
+ assert.doesNotMatch(rendered, /S02: Feature/, "post: ROADMAP-only S02 removed from projection");
717
726
  const roadmapRepaired = result.repaired.find((d) => d.kind === "roadmap-divergence");
718
727
  assert.ok(roadmapRepaired, "repaired list should include the roadmap-divergence drift");
719
728
  if (roadmapRepaired?.kind === "roadmap-divergence") {
@@ -721,6 +730,93 @@ test("ADR-017 (#5705): ROADMAP declares slice missing from DB → slice inserted
721
730
  }
722
731
  });
723
732
 
733
+ test("ADR-017 (#5705): ROADMAP sequence drift re-renders from DB order without mutating DB", async (t) => {
734
+ const base = mkdtempSync(join(tmpdir(), "gsd-adr017-roadmap-sequence-"));
735
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
736
+ const roadmapPath = join(milestoneDir, "M001-ROADMAP.md");
737
+ mkdirSync(milestoneDir, { recursive: true });
738
+ writeFileSync(
739
+ roadmapPath,
740
+ [
741
+ "# M001: Test",
742
+ "",
743
+ "**Vision:** Verify sequence drift",
744
+ "",
745
+ "## Slices",
746
+ "",
747
+ "- [ ] **S02: Feature** `risk:medium` `depends:[]`",
748
+ "- [ ] **S01: Foundation** `risk:medium` `depends:[]`",
749
+ "",
750
+ ].join("\n"),
751
+ );
752
+ t.after(() => {
753
+ try { closeDatabase(); } catch { /* noop */ }
754
+ rmSync(base, { recursive: true, force: true });
755
+ });
756
+
757
+ openDatabase(join(base, ".gsd", "gsd.db"));
758
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
759
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Foundation", status: "pending", risk: "medium", depends: [], demo: "", sequence: 1 });
760
+ insertSlice({ id: "S02", milestoneId: "M001", title: "Feature", status: "pending", risk: "medium", depends: [], demo: "", sequence: 2 });
761
+
762
+ const result = await reconcileBeforeDispatch(base, {
763
+ invalidateStateCache: () => {},
764
+ deriveState: async () => makeState(),
765
+ });
766
+
767
+ assert.equal(result.ok, true);
768
+ assert.equal(getSlice("M001", "S01")?.sequence, 1, "post: S01 DB sequence remains authoritative");
769
+ assert.equal(getSlice("M001", "S02")?.sequence, 2, "post: S02 DB sequence remains authoritative");
770
+ const rendered = readFileSync(roadmapPath, "utf-8");
771
+ assert.ok(
772
+ rendered.indexOf("S01: Foundation") < rendered.indexOf("S02: Feature"),
773
+ "post: ROADMAP projection follows DB sequence",
774
+ );
775
+ assert.ok(result.repaired.some((d) => d.kind === "roadmap-divergence"));
776
+ });
777
+
778
+ test("ADR-017 (#5705): ROADMAP checkbox drift re-renders from DB status without mutating DB", async (t) => {
779
+ const base = mkdtempSync(join(tmpdir(), "gsd-adr017-roadmap-checkbox-"));
780
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
781
+ const roadmapPath = join(milestoneDir, "M001-ROADMAP.md");
782
+ mkdirSync(milestoneDir, { recursive: true });
783
+ writeFileSync(
784
+ roadmapPath,
785
+ [
786
+ "# M001: Test",
787
+ "",
788
+ "**Vision:** Verify checkbox drift",
789
+ "",
790
+ "## Slices",
791
+ "",
792
+ "- [x] **S01: Foundation** `risk:medium` `depends:[]`",
793
+ "",
794
+ ].join("\n"),
795
+ );
796
+ t.after(() => {
797
+ try { closeDatabase(); } catch { /* noop */ }
798
+ rmSync(base, { recursive: true, force: true });
799
+ });
800
+
801
+ openDatabase(join(base, ".gsd", "gsd.db"));
802
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
803
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Foundation", status: "pending", risk: "medium", depends: [], demo: "", sequence: 1 });
804
+
805
+ const result = await reconcileBeforeDispatch(base, {
806
+ invalidateStateCache: () => {},
807
+ deriveState: async () => makeState(),
808
+ });
809
+
810
+ assert.equal(result.ok, true);
811
+ assert.equal(getSlice("M001", "S01")?.status, "pending", "post: DB status remains authoritative");
812
+ assert.match(
813
+ readFileSync(roadmapPath, "utf-8"),
814
+ /- \[ \] \*\*S01: Foundation\*\*/,
815
+ "post: ROADMAP checkbox reflects DB status",
816
+ );
817
+ assert.ok(result.repaired.some((d) => d.kind === "roadmap-divergence"));
818
+ });
819
+
724
820
  test("ADR-017 (#5705): in-sync ROADMAP and DB → no roadmap-divergence drift", async (t) => {
725
821
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-roadmap-clean-"));
726
822
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
@@ -3,7 +3,7 @@
3
3
  import test from 'node:test';
4
4
  import assert from 'node:assert/strict';
5
5
 
6
- import { isClosedStatus } from '../status-guards.ts';
6
+ import { isClosedStatus, isFutureMilestoneStatus } from '../status-guards.ts';
7
7
 
8
8
  test('isClosedStatus: "complete" returns true', () => {
9
9
  assert.equal(isClosedStatus('complete'), true);
@@ -32,3 +32,15 @@ test('isClosedStatus: "active" returns false', () => {
32
32
  test('isClosedStatus: "" (empty string) returns false', () => {
33
33
  assert.equal(isClosedStatus(''), false);
34
34
  });
35
+
36
+ test('isFutureMilestoneStatus includes future milestone aliases', () => {
37
+ assert.equal(isFutureMilestoneStatus('pending'), true);
38
+ assert.equal(isFutureMilestoneStatus('queued'), true);
39
+ assert.equal(isFutureMilestoneStatus('planned'), true);
40
+ });
41
+
42
+ test('isFutureMilestoneStatus excludes active and closed milestones', () => {
43
+ assert.equal(isFutureMilestoneStatus('active'), false);
44
+ assert.equal(isFutureMilestoneStatus('complete'), false);
45
+ assert.equal(isFutureMilestoneStatus('parked'), false);
46
+ });
@@ -162,7 +162,7 @@ describe("validate-milestone stuck-loop guard (#4094)", () => {
162
162
  assert.equal(pauseAutoMock.mock.callCount(), 0);
163
163
  });
164
164
 
165
- test("continues when no VALIDATION file exists yet", async () => {
165
+ test("retries when no VALIDATION file exists yet", async () => {
166
166
  insertMilestone({ id: "M001" });
167
167
  insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
168
168
 
@@ -173,7 +173,34 @@ describe("validate-milestone stuck-loop guard (#4094)", () => {
173
173
 
174
174
  const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
175
175
 
176
- assert.equal(result, "continue");
176
+ assert.equal(result, "retry");
177
+ assert.equal(pauseAutoMock.mock.callCount(), 0);
178
+ assert.ok(s.pendingVerificationRetry);
179
+ assert.equal(s.pendingVerificationRetry!.unitId, "M001");
180
+ assert.match(s.pendingVerificationRetry!.failureContext, /gsd_validate_milestone/);
181
+ assert.equal(s.pendingVerificationRetry!.attempt, 1);
182
+ });
183
+
184
+ test("retries when VALIDATION file exists but is empty", async () => {
185
+ insertMilestone({ id: "M001" });
186
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
187
+
188
+ const path = join(tempDir, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
189
+ writeFileSync(path, "", "utf-8");
190
+ invalidateAllCaches();
191
+
192
+ const ctx = makeMockCtx();
193
+ const pi = makeMockPi();
194
+ const pauseAutoMock = mock.fn(async () => {});
195
+ const s = makeMockSession(tempDir, "validate-milestone", "M001");
196
+
197
+ const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
198
+
199
+ assert.equal(result, "retry");
177
200
  assert.equal(pauseAutoMock.mock.callCount(), 0);
201
+ assert.ok(s.pendingVerificationRetry);
202
+ assert.equal(s.pendingVerificationRetry!.unitId, "M001");
203
+ assert.match(s.pendingVerificationRetry!.failureContext, /exists but is empty/);
204
+ assert.equal(s.pendingVerificationRetry!.attempt, 1);
178
205
  });
179
206
  });
@@ -746,3 +746,21 @@ test("transport compatibility still blocks units whose MCP tools are not exposed
746
746
  assert.match(error ?? "", /requires secure_env_collect/);
747
747
  assert.match(error ?? "", /currently exposes only/);
748
748
  });
749
+
750
+ test("transport compatibility accepts MCP-namespaced runtime tools", () => {
751
+ const error = getWorkflowTransportSupportError(
752
+ "claude-code",
753
+ ["gsd_summary_save"],
754
+ {
755
+ projectRoot: "/tmp/project",
756
+ env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
757
+ surface: "auto-mode",
758
+ unitType: "research-slice",
759
+ authMode: "externalCli",
760
+ baseUrl: "local://claude-code",
761
+ activeTools: ["mcp__gsd-workflow__gsd_summary_save"],
762
+ },
763
+ );
764
+
765
+ assert.equal(error, null);
766
+ });
@@ -18,6 +18,7 @@ export interface WorkflowCapabilityOptions {
18
18
  unitType?: string;
19
19
  authMode?: "apiKey" | "oauth" | "externalCli" | "none";
20
20
  baseUrl?: string;
21
+ activeTools?: string[];
21
22
  }
22
23
 
23
24
  const MCP_WORKFLOW_TOOL_SURFACE = new Set([
@@ -383,6 +384,15 @@ function hasAskUserQuestionsTool(activeTools: string[]): boolean {
383
384
  });
384
385
  }
385
386
 
387
+ function hasRequiredTool(requiredTool: string, activeTools: string[]): boolean {
388
+ return activeTools.some((toolName) => {
389
+ if (toolName === requiredTool) return true;
390
+ if (!toolName.startsWith("mcp__")) return false;
391
+ const toolSeparator = toolName.indexOf("__", "mcp__".length);
392
+ return toolSeparator >= 0 && toolName.slice(toolSeparator + 2) === requiredTool;
393
+ });
394
+ }
395
+
386
396
  function workflowMcpStructuredQuestionsOptIn(env: NodeJS.ProcessEnv = process.env): boolean {
387
397
  const value = env.GSD_WORKFLOW_MCP_STRUCTURED_QUESTIONS;
388
398
  return value === "1" || value === "true";
@@ -423,8 +433,15 @@ export function getWorkflowTransportSupportError(
423
433
  return `Provider ${providerLabel} cannot run ${surface}${unitLabel}: the GSD workflow MCP server is not configured or discoverable. Detected Claude Code model but no workflow MCP. Please run /gsd mcp init . from your project root. You can also configure GSD_WORKFLOW_MCP_COMMAND, build packages/mcp-server/dist/cli.js, or install gsd-mcp-server on PATH.`;
424
434
  }
425
435
 
426
- const missing = [...new Set(requiredTools)].filter((tool) => !MCP_WORKFLOW_TOOL_SURFACE.has(tool));
436
+ const uniqueRequired = [...new Set(requiredTools)];
437
+ const missing = (options.activeTools && options.activeTools.length > 0)
438
+ ? uniqueRequired.filter((tool) => !hasRequiredTool(tool, options.activeTools!))
439
+ : uniqueRequired.filter((tool) => !MCP_WORKFLOW_TOOL_SURFACE.has(tool));
427
440
  if (missing.length === 0) return null;
428
441
 
442
+ if (options.activeTools && options.activeTools.length > 0) {
443
+ return `Provider ${providerLabel} cannot run ${surface}${unitLabel}: this unit requires ${missing.join(", ")}, but the active runtime toolset currently exposes only ${options.activeTools.slice().sort().join(", ")}.`;
444
+ }
445
+
429
446
  return `Provider ${providerLabel} cannot run ${surface}${unitLabel}: this unit requires ${missing.join(", ")}, but the workflow MCP transport currently exposes only ${Array.from(MCP_WORKFLOW_TOOL_SURFACE).sort().join(", ")}.`;
430
447
  }