@opengsd/gsd-pi 1.2.0-dev.d6c5343c → 1.2.0-dev.ddc97c10

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 (225) hide show
  1. package/dist/mcp-server.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/orchestrator.js +28 -10
  4. package/dist/resources/extensions/gsd/auto/phases.js +47 -4
  5. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +3 -2
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -2
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +11 -7
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +18 -6
  10. package/dist/resources/extensions/gsd/auto-unit-closeout.js +45 -21
  11. package/dist/resources/extensions/gsd/auto-verification.js +14 -2
  12. package/dist/resources/extensions/gsd/auto.js +37 -1
  13. package/dist/resources/extensions/gsd/blocked-models.js +28 -0
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +26 -6
  15. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  16. package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
  17. package/dist/resources/extensions/gsd/commands/context.js +16 -2
  18. package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
  19. package/dist/resources/extensions/gsd/consent-question.js +16 -0
  20. package/dist/resources/extensions/gsd/crash-recovery.js +8 -3
  21. package/dist/resources/extensions/gsd/doctor-engine-checks.js +3 -3
  22. package/dist/resources/extensions/gsd/doctor-git-checks.js +2 -18
  23. package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
  24. package/dist/resources/extensions/gsd/gsd-db.js +2 -1
  25. package/dist/resources/extensions/gsd/guided-flow.js +6 -3
  26. package/dist/resources/extensions/gsd/milestone-closeout.js +73 -2
  27. package/dist/resources/extensions/gsd/milestone-planning-persistence.js +2 -2
  28. package/dist/resources/extensions/gsd/projection-flush.js +7 -0
  29. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -1
  34. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +1 -1
  43. package/dist/resources/extensions/gsd/roadmap-slices.js +25 -3
  44. package/dist/resources/extensions/gsd/session-lock.js +1 -1
  45. package/dist/resources/extensions/gsd/tool-contract.js +14 -3
  46. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  47. package/dist/resources/extensions/gsd/tools/complete-slice.js +2 -2
  48. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  49. package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -2
  50. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -2
  51. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +2 -2
  52. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +2 -2
  53. package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -2
  54. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -2
  55. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -2
  56. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
  57. package/dist/resources/extensions/gsd/verification-verdict.js +2 -1
  58. package/dist/resources/extensions/shared/gsd-browser-cli.js +21 -2
  59. package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
  60. package/dist/resources/shared/package-manager-detection.js +1 -1
  61. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  62. package/dist/update-check.d.ts +2 -0
  63. package/dist/update-check.js +24 -1
  64. package/dist/update-cmd.js +20 -3
  65. package/dist/web/standalone/.next/BUILD_ID +1 -1
  66. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  67. package/dist/web/standalone/.next/build-manifest.json +2 -2
  68. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  69. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.html +1 -1
  86. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  93. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  94. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  96. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  97. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  98. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  99. package/package.json +1 -1
  100. package/packages/cloud-mcp-gateway/package.json +2 -2
  101. package/packages/contracts/package.json +1 -1
  102. package/packages/daemon/package.json +4 -4
  103. package/packages/gsd-agent-core/package.json +5 -5
  104. package/packages/gsd-agent-modes/package.json +7 -7
  105. package/packages/mcp-server/dist/cli.js +10 -5
  106. package/packages/mcp-server/dist/cli.js.map +1 -1
  107. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
  108. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
  109. package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
  110. package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
  111. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  112. package/packages/mcp-server/dist/server.js +4 -0
  113. package/packages/mcp-server/dist/server.js.map +1 -1
  114. package/packages/mcp-server/dist/workflow-tools.d.ts +18 -18
  115. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  116. package/packages/mcp-server/dist/workflow-tools.js +99 -38
  117. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  118. package/packages/mcp-server/package.json +5 -4
  119. package/packages/native/package.json +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/index.d.ts +2 -0
  122. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/index.js +2 -0
  124. package/packages/pi-ai/dist/index.js.map +1 -1
  125. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  126. package/packages/pi-ai/dist/providers/anthropic.js +12 -7
  127. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  128. package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
  129. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  130. package/packages/pi-ai/dist/providers/google-shared.js +12 -3
  131. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  132. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  133. package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
  134. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  135. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
  136. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
  137. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
  138. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
  139. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  140. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
  141. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  142. package/packages/pi-ai/package.json +1 -1
  143. package/packages/pi-coding-agent/package.json +7 -7
  144. package/packages/pi-tui/package.json +2 -2
  145. package/packages/rpc-client/package.json +2 -2
  146. package/pkg/package.json +1 -1
  147. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +11 -0
  148. package/src/resources/extensions/gsd/auto/orchestrator.ts +28 -10
  149. package/src/resources/extensions/gsd/auto/phases.ts +63 -24
  150. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  151. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +10 -16
  152. package/src/resources/extensions/gsd/auto-dispatch.ts +11 -10
  153. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -7
  154. package/src/resources/extensions/gsd/auto-post-unit.ts +21 -6
  155. package/src/resources/extensions/gsd/auto-unit-closeout.ts +83 -28
  156. package/src/resources/extensions/gsd/auto-verification.ts +18 -2
  157. package/src/resources/extensions/gsd/auto.ts +44 -1
  158. package/src/resources/extensions/gsd/blocked-models.ts +49 -0
  159. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -5
  160. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  161. package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
  162. package/src/resources/extensions/gsd/commands/context.ts +16 -2
  163. package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
  164. package/src/resources/extensions/gsd/consent-question.ts +15 -0
  165. package/src/resources/extensions/gsd/crash-recovery.ts +10 -2
  166. package/src/resources/extensions/gsd/doctor-engine-checks.ts +3 -3
  167. package/src/resources/extensions/gsd/doctor-git-checks.ts +2 -19
  168. package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
  169. package/src/resources/extensions/gsd/gsd-db.ts +4 -3
  170. package/src/resources/extensions/gsd/guided-flow.ts +21 -26
  171. package/src/resources/extensions/gsd/milestone-closeout.ts +97 -2
  172. package/src/resources/extensions/gsd/milestone-planning-persistence.ts +2 -2
  173. package/src/resources/extensions/gsd/projection-flush.ts +20 -0
  174. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  177. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  178. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  180. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  181. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  182. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  183. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  184. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  185. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
  186. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  187. package/src/resources/extensions/gsd/prompts/validate-milestone.md +1 -1
  188. package/src/resources/extensions/gsd/roadmap-slices.ts +28 -3
  189. package/src/resources/extensions/gsd/session-lock.ts +1 -1
  190. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +69 -0
  191. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +97 -0
  192. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +65 -3
  193. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
  194. package/src/resources/extensions/gsd/tests/consent-question.test.ts +15 -0
  195. package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
  196. package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
  197. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +2 -6
  198. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
  199. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  200. package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +242 -0
  201. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +63 -2
  202. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +68 -0
  203. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +19 -1
  204. package/src/resources/extensions/gsd/tests/tool-unavailable-retry.test.ts +33 -0
  205. package/src/resources/extensions/gsd/tests/transport-gate-double-complete.test.ts +139 -0
  206. package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +2 -0
  207. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +273 -38
  208. package/src/resources/extensions/gsd/tool-contract.ts +38 -3
  209. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  210. package/src/resources/extensions/gsd/tools/complete-slice.ts +2 -2
  211. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -2
  212. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -2
  213. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -2
  214. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +2 -2
  215. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +2 -2
  216. package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -2
  217. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -2
  218. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -2
  219. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
  220. package/src/resources/extensions/gsd/verification-verdict.ts +4 -2
  221. package/src/resources/extensions/shared/gsd-browser-cli.ts +23 -2
  222. package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
  223. package/src/resources/shared/package-manager-detection.ts +1 -1
  224. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → McokybTayhff1xEVc-d3T}/_buildManifest.js +0 -0
  225. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → McokybTayhff1xEVc-d3T}/_ssgManifest.js +0 -0
@@ -0,0 +1,139 @@
1
+ // gsd-pi — Regression tests for the centralized transport gate and
2
+ // milestone double-complete guard (auto-dispatch.ts + phases.ts).
3
+
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+
10
+ import { resolveDispatch } from "../auto-dispatch.ts";
11
+ import { openDatabase, closeDatabase, insertMilestone, getMilestone } from "../gsd-db.ts";
12
+ import { isClosedStatus } from "../status-guards.ts";
13
+ import { getWorkflowTransportSupportError } from "../workflow-mcp.ts";
14
+
15
+ // ── transport gate: getWorkflowTransportSupportError blocks missing tools ────
16
+
17
+ test("getWorkflowTransportSupportError blocks when required non-surface tools are missing", () => {
18
+ const err = getWorkflowTransportSupportError(
19
+ "claude-code",
20
+ ["custom_non_surface_tool"],
21
+ {
22
+ projectRoot: process.cwd(),
23
+ surface: "auto-mode",
24
+ unitType: "execute-task",
25
+ authMode: "externalCli",
26
+ baseUrl: "local://claude-code",
27
+ activeTools: ["some_other_tool"],
28
+ },
29
+ );
30
+ assert.ok(err, "should return an error when required non-surface tools are missing from activeTools");
31
+ assert.match(err, /cannot run/i);
32
+ });
33
+
34
+ test("getWorkflowTransportSupportError passes for non-MCP transport", () => {
35
+ const err = getWorkflowTransportSupportError(
36
+ "claude-code",
37
+ ["custom_non_surface_tool"],
38
+ {
39
+ projectRoot: process.cwd(),
40
+ surface: "auto-mode",
41
+ unitType: "execute-task",
42
+ authMode: "apiKey",
43
+ baseUrl: "https://api.anthropic.com",
44
+ activeTools: [],
45
+ },
46
+ );
47
+ assert.equal(err, null, "non-MCP transport should not be blocked");
48
+ });
49
+
50
+ test("resolveDispatch allows dispatch when MCP tools are available", async (t) => {
51
+ const base = mkdtempSync(join(tmpdir(), "gsd-transport-pass-"));
52
+ mkdirSync(join(base, ".gsd"), { recursive: true });
53
+ openDatabase(join(base, ".gsd", "gsd.db"));
54
+ t.after(() => {
55
+ closeDatabase();
56
+ rmSync(base, { recursive: true, force: true });
57
+ });
58
+
59
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
60
+
61
+ const action = await resolveDispatch({
62
+ basePath: base,
63
+ mid: "M001",
64
+ midTitle: "Test",
65
+ state: {
66
+ phase: "executing",
67
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
68
+ activeSlice: { id: "S01", title: "Slice" },
69
+ activeTask: { id: "M001/S01/T01", title: "Task", status: "pending" },
70
+ registry: [],
71
+ blockers: [],
72
+ } as any,
73
+ prefs: undefined,
74
+ // No transport gate deps → no MCP check → dispatch should go through
75
+ });
76
+
77
+ assert.equal(action.action, "dispatch");
78
+ });
79
+
80
+ // ── double-complete guard: isClosedStatus prevents duplicate closeout ────────
81
+
82
+ test("isClosedStatus detects complete milestone status", () => {
83
+ assert.equal(isClosedStatus("complete"), true);
84
+ assert.equal(isClosedStatus("done"), true);
85
+ assert.equal(isClosedStatus("skipped"), true);
86
+ assert.equal(isClosedStatus("closed"), true);
87
+ assert.equal(isClosedStatus("active"), false);
88
+ assert.equal(isClosedStatus("pending"), false);
89
+ assert.equal(isClosedStatus(""), false);
90
+ });
91
+
92
+ test("getMilestone + isClosedStatus guards against double-complete dispatch", (t) => {
93
+ const base = mkdtempSync(join(tmpdir(), "gsd-double-complete-"));
94
+ mkdirSync(join(base, ".gsd"), { recursive: true });
95
+ openDatabase(join(base, ".gsd", "gsd.db"));
96
+ t.after(() => {
97
+ closeDatabase();
98
+ rmSync(base, { recursive: true, force: true });
99
+ });
100
+
101
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
102
+
103
+ // resolveDispatch itself checks for closed milestones at the top
104
+ // (before any dispatch rule fires), which is the same guard used
105
+ // by the phases.ts double-complete check.
106
+ const milestone = getMilestone("M001");
107
+ assert.ok(milestone, "milestone should exist in DB");
108
+ assert.equal(isClosedStatus(milestone.status), true, "closed milestone must be detected");
109
+ });
110
+
111
+ test("resolveDispatch stops on closed milestone before dispatching", async (t) => {
112
+ const base = mkdtempSync(join(tmpdir(), "gsd-closed-milestone-"));
113
+ mkdirSync(join(base, ".gsd"), { recursive: true });
114
+ openDatabase(join(base, ".gsd", "gsd.db"));
115
+ t.after(() => {
116
+ closeDatabase();
117
+ rmSync(base, { recursive: true, force: true });
118
+ });
119
+
120
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
121
+
122
+ const action = await resolveDispatch({
123
+ basePath: base,
124
+ mid: "M001",
125
+ midTitle: "Test",
126
+ state: {
127
+ phase: "complete",
128
+ activeMilestone: { id: "M001", title: "Test", status: "complete" },
129
+ activeSlice: null,
130
+ activeTask: null,
131
+ registry: [],
132
+ blockers: [],
133
+ } as any,
134
+ prefs: undefined,
135
+ });
136
+
137
+ assert.equal(action.action, "stop");
138
+ assert.match(action.reason ?? "", /closed/i);
139
+ });
@@ -24,6 +24,8 @@ test("execute-task fails closed when no host-owned checks are discovered", () =>
24
24
  assert.equal(verdict.reason, "no-host-checks");
25
25
  assert.equal(verdict.retryable, false);
26
26
  assert.match(verdict.failureContext, /No runnable host-owned verification command/);
27
+ assert.match(verdict.failureContext, /\.gsd\/PREFERENCES\.md/);
28
+ assert.match(verdict.failureContext, /\/gsd next/);
27
29
  });
28
30
 
29
31
  test("execute-task passes when non-runnable task-plan prose is the verification source", () => {
@@ -15,8 +15,11 @@ import {
15
15
  upsertRequirement,
16
16
  getAllMilestones,
17
17
  } from "../gsd-db.ts";
18
+ import { registerAutoWorker } from "../db/auto-workers.ts";
19
+ import { claimMilestoneLease, getMilestoneLease } from "../db/milestone-leases.ts";
18
20
  import { deriveState, invalidateStateCache } from "../state.ts";
19
21
  import { autoSession } from "../auto-runtime-state.ts";
22
+ import { normalizeRealPath } from "../paths.ts";
20
23
  import { markApprovalGateVerified, markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot, setPendingGate } from "../bootstrap/write-gate.ts";
21
24
  import {
22
25
  executeCompleteMilestone,
@@ -94,6 +97,28 @@ function seedMilestone(milestoneId: string, title: string, status = "active"): v
94
97
  ).run(milestoneId, title, status, new Date().toISOString());
95
98
  }
96
99
 
100
+ function validMilestonePlan(milestoneId = "M001"): Parameters<typeof executePlanMilestone>[0] {
101
+ return {
102
+ milestoneId,
103
+ title: "Workflow MCP planning",
104
+ vision: "Plan milestone over shared executors.",
105
+ slices: [
106
+ {
107
+ sliceId: "S01",
108
+ title: "Bridge planning",
109
+ risk: "medium",
110
+ depends: [],
111
+ demo: "Milestone plan persists through MCP.",
112
+ goal: "Persist roadmap state.",
113
+ successCriteria: "ROADMAP.md renders from DB.",
114
+ proofLevel: "integration",
115
+ integrationClosure: "Prompts and MCP call the same handler.",
116
+ observabilityImpact: "Executor tests cover output paths.",
117
+ },
118
+ ],
119
+ };
120
+ }
121
+
97
122
  function seedSlice(milestoneId: string, sliceId: string, status: string): void {
98
123
  const db = _getAdapter();
99
124
  if (!db) throw new Error("DB not open");
@@ -461,25 +486,7 @@ test("executePlanMilestone writes roadmap state and rendered roadmap path", asyn
461
486
  try {
462
487
  openTestDb(base);
463
488
 
464
- const result = await inProjectDir(base, () => executePlanMilestone({
465
- milestoneId: "M001",
466
- title: "Workflow MCP planning",
467
- vision: "Plan milestone over shared executors.",
468
- slices: [
469
- {
470
- sliceId: "S01",
471
- title: "Bridge planning",
472
- risk: "medium",
473
- depends: [],
474
- demo: "Milestone plan persists through MCP.",
475
- goal: "Persist roadmap state.",
476
- successCriteria: "ROADMAP.md renders from DB.",
477
- proofLevel: "integration",
478
- integrationClosure: "Prompts and MCP call the same handler.",
479
- observabilityImpact: "Executor tests cover output paths.",
480
- },
481
- ],
482
- }, base));
489
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan(), base));
483
490
 
484
491
  assert.equal(result.details.operation, "plan_milestone");
485
492
  assert.equal(result.details.milestoneId, "M001");
@@ -492,29 +499,257 @@ test("executePlanMilestone writes roadmap state and rendered roadmap path", asyn
492
499
  }
493
500
  });
494
501
 
502
+ test("executePlanMilestone refuses a same-milestone lease conflict", async () => {
503
+ const base = makeTmpBase();
504
+ try {
505
+ openTestDb(base);
506
+ seedMilestone("M001", "Existing holder");
507
+ const holder = registerAutoWorker({ projectRootRealpath: join(base, "other-project") });
508
+ const lease = claimMilestoneLease(holder, "M001");
509
+ assert.equal(lease.ok, true);
510
+
511
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M001"), base));
512
+
513
+ assert.equal(result.isError, true);
514
+ assert.equal(result.details.operation, "plan_milestone");
515
+ assert.equal(result.details.error, "milestone_lease_conflict");
516
+ assert.match(result.content[0].text, /Milestone M001 is currently leased/);
517
+ } finally {
518
+ closeDatabase();
519
+ cleanup(base);
520
+ }
521
+ });
522
+
523
+ test("executePlanMilestone releases its one-shot milestone lease after planning", async () => {
524
+ const base = makeTmpBase();
525
+ try {
526
+ openTestDb(base);
527
+ seedMilestone("M001", "Existing milestone");
528
+
529
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M001"), base));
530
+
531
+ assert.equal(result.isError, undefined);
532
+ const lease = getMilestoneLease("M001");
533
+ assert.ok(lease, "planning should participate in milestone lease coordination");
534
+ assert.equal(lease.status, "released");
535
+ } finally {
536
+ closeDatabase();
537
+ cleanup(base);
538
+ }
539
+ });
540
+
541
+ test("executePlanMilestone creates a fresh milestone without a one-shot lease", async () => {
542
+ const base = makeTmpBase();
543
+ try {
544
+ openTestDb(base);
545
+
546
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M042"), base));
547
+
548
+ assert.equal(result.isError, undefined);
549
+ assert.equal(result.details.operation, "plan_milestone");
550
+ assert.equal(result.details.milestoneId, "M042");
551
+ assert.equal(getMilestoneLease("M042"), null,
552
+ "fresh milestone creation must pass through without creating a lease row");
553
+ } finally {
554
+ closeDatabase();
555
+ cleanup(base);
556
+ }
557
+ });
558
+
559
+ test("executePlanMilestone does not create a placeholder when fresh planning validation fails", async () => {
560
+ const base = makeTmpBase();
561
+ try {
562
+ openTestDb(base);
563
+
564
+ // handlePlanMilestone rejects empty slice arrays during validation. Fresh
565
+ // milestone creation must pass through without pre-inserting a leaseable
566
+ // placeholder row.
567
+ const invalid = { ...validMilestonePlan("M042"), slices: [] };
568
+ const result = await inProjectDir(base, () => executePlanMilestone(invalid, base));
569
+
570
+ assert.equal(result.isError, true);
571
+ assert.equal(result.details.operation, "plan_milestone");
572
+ const milestones = getAllMilestones();
573
+ assert.equal(milestones.find(m => m.id === "M042"), undefined,
574
+ "failed fresh planning must not leave a milestone row behind");
575
+ assert.equal(getMilestoneLease("M042"), null,
576
+ "failed fresh planning must not create a lease row");
577
+ } finally {
578
+ closeDatabase();
579
+ cleanup(base);
580
+ }
581
+ });
582
+
583
+ test("executePlanMilestone does not delete a peer worker's milestone row on lease conflict", async () => {
584
+ const base = makeTmpBase();
585
+ try {
586
+ openTestDb(base);
587
+
588
+ // Simulate a peer worker (different project_root) that has already
589
+ // created the milestone row and taken its lease. The one-shot executor
590
+ // must observe the active lease and refuse — without removing the peer's
591
+ // milestone row or lease. The pre-rollback bug was that
592
+ // INSERT OR IGNORE's silent no-op was treated as proof of authorship and
593
+ // the cleanup path then deleted the peer's row.
594
+ const peer = registerAutoWorker({ projectRootRealpath: join(base, "peer-project") });
595
+ seedMilestone("M050", "Peer-owned milestone");
596
+ const peerLease = claimMilestoneLease(peer, "M050");
597
+ assert.equal(peerLease.ok, true);
598
+ const peerLeaseToken = peerLease.ok ? peerLease.token : -1;
599
+
600
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M050"), base));
601
+
602
+ assert.equal(result.isError, true);
603
+ assert.equal(result.details.error, "milestone_lease_conflict");
604
+ const peerMilestone = getAllMilestones().find(m => m.id === "M050");
605
+ assert.ok(peerMilestone, "peer milestone row must survive the one-shot conflict path");
606
+ assert.equal(peerMilestone?.title, "Peer-owned milestone",
607
+ "peer milestone row must not be clobbered or recreated");
608
+ const surviving = getMilestoneLease("M050");
609
+ assert.ok(surviving, "peer lease row must survive");
610
+ assert.equal(surviving.status, "held", "peer must still hold the lease");
611
+ assert.equal(surviving.worker_id, peer);
612
+ assert.equal(surviving.fencing_token, peerLeaseToken,
613
+ "peer's fencing token must not be incremented by the rejected one-shot");
614
+ } finally {
615
+ closeDatabase();
616
+ cleanup(base);
617
+ }
618
+ });
619
+
620
+ test("executePlanMilestone refuses when a foreign active worker holds the lease even while in-process auto is active", async () => {
621
+ const base = makeTmpBase();
622
+ autoSession.reset();
623
+ try {
624
+ openTestDb(base);
625
+ seedMilestone("M060", "Foreign-held milestone");
626
+
627
+ // In-process auto must still reject a lease held by another worker.
628
+ const foreign = registerAutoWorker({ projectRootRealpath: join(base, "foreign-project") });
629
+ const foreignLease = claimMilestoneLease(foreign, "M060");
630
+ assert.equal(foreignLease.ok, true);
631
+ const foreignToken = foreignLease.ok ? foreignLease.token : -1;
632
+
633
+ const ownAutoWorker = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
634
+ autoSession.active = true;
635
+ autoSession.workerId = ownAutoWorker;
636
+
637
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M060"), base));
638
+
639
+ assert.equal(result.isError, true);
640
+ assert.equal(result.details.error, "milestone_lease_conflict",
641
+ "in-process auto must NOT skip lease checks when the lease is held by a different active worker");
642
+ const surviving = getMilestoneLease("M060");
643
+ assert.ok(surviving);
644
+ assert.equal(surviving.status, "held");
645
+ assert.equal(surviving.worker_id, foreign);
646
+ assert.equal(surviving.fencing_token, foreignToken,
647
+ "foreign worker's fencing token must not be incremented by the rejected call");
648
+ } finally {
649
+ autoSession.reset();
650
+ closeDatabase();
651
+ cleanup(base);
652
+ }
653
+ });
654
+
655
+ test("executePlanMilestone proceeds without re-claiming when in-process auto holds the lease itself", async () => {
656
+ const base = makeTmpBase();
657
+ autoSession.reset();
658
+ try {
659
+ openTestDb(base);
660
+ seedMilestone("M070", "Auto-held milestone");
661
+
662
+ // In-process auto should not re-claim its own lease and bump its token.
663
+ const ownAutoWorker = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
664
+ const heldLease = claimMilestoneLease(ownAutoWorker, "M070");
665
+ assert.equal(heldLease.ok, true);
666
+ const heldToken = heldLease.ok ? heldLease.token : -1;
667
+ autoSession.active = true;
668
+ autoSession.workerId = ownAutoWorker;
669
+
670
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M070"), base));
671
+
672
+ assert.equal(result.isError, undefined, `auto's own plan-milestone call should succeed: ${result.content?.[0]?.text}`);
673
+ const surviving = getMilestoneLease("M070");
674
+ assert.ok(surviving);
675
+ assert.equal(surviving.status, "held", "auto's lease must still be held after the planning call");
676
+ assert.equal(surviving.worker_id, ownAutoWorker);
677
+ assert.equal(surviving.fencing_token, heldToken,
678
+ "auto's fencing token must not be incremented by an in-process plan call");
679
+ } finally {
680
+ autoSession.reset();
681
+ closeDatabase();
682
+ cleanup(base);
683
+ }
684
+ });
685
+
686
+ test("executePlanMilestone refuses a same-process holder when active auto does not own it", async () => {
687
+ const base = makeTmpBase();
688
+ autoSession.reset();
689
+ try {
690
+ openTestDb(base);
691
+ seedMilestone("M080", "Same-process holder");
692
+ const staleWorker = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
693
+ const heldLease = claimMilestoneLease(staleWorker, "M080");
694
+ assert.equal(heldLease.ok, true);
695
+ const heldToken = heldLease.ok ? heldLease.token : -1;
696
+
697
+ autoSession.active = true;
698
+ autoSession.workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
699
+
700
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M080"), base));
701
+
702
+ assert.equal(result.isError, true);
703
+ assert.equal(result.details?.error, "milestone_lease_conflict");
704
+ const surviving = getMilestoneLease("M080");
705
+ assert.ok(surviving, "same-process holder lease must remain after conflict");
706
+ assert.equal(surviving.worker_id, staleWorker);
707
+ assert.equal(surviving.fencing_token, heldToken);
708
+ assert.equal(surviving.status, "held");
709
+ } finally {
710
+ autoSession.reset();
711
+ closeDatabase();
712
+ cleanup(base);
713
+ }
714
+ });
715
+
716
+ test("executePlanMilestone takes over a stale same-process lease via reentry instead of waiting for TTL", async () => {
717
+ const base = makeTmpBase();
718
+ autoSession.reset();
719
+ try {
720
+ openTestDb(base);
721
+ seedMilestone("M080", "Stale-locked milestone");
722
+
723
+ // One-shot planning should reach claimMilestoneLease so its same-process
724
+ // reentry clause can recover stale worker rows.
725
+ const staleWorker = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
726
+ const staleLease = claimMilestoneLease(staleWorker, "M080");
727
+ assert.equal(staleLease.ok, true);
728
+ const staleToken = staleLease.ok ? staleLease.token : -1;
729
+
730
+ const result = await inProjectDir(base, () => executePlanMilestone(validMilestonePlan("M080"), base));
731
+
732
+ assert.equal(result.isError, undefined,
733
+ `same-process reentry takeover must succeed, not return milestone_lease_conflict: ${result.content?.[0]?.text}`);
734
+ const after = getMilestoneLease("M080");
735
+ assert.ok(after, "lease row must remain after takeover + release");
736
+ assert.equal(after.status, "released", "the executor releases its own claim before returning");
737
+ assert.notEqual(after.worker_id, staleWorker,
738
+ "lease must be owned by the new worker after takeover");
739
+ assert.ok(after.fencing_token > staleToken,
740
+ `fencing token must monotonically advance on takeover (was ${staleToken}, now ${after.fencing_token})`);
741
+ } finally {
742
+ autoSession.reset();
743
+ closeDatabase();
744
+ cleanup(base);
745
+ }
746
+ });
747
+
495
748
  test("executePlanSlice writes task planning state and rendered plan artifacts", async () => {
496
749
  const base = makeTmpBase();
497
750
  try {
498
751
  openTestDb(base);
499
- await inProjectDir(base, () => executePlanMilestone({
500
- milestoneId: "M001",
501
- title: "Workflow MCP planning",
502
- vision: "Plan milestone over shared executors.",
503
- slices: [
504
- {
505
- sliceId: "S01",
506
- title: "Bridge planning",
507
- risk: "medium",
508
- depends: [],
509
- demo: "Milestone plan persists through MCP.",
510
- goal: "Persist roadmap state.",
511
- successCriteria: "ROADMAP.md renders from DB.",
512
- proofLevel: "integration",
513
- integrationClosure: "Prompts and MCP call the same handler.",
514
- observabilityImpact: "Executor tests cover output paths.",
515
- },
516
- ],
517
- }, base));
752
+ await inProjectDir(base, () => executePlanMilestone(validMilestonePlan(), base));
518
753
 
519
754
  const result = await inProjectDir(base, () => executePlanSlice({
520
755
  milestoneId: "M001",
@@ -9,8 +9,14 @@ import {
9
9
  type ToolsPolicy,
10
10
  type UnitContextManifest,
11
11
  } from "./unit-context-manifest.js";
12
- import { getRequiredWorkflowToolsForAutoUnit } from "./workflow-mcp.js";
13
- import { getUnitToolSurfaceContract } from "./unit-tool-contracts.js";
12
+ import {
13
+ getWorkflowTransportSupportError,
14
+ type WorkflowCapabilityOptions,
15
+ } from "./workflow-mcp.js";
16
+ import {
17
+ getRequiredWorkflowToolsForUnit,
18
+ getUnitToolSurfaceContract,
19
+ } from "./unit-tool-contracts.js";
14
20
  import {
15
21
  WHOLE_FILE_OBSERVATION_MAX_BYTES,
16
22
  WHOLE_FILE_OBSERVATION_MAX_LINES,
@@ -68,6 +74,17 @@ export type UnitContextContractResult =
68
74
  | { ok: true; contract: UnitPromptContextContract }
69
75
  | { ok: false; reason: "unknown-unit-type"; detail: string };
70
76
 
77
+ export interface UnitWorkflowDispatchReadinessInput {
78
+ provider?: string;
79
+ unitType: string;
80
+ projectRoot?: string;
81
+ env?: NodeJS.ProcessEnv;
82
+ surface?: string;
83
+ authMode?: WorkflowCapabilityOptions["authMode"];
84
+ baseUrl?: string;
85
+ activeTools?: string[];
86
+ }
87
+
71
88
  export function compileUnitContextContract(unitType: string): UnitContextContractResult {
72
89
  const manifest = resolveManifest(unitType);
73
90
  if (!manifest) {
@@ -80,6 +97,24 @@ export function compileUnitContextContract(unitType: string): UnitContextContrac
80
97
  return { ok: true, contract: buildPromptContextContract(unitType, manifest) };
81
98
  }
82
99
 
100
+ export function getUnitWorkflowDispatchReadinessError(
101
+ input: UnitWorkflowDispatchReadinessInput,
102
+ ): string | null {
103
+ return getWorkflowTransportSupportError(
104
+ input.provider,
105
+ getRequiredWorkflowToolsForUnit(input.unitType),
106
+ {
107
+ projectRoot: input.projectRoot,
108
+ env: input.env,
109
+ surface: input.surface,
110
+ unitType: input.unitType,
111
+ authMode: input.authMode,
112
+ baseUrl: input.baseUrl,
113
+ activeTools: input.activeTools,
114
+ },
115
+ );
116
+ }
117
+
83
118
  export function compileUnitToolContract(unitType: string): ToolContractResult {
84
119
  const manifest = resolveManifest(unitType);
85
120
  const surfaceContract = getUnitToolSurfaceContract(unitType);
@@ -91,7 +126,7 @@ export function compileUnitToolContract(unitType: string): ToolContractResult {
91
126
  };
92
127
  }
93
128
 
94
- const requiredWorkflowTools = getRequiredWorkflowToolsForAutoUnit(unitType);
129
+ const requiredWorkflowTools = getRequiredWorkflowToolsForUnit(unitType);
95
130
  const forbiddenWorkflowTools = Object.entries(surfaceContract?.forbiddenGsdTools ?? {})
96
131
  .map(([name, reason]) => ({ name, reason }));
97
132
  const closeoutTools = requiredWorkflowTools.filter((tool) =>
@@ -26,7 +26,8 @@ import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
26
26
  import { isClosedStatus } from "../status-guards.js";
27
27
  import { saveFile, clearParseCache } from "../files.js";
28
28
  import { invalidateStateCache } from "../state.js";
29
- import { renderAllProjections, stripIdPrefix } from "../workflow-projections.js";
29
+ import { stripIdPrefix } from "../workflow-projections.js";
30
+ import { flushWorkflowProjections } from "../projection-flush.js";
30
31
  import { writeManifest } from "../workflow-manifest.js";
31
32
  import { appendEvent } from "../workflow-events.js";
32
33
  import { logWarning, logError } from "../workflow-logger.js";
@@ -240,7 +241,7 @@ export async function handleCompleteMilestone(
240
241
  // Separate try/catch per step so a projection failure doesn't prevent
241
242
  // the event log entry (critical for worktree reconciliation).
242
243
  try {
243
- await renderAllProjections(artifactBasePath, params.milestoneId);
244
+ await flushWorkflowProjections(artifactBasePath, { milestoneId: params.milestoneId });
244
245
  } catch (projErr) {
245
246
  logWarning("tool", `complete-milestone projection warning: ${(projErr as Error).message}`);
246
247
  }
@@ -30,7 +30,7 @@ import { classifyUatContent, escalatesArtifactUatToBrowser } from "../uat-policy
30
30
  import { invalidateStateCache } from "../state.js";
31
31
  import { renderRoadmapFromDb, roadmapRenderMarksSliceDone } from "../markdown-renderer.js";
32
32
  import { isStaleWrite } from "../auto/turn-epoch.js";
33
- import { renderAllProjections } from "../workflow-projections.js";
33
+ import { flushWorkflowProjections } from "../projection-flush.js";
34
34
  import { writeManifest } from "../workflow-manifest.js";
35
35
  import { appendEvent } from "../workflow-events.js";
36
36
  import { logWarning, logError } from "../workflow-logger.js";
@@ -537,7 +537,7 @@ export async function handleCompleteSlice(
537
537
  // Separate try/catch per step so a projection failure doesn't prevent
538
538
  // the event log entry (critical for worktree reconciliation).
539
539
  try {
540
- await renderAllProjections(artifactBasePath, params.milestoneId);
540
+ await flushWorkflowProjections(artifactBasePath, { milestoneId: params.milestoneId });
541
541
  } catch (projErr) {
542
542
  logWarning("tool", `complete-slice projection warning for ${params.milestoneId}/${params.sliceId}: ${(projErr as Error).message}`);
543
543
  }
@@ -35,7 +35,8 @@ import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
35
35
  import { saveFile, clearParseCache } from "../files.js";
36
36
  import { invalidateStateCache } from "../state.js";
37
37
  import { renderPlanCheckboxes } from "../markdown-renderer.js";
38
- import { renderAllProjections, renderSummaryContent } from "../workflow-projections.js";
38
+ import { renderSummaryContent } from "../workflow-projections.js";
39
+ import { flushWorkflowProjections } from "../projection-flush.js";
39
40
  import { writeManifest } from "../workflow-manifest.js";
40
41
  import { appendEvent } from "../workflow-events.js";
41
42
  import { logWarning, logError } from "../workflow-logger.js";
@@ -467,7 +468,7 @@ export async function handleCompleteTask(
467
468
  // Separate try/catch per step so a projection failure doesn't prevent
468
469
  // the event log entry (critical for worktree reconciliation).
469
470
  try {
470
- await renderAllProjections(artifactBasePath, params.milestoneId);
471
+ await flushWorkflowProjections(artifactBasePath, { milestoneId: params.milestoneId });
471
472
  } catch (projErr) {
472
473
  logWarning("tool", `complete-task projection warning: ${(projErr as Error).message}`);
473
474
  }
@@ -21,7 +21,7 @@ import {
21
21
  import type { GateEvaluationConfig, GateId } from "../types.js";
22
22
  import { invalidateStateCache } from "../state.js";
23
23
  import { renderPlanFromDb } from "../markdown-renderer.js";
24
- import { renderAllProjections } from "../workflow-projections.js";
24
+ import { flushWorkflowProjections } from "../projection-flush.js";
25
25
  import { writeManifest } from "../workflow-manifest.js";
26
26
  import { appendEvent } from "../workflow-events.js";
27
27
  import { logWarning } from "../workflow-logger.js";
@@ -468,7 +468,7 @@ export async function handlePlanSlice(
468
468
 
469
469
  // ── Post-mutation hook: projections, manifest, event log ─────────────
470
470
  try {
471
- await renderAllProjections(basePath, params.milestoneId);
471
+ await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
472
472
  writeManifest(basePath);
473
473
  appendEvent(basePath, {
474
474
  cmd: "plan-slice",
@@ -4,7 +4,7 @@ import { isNonEmptyString, validateStringArray } from "../validation.js";
4
4
  import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
5
5
  import { invalidateStateCache } from "../state.js";
6
6
  import { renderTaskPlanFromDb } from "../markdown-renderer.js";
7
- import { renderAllProjections } from "../workflow-projections.js";
7
+ import { flushWorkflowProjections } from "../projection-flush.js";
8
8
  import { writeManifest } from "../workflow-manifest.js";
9
9
  import { appendEvent } from "../workflow-events.js";
10
10
  import { logWarning } from "../workflow-logger.js";
@@ -142,7 +142,7 @@ export async function handlePlanTask(
142
142
 
143
143
  // ── Post-mutation hook: projections, manifest, event log ─────────────
144
144
  try {
145
- await renderAllProjections(basePath, params.milestoneId);
145
+ await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
146
146
  writeManifest(basePath);
147
147
  appendEvent(basePath, {
148
148
  cmd: "plan-task",
@@ -16,7 +16,7 @@ import {
16
16
  } from "../gsd-db.js";
17
17
  import { invalidateStateCache } from "../state.js";
18
18
  import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.js";
19
- import { renderAllProjections } from "../workflow-projections.js";
19
+ import { flushWorkflowProjections } from "../projection-flush.js";
20
20
  import { writeManifest } from "../workflow-manifest.js";
21
21
  import { appendEvent } from "../workflow-events.js";
22
22
  import { logWarning } from "../workflow-logger.js";
@@ -306,7 +306,7 @@ export async function handleReassessRoadmap(
306
306
 
307
307
  // ── Post-mutation hook: projections, manifest, event log ─────
308
308
  try {
309
- await renderAllProjections(basePath, params.milestoneId);
309
+ await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
310
310
  writeManifest(basePath);
311
311
  appendEvent(basePath, {
312
312
  cmd: "reassess-roadmap",