@opengsd/gsd-pi 1.1.1-dev.74e8dd1 → 1.1.1-dev.9bb7453

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 (259) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/help-text.js +10 -6
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +495 -0
  5. package/dist/resources/extensions/browser-tools/engine/selection.js +16 -0
  6. package/dist/resources/extensions/browser-tools/extension-manifest.json +2 -2
  7. package/dist/resources/extensions/browser-tools/index.js +57 -9
  8. package/dist/resources/extensions/browser-tools/package.json +5 -1
  9. package/dist/resources/extensions/gsd/auto/orchestrator.js +0 -1
  10. package/dist/resources/extensions/gsd/auto-dashboard.js +77 -13
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +5 -0
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +21 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.js +59 -22
  14. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  15. package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
  16. package/dist/resources/extensions/gsd/auto.js +9 -2
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -4
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -5
  19. package/dist/resources/extensions/gsd/browser-evidence.js +29 -2
  20. package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -2
  21. package/dist/resources/extensions/gsd/commands-handlers.js +76 -11
  22. package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -1
  23. package/dist/resources/extensions/gsd/dashboard-overlay.js +21 -7
  24. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -0
  25. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +2 -2
  26. package/dist/resources/extensions/gsd/escalation.js +4 -4
  27. package/dist/resources/extensions/gsd/forensics.js +74 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +5 -2
  29. package/dist/resources/extensions/gsd/guided-flow.js +29 -68
  30. package/dist/resources/extensions/gsd/mcp-project-config.js +9 -76
  31. package/dist/resources/extensions/gsd/memory-store.js +4 -1
  32. package/dist/resources/extensions/gsd/post-unit-hooks.js +9 -0
  33. package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
  34. package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
  35. package/dist/resources/extensions/gsd/prompts/forensics.md +61 -1
  36. package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +3 -1
  37. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +3 -1
  38. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +3 -1
  40. package/dist/resources/extensions/gsd/prompts/run-uat.md +40 -22
  41. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
  42. package/dist/resources/extensions/gsd/rule-registry.js +428 -52
  43. package/dist/resources/extensions/gsd/state.js +2 -2
  44. package/dist/resources/extensions/gsd/templates/plan.md +3 -1
  45. package/dist/resources/extensions/gsd/tools/complete-slice.js +15 -1
  46. package/dist/resources/extensions/gsd/tools/complete-task.js +11 -1
  47. package/dist/resources/extensions/gsd/tools/validate-milestone.js +46 -16
  48. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +51 -14
  49. package/dist/resources/extensions/gsd/verdict-parser.js +59 -15
  50. package/dist/resources/extensions/gsd/verification-gate.js +72 -1
  51. package/dist/resources/extensions/shared/gsd-browser-cli.js +145 -0
  52. package/dist/rtk.d.ts +7 -1
  53. package/dist/rtk.js +27 -11
  54. package/dist/update-check.d.ts +15 -1
  55. package/dist/update-check.js +87 -12
  56. package/dist/update-cmd.d.ts +1 -0
  57. package/dist/update-cmd.js +53 -2
  58. package/dist/web/standalone/.next/BUILD_ID +1 -1
  59. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  60. package/dist/web/standalone/.next/build-manifest.json +2 -2
  61. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  62. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/index.html +1 -1
  80. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  87. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  88. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  90. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  91. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  92. package/package.json +4 -2
  93. package/packages/cloud-mcp-gateway/package.json +2 -2
  94. package/packages/contracts/package.json +1 -1
  95. package/packages/daemon/package.json +4 -4
  96. package/packages/gsd-agent-core/dist/agent-session.d.ts +9 -0
  97. package/packages/gsd-agent-core/dist/agent-session.d.ts.map +1 -1
  98. package/packages/gsd-agent-core/dist/agent-session.js +32 -0
  99. package/packages/gsd-agent-core/dist/agent-session.js.map +1 -1
  100. package/packages/gsd-agent-core/dist/index.d.ts +1 -0
  101. package/packages/gsd-agent-core/dist/index.d.ts.map +1 -1
  102. package/packages/gsd-agent-core/dist/index.js +1 -0
  103. package/packages/gsd-agent-core/dist/index.js.map +1 -1
  104. package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts +2 -0
  105. package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts.map +1 -1
  106. package/packages/gsd-agent-core/dist/session/agent-session-compaction.js +8 -2
  107. package/packages/gsd-agent-core/dist/session/agent-session-compaction.js.map +1 -1
  108. package/packages/gsd-agent-core/dist/session/agent-session-host.d.ts +7 -0
  109. package/packages/gsd-agent-core/dist/session/agent-session-host.d.ts.map +1 -1
  110. package/packages/gsd-agent-core/dist/session/agent-session-host.js.map +1 -1
  111. package/packages/gsd-agent-core/dist/session/agent-session-prompt.d.ts.map +1 -1
  112. package/packages/gsd-agent-core/dist/session/agent-session-prompt.js +69 -1
  113. package/packages/gsd-agent-core/dist/session/agent-session-prompt.js.map +1 -1
  114. package/packages/gsd-agent-core/dist/turn-latency.d.ts +47 -0
  115. package/packages/gsd-agent-core/dist/turn-latency.d.ts.map +1 -0
  116. package/packages/gsd-agent-core/dist/turn-latency.js +123 -0
  117. package/packages/gsd-agent-core/dist/turn-latency.js.map +1 -0
  118. package/packages/gsd-agent-core/package.json +6 -6
  119. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts +21 -0
  120. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts.map +1 -0
  121. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js +213 -0
  122. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js.map +1 -0
  123. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  124. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +20 -0
  125. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  126. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  127. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js +7 -1
  128. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  129. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-command-handlers.d.ts.map +1 -1
  130. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-command-handlers.js +6 -0
  131. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-command-handlers.js.map +1 -1
  132. package/packages/gsd-agent-modes/package.json +7 -7
  133. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  134. package/packages/mcp-server/dist/remote-questions.js +23 -9
  135. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  136. package/packages/mcp-server/dist/workflow-tools.js +2 -2
  137. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  138. package/packages/mcp-server/package.json +3 -3
  139. package/packages/native/package.json +1 -1
  140. package/packages/pi-agent-core/dist/agent-loop.js +38 -0
  141. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  142. package/packages/pi-agent-core/dist/agent.d.ts +5 -1
  143. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  144. package/packages/pi-agent-core/dist/agent.js +2 -0
  145. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  146. package/packages/pi-agent-core/dist/types.d.ts +3 -0
  147. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  148. package/packages/pi-agent-core/dist/types.js.map +1 -1
  149. package/packages/pi-agent-core/package.json +1 -1
  150. package/packages/pi-ai/dist/api-registry.d.ts +2 -0
  151. package/packages/pi-ai/dist/api-registry.d.ts.map +1 -1
  152. package/packages/pi-ai/dist/api-registry.js +23 -0
  153. package/packages/pi-ai/dist/api-registry.js.map +1 -1
  154. package/packages/pi-ai/dist/models.generated.d.ts +68 -0
  155. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  156. package/packages/pi-ai/dist/models.generated.js +72 -4
  157. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  158. package/packages/pi-ai/dist/stream.js +6 -6
  159. package/packages/pi-ai/dist/stream.js.map +1 -1
  160. package/packages/pi-ai/package.json +1 -1
  161. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -2
  163. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  164. package/packages/pi-coding-agent/package.json +7 -7
  165. package/packages/pi-tui/package.json +1 -1
  166. package/packages/rpc-client/package.json +2 -2
  167. package/pkg/package.json +1 -1
  168. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +579 -0
  169. package/src/resources/extensions/browser-tools/engine/selection.ts +19 -0
  170. package/src/resources/extensions/browser-tools/extension-manifest.json +2 -2
  171. package/src/resources/extensions/browser-tools/index.ts +60 -9
  172. package/src/resources/extensions/browser-tools/package.json +5 -1
  173. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +35 -0
  174. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +33 -0
  175. package/src/resources/extensions/gsd/auto/orchestrator.ts +0 -1
  176. package/src/resources/extensions/gsd/auto-dashboard.ts +82 -14
  177. package/src/resources/extensions/gsd/auto-dispatch.ts +5 -0
  178. package/src/resources/extensions/gsd/auto-post-unit.ts +28 -2
  179. package/src/resources/extensions/gsd/auto-prompts.ts +93 -15
  180. package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -0
  181. package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
  182. package/src/resources/extensions/gsd/auto.ts +12 -2
  183. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -4
  184. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -5
  185. package/src/resources/extensions/gsd/browser-evidence.ts +26 -2
  186. package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -2
  187. package/src/resources/extensions/gsd/commands-handlers.ts +76 -11
  188. package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -1
  189. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -7
  190. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -0
  191. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +2 -2
  192. package/src/resources/extensions/gsd/escalation.ts +4 -4
  193. package/src/resources/extensions/gsd/forensics.ts +99 -5
  194. package/src/resources/extensions/gsd/gsd-db.ts +5 -2
  195. package/src/resources/extensions/gsd/guided-flow.ts +90 -82
  196. package/src/resources/extensions/gsd/mcp-project-config.ts +13 -78
  197. package/src/resources/extensions/gsd/memory-store.ts +4 -1
  198. package/src/resources/extensions/gsd/post-unit-hooks.ts +14 -1
  199. package/src/resources/extensions/gsd/preferences-validation.ts +36 -0
  200. package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
  201. package/src/resources/extensions/gsd/prompts/forensics.md +61 -1
  202. package/src/resources/extensions/gsd/prompts/gate-evaluate.md +3 -1
  203. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +3 -1
  204. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  205. package/src/resources/extensions/gsd/prompts/reactive-execute.md +3 -1
  206. package/src/resources/extensions/gsd/prompts/run-uat.md +40 -22
  207. package/src/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
  208. package/src/resources/extensions/gsd/rule-registry.ts +558 -58
  209. package/src/resources/extensions/gsd/rule-types.ts +2 -0
  210. package/src/resources/extensions/gsd/state.ts +2 -2
  211. package/src/resources/extensions/gsd/templates/plan.md +3 -1
  212. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +105 -4
  213. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +37 -0
  214. package/src/resources/extensions/gsd/tests/browser-evidence.test.ts +142 -0
  215. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +30 -0
  216. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +42 -0
  217. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +45 -0
  218. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +53 -0
  219. package/src/resources/extensions/gsd/tests/discuss-milestone-structured-questions.test.ts +31 -0
  220. package/src/resources/extensions/gsd/tests/doctor-runtime-checks.test.ts +27 -0
  221. package/src/resources/extensions/gsd/tests/escalation.test.ts +16 -27
  222. package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +20 -0
  223. package/src/resources/extensions/gsd/tests/forensics-prompt-rendering.test.ts +3 -0
  224. package/src/resources/extensions/gsd/tests/forensics-tool-scope.test.ts +69 -0
  225. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +40 -1
  226. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +86 -0
  227. package/src/resources/extensions/gsd/tests/guided-flow.test.ts +12 -9
  228. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +4 -4
  229. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +66 -10
  230. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +32 -0
  231. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +2 -0
  232. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +39 -8
  233. package/src/resources/extensions/gsd/tests/new-milestone-discuss-routing.test.ts +3 -3
  234. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +9 -0
  235. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +157 -0
  236. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +179 -0
  237. package/src/resources/extensions/gsd/tests/preferences.test.ts +29 -0
  238. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +43 -1
  239. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +14 -0
  240. package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +7 -8
  241. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +75 -0
  242. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +100 -0
  243. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +139 -0
  244. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  245. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +7 -1
  246. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +6 -3
  247. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +133 -0
  248. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +51 -0
  249. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +130 -0
  250. package/src/resources/extensions/gsd/tools/complete-slice.ts +14 -1
  251. package/src/resources/extensions/gsd/tools/complete-task.ts +20 -2
  252. package/src/resources/extensions/gsd/tools/validate-milestone.ts +46 -15
  253. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +63 -15
  254. package/src/resources/extensions/gsd/types.ts +69 -5
  255. package/src/resources/extensions/gsd/verdict-parser.ts +54 -13
  256. package/src/resources/extensions/gsd/verification-gate.ts +87 -1
  257. package/src/resources/extensions/shared/gsd-browser-cli.ts +172 -0
  258. /package/dist/web/standalone/.next/static/{eRWf-RI9bzbrwEurm_3uI → jBtwT9v1u2lUA3UEOy_ZH}/_buildManifest.js +0 -0
  259. /package/dist/web/standalone/.next/static/{eRWf-RI9bzbrwEurm_3uI → jBtwT9v1u2lUA3UEOy_ZH}/_ssgManifest.js +0 -0
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
7
  import {
8
+ buildProjectBrowserMcpServerConfig,
8
9
  ensureClaudeCodeMcpJsonServerEnabled,
9
10
  ensureProjectWorkflowMcpConfig,
10
11
  GSD_BROWSER_MCP_SERVER_NAME,
@@ -160,6 +161,37 @@ test("ensureProjectWorkflowMcpConfig can disable the default browser MCP server"
160
161
  }
161
162
  });
162
163
 
164
+ test("buildProjectBrowserMcpServerConfig prefers newer gsd-browser on PATH", () => {
165
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-browser-"));
166
+
167
+ try {
168
+ const config = buildProjectBrowserMcpServerConfig(projectRoot, {
169
+ GSD_BROWSER_PATH_VERSION: "99.0.0",
170
+ });
171
+
172
+ assert.equal(config?.command, "gsd-browser");
173
+ assert.equal(config?.args?.[0], "mcp");
174
+ } finally {
175
+ rmSync(projectRoot, { recursive: true, force: true });
176
+ }
177
+ });
178
+
179
+ test("buildProjectBrowserMcpServerConfig keeps bundled browser when PATH version is older", () => {
180
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-browser-"));
181
+
182
+ try {
183
+ const config = buildProjectBrowserMcpServerConfig(projectRoot, {
184
+ GSD_BROWSER_PATH_VERSION: "0.0.1",
185
+ });
186
+
187
+ assert.equal(config?.command, process.execPath);
188
+ assert.match(config?.args?.[0] ?? "", /@opengsd[\/\\]gsd-browser[\/\\]bin[\/\\]gsd-browser/);
189
+ assert.equal(config?.args?.[1], "mcp");
190
+ } finally {
191
+ rmSync(projectRoot, { recursive: true, force: true });
192
+ }
193
+ });
194
+
163
195
  test("ensureProjectWorkflowMcpConfig is idempotent when config is already current", () => {
164
196
  const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-init-"));
165
197
  mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
@@ -341,6 +341,8 @@ describe("formatMcpInitResult", () => {
341
341
  assert.match(result, /created project mcp config/i);
342
342
  assert.match(result, /\/tmp\/project\/\.mcp\.json/);
343
343
  assert.match(result, /mcp-capable clients/i);
344
+ assert.match(result, /workflow and gsd-browser MCP servers/i);
345
+ assert.match(result, /Pi Providers use the managed gsd-browser engine/i);
344
346
  assert.doesNotMatch(result, /claude code/i);
345
347
  });
346
348
 
@@ -11,6 +11,16 @@ import {
11
11
  import { createMemoryRelation, listRelationsFor } from '../memory-relations.ts';
12
12
  import { saveEmbedding, getEmbeddingForMemory } from '../memory-embeddings.ts';
13
13
 
14
+ function markProcessedUnits(count = 21): void {
15
+ const now = Date.now();
16
+ for (let i = 0; i < count; i++) {
17
+ markUnitProcessed(`unit/${i}`, `file-${i}`);
18
+ _getAdapter()!
19
+ .prepare('UPDATE memory_processed_units SET processed_at = :ts WHERE unit_key = :key')
20
+ .run({ ':ts': new Date(now + i * 1000).toISOString(), ':key': `unit/${i}` });
21
+ }
22
+ }
23
+
14
24
  // ═══════════════════════════════════════════════════════════════════════════
15
25
  // enforceMemoryCap — cascade cleanup of embeddings and relations
16
26
  // ═══════════════════════════════════════════════════════════════════════════
@@ -71,14 +81,7 @@ test('memory-decay: returns decayed memory IDs', () => {
71
81
  openDatabase(':memory:');
72
82
 
73
83
  // Insert processed units — decayStaleMemories needs at least N rows.
74
- const now = Date.now();
75
- for (let i = 0; i < 21; i++) {
76
- markUnitProcessed(`unit/${i}`, `file-${i}`);
77
- // small spacing to create deterministic ordering
78
- const row = _getAdapter()!
79
- .prepare('UPDATE memory_processed_units SET processed_at = :ts WHERE unit_key = :key');
80
- row.run({ ':ts': new Date(now + i * 1000).toISOString(), ':key': `unit/${i}` });
81
- }
84
+ markProcessedUnits();
82
85
 
83
86
  // Create memory with updated_at in the distant past
84
87
  createMemory({ category: 'pattern', content: 'stale entry', confidence: 0.9 });
@@ -98,6 +101,34 @@ test('memory-decay: returns decayed memory IDs', () => {
98
101
  closeDatabase();
99
102
  });
100
103
 
104
+ test('memory-decay: skips stale decision-sourced memories', () => {
105
+ openDatabase(':memory:');
106
+ markProcessedUnits();
107
+
108
+ createMemory({ category: 'pattern', content: 'stale working note', confidence: 0.9 });
109
+ createMemory({
110
+ category: 'architecture',
111
+ content: 'decision-backed architecture memory',
112
+ confidence: 0.85,
113
+ structuredFields: { sourceDecisionId: 'D001' },
114
+ });
115
+ _getAdapter()!
116
+ .prepare("UPDATE memories SET updated_at = '2000-01-01T00:00:00Z' WHERE id IN ('MEM001', 'MEM002')")
117
+ .run({});
118
+
119
+ const decayed = decayStaleMemories(20);
120
+ assert.ok(decayed.includes('MEM001'));
121
+ assert.ok(!decayed.includes('MEM002'));
122
+
123
+ const rows = _getAdapter()!
124
+ .prepare("SELECT id, confidence FROM memories WHERE id IN ('MEM001', 'MEM002') ORDER BY id")
125
+ .all() as Array<{ id: string; confidence: number }>;
126
+ assert.ok(rows.find((row) => row.id === 'MEM001')!.confidence < 0.9);
127
+ assert.equal(rows.find((row) => row.id === 'MEM002')!.confidence, 0.85);
128
+
129
+ closeDatabase();
130
+ });
131
+
101
132
  test('memory-decay: returns empty when there are fewer processed units than the threshold', () => {
102
133
  openDatabase(':memory:');
103
134
  createMemory({ category: 'pattern', content: 'fresh' });
@@ -13,10 +13,10 @@ test("dispatchNewMilestoneDiscuss uses discuss.md only on greenfield projects",
13
13
 
14
14
  assert.match(fnBody, /findMilestoneIds\(basePath\)\.length === 0/);
15
15
  assert.match(fnBody, /prepareAndBuildDiscussPrompt/);
16
- assert.match(fnBody, /loadPrompt\("guided-discuss-milestone"/);
16
+ assert.match(fnBody, /buildDiscussMilestonePrompt/);
17
17
  assert.match(
18
18
  fnBody,
19
- /if \(isGreenfield\)[\s\S]*prepareAndBuildDiscussPrompt[\s\S]*loadPrompt\("guided-discuss-milestone"/,
19
+ /if \(isGreenfield\)[\s\S]*prepareAndBuildDiscussPrompt[\s\S]*buildDiscussMilestonePrompt/,
20
20
  "greenfield branch must precede guided-discuss-milestone branch",
21
21
  );
22
22
  });
@@ -24,7 +24,7 @@ test("dispatchNewMilestoneDiscuss uses discuss.md only on greenfield projects",
24
24
  test("dispatchNewMilestoneDiscuss uses milestone-specific preparation guidance", () => {
25
25
  const source = readFileSync(join(__dirname, "..", "guided-flow.ts"), "utf-8");
26
26
  const fnBody = extractSourceRegion(source, "async function dispatchNewMilestoneDiscuss(");
27
- assert.match(fnBody, /buildDiscussPreparationContext\(ctx, basePath, "milestone"\)/);
27
+ assert.match(fnBody, /buildDiscussPreparationContext\(ctx, basePath, "milestone", true\)/);
28
28
  });
29
29
 
30
30
  test("launchNextMilestoneDiscuss routes through dispatchNewMilestoneDiscuss for normal path", () => {
@@ -83,6 +83,15 @@ test("plan-slice prompt: compact planning gates survive template substitution",
83
83
  assert.ok(!result.includes("{{"));
84
84
  });
85
85
 
86
+ test("plan-slice prompt: absence checks use negated quiet searches", () => {
87
+ const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
88
+ assert.ok(result.includes("For absence checks"));
89
+ assert.ok(result.includes("`! grep -q 'pattern' file`"));
90
+ assert.ok(result.includes("`! rg -q 'pattern' file`"));
91
+ assert.ok(result.includes("do not use `grep -c` or `rg -c`"));
92
+ assert.ok(result.includes("count commands exit 1 when they find zero matches"));
93
+ });
94
+
86
95
  test("plan-slice prompt: footer references gsd_plan_slice tool, not direct write", () => {
87
96
  const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
88
97
  assert.ok(
@@ -11,6 +11,7 @@ import {
11
11
  resetHookState,
12
12
  isRetryPending,
13
13
  consumeRetryTrigger,
14
+ consumeGateBlock,
14
15
  resolveHookArtifactPath,
15
16
  runPreDispatchHooks,
16
17
  persistHookState,
@@ -20,6 +21,7 @@ import {
20
21
  formatHookStatus,
21
22
  triggerHookManually,
22
23
  } from "../post-unit-hooks.ts";
24
+ import { invalidateAllCaches } from "../cache.ts";
23
25
 
24
26
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
25
27
 
@@ -29,6 +31,11 @@ function createFixtureBase(): string {
29
31
  return base;
30
32
  }
31
33
 
34
+ function writeHookPreferences(base: string, hookYaml: string): void {
35
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), `---\npost_unit_hooks:\n${hookYaml}\n---\n`, "utf-8");
36
+ invalidateAllCaches();
37
+ }
38
+
32
39
  // ═══════════════════════════════════════════════════════════════════════════
33
40
  // Phase 1: Post-Unit Hook Tests
34
41
  // ═══════════════════════════════════════════════════════════════════════════
@@ -104,6 +111,156 @@ test('consumeRetryTrigger clears state', () => {
104
111
  assert.ok(!isRetryPending(), "no retry initially");
105
112
  });
106
113
 
114
+ test('Advisory hook keeps artifact idempotency without verdict frontmatter', () => {
115
+ resetHookState();
116
+ const base = createFixtureBase();
117
+ try {
118
+ writeHookPreferences(base, ` - name: docs-hint
119
+ after:
120
+ - execute-task
121
+ prompt: Review docs
122
+ artifact: DOCS-HINT.md
123
+ `);
124
+ writeFileSync(resolveHookArtifactPath(base, "M001/S01/T01", "DOCS-HINT.md"), "plain advisory note", "utf-8");
125
+
126
+ const result = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
127
+ assert.deepStrictEqual(result, null, "existing advisory artifact remains idempotent");
128
+ assert.deepStrictEqual(consumeGateBlock(), null, "advisory hook does not create gate block");
129
+ } finally {
130
+ resetHookState();
131
+ invalidateAllCaches();
132
+ rmSync(base, { recursive: true, force: true });
133
+ }
134
+ });
135
+
136
+ test('Blocking hook skips only after passing frontmatter verdict', () => {
137
+ resetHookState();
138
+ const base = createFixtureBase();
139
+ try {
140
+ writeHookPreferences(base, ` - name: security-review
141
+ after:
142
+ - execute-task
143
+ prompt: Review security
144
+ artifact: SECURITY-REVIEW.md
145
+ criticality: blocking
146
+ `);
147
+ writeFileSync(
148
+ resolveHookArtifactPath(base, "M001/S01/T01", "SECURITY-REVIEW.md"),
149
+ "---\nverdict: pass\n---\n\nNo blocking findings.\n",
150
+ "utf-8",
151
+ );
152
+
153
+ const result = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
154
+ assert.deepStrictEqual(result, null, "passing gate artifact is idempotent");
155
+ assert.deepStrictEqual(consumeGateBlock(), null, "passing gate does not block");
156
+ } finally {
157
+ resetHookState();
158
+ invalidateAllCaches();
159
+ rmSync(base, { recursive: true, force: true });
160
+ }
161
+ });
162
+
163
+ test('Blocking hook reruns invalid artifact once then blocks at cycle budget', () => {
164
+ resetHookState();
165
+ const base = createFixtureBase();
166
+ try {
167
+ writeHookPreferences(base, ` - name: security-review
168
+ after:
169
+ - execute-task
170
+ prompt: Review security
171
+ artifact: SECURITY-REVIEW.md
172
+ criticality: blocking
173
+ `);
174
+ writeFileSync(resolveHookArtifactPath(base, "M001/S01/T01", "SECURITY-REVIEW.md"), "partial output", "utf-8");
175
+
176
+ const dispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
177
+ assert.ok(dispatch, "invalid gate artifact dispatches the blocking hook");
178
+ assert.equal(dispatch.unitType, "hook/security-review");
179
+
180
+ const afterHook = checkPostUnitHooks("hook/security-review", "M001/S01/T01", base);
181
+ assert.deepStrictEqual(afterHook, null, "no further hook dispatch after max_cycles=1");
182
+ const block = consumeGateBlock();
183
+ assert.ok(block, "gate block is recorded");
184
+ assert.equal(block.hookName, "security-review");
185
+ assert.match(block.reason, /missing frontmatter verdict/);
186
+ } finally {
187
+ resetHookState();
188
+ invalidateAllCaches();
189
+ rmSync(base, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ test('Blocking hook restored from disk does not trust artifact without clean hook completion', () => {
194
+ resetHookState();
195
+ const base = createFixtureBase();
196
+ try {
197
+ writeHookPreferences(base, ` - name: security-review
198
+ after:
199
+ - execute-task
200
+ prompt: Review security
201
+ artifact: SECURITY-REVIEW.md
202
+ criticality: blocking
203
+ max_cycles: 2
204
+ `);
205
+ const firstDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
206
+ assert.ok(firstDispatch, "gate dispatches first cycle");
207
+ persistHookState(base);
208
+
209
+ writeFileSync(
210
+ resolveHookArtifactPath(base, "M001/S01/T01", "SECURITY-REVIEW.md"),
211
+ "---\noutcome:\n verdict: pass\n---\n",
212
+ "utf-8",
213
+ );
214
+
215
+ resetHookState();
216
+ restoreHookState(base);
217
+
218
+ const resumed = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
219
+ assert.ok(resumed, "persisted active gate reruns when clean hook completion was not observed");
220
+ assert.equal(resumed.unitType, "hook/security-review");
221
+ } finally {
222
+ resetHookState();
223
+ invalidateAllCaches();
224
+ rmSync(base, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ test('Blocking hook needs-rework verdict requests trigger unit retry', () => {
229
+ resetHookState();
230
+ const base = createFixtureBase();
231
+ try {
232
+ writeHookPreferences(base, ` - name: review-arbiter
233
+ after:
234
+ - execute-task
235
+ prompt: Review task
236
+ artifact: REVIEW-DEBATE.md
237
+ criticality: blocking
238
+ max_cycles: 2
239
+ on_block:
240
+ action: retry-unit
241
+ `);
242
+ const dispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
243
+ assert.ok(dispatch, "gate dispatches");
244
+ writeFileSync(
245
+ resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-DEBATE.md"),
246
+ "---\nverdict: needs-rework\n---\n\nRework required.\n",
247
+ "utf-8",
248
+ );
249
+
250
+ const afterHook = checkPostUnitHooks("hook/review-arbiter", "M001/S01/T01", base);
251
+ assert.deepStrictEqual(afterHook, null, "needs-rework routes via retry signal");
252
+ assert.ok(isRetryPending(), "retry is pending");
253
+ assert.deepStrictEqual(consumeRetryTrigger(), {
254
+ unitType: "execute-task",
255
+ unitId: "M001/S01/T01",
256
+ });
257
+ } finally {
258
+ resetHookState();
259
+ invalidateAllCaches();
260
+ rmSync(base, { recursive: true, force: true });
261
+ }
262
+ });
263
+
107
264
  // ─── Variable substitution in prompts ──────────────────────────────────────
108
265
  test('Variable substitution', () => {
109
266
  const base = "/project";
@@ -8,6 +8,7 @@ import { mock } from "node:test";
8
8
  import { postUnitPostVerification, type PostUnitContext } from "../auto-post-unit.ts";
9
9
  import { AutoSession } from "../auto/session.ts";
10
10
  import { checkPostUnitHooks, resetHookState, resolveHookArtifactPath } from "../post-unit-hooks.ts";
11
+ import { emitJournalEvent } from "../journal.ts";
11
12
  import { _clearGsdRootCache } from "../paths.ts";
12
13
  import { invalidateAllCaches } from "../cache.ts";
13
14
 
@@ -28,6 +29,43 @@ post_unit_hooks:
28
29
  writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
29
30
  }
30
31
 
32
+ function writeFailingHookPreferences(basePath: string): void {
33
+ const content = `---
34
+ post_unit_hooks:
35
+ - name: review-arbiter
36
+ after:
37
+ - execute-task
38
+ prompt: Review {taskId}
39
+ artifact: REVIEW-DEBATE.md
40
+ max_cycles: 1
41
+ enabled: true
42
+ - name: follow-up-review
43
+ after:
44
+ - execute-task
45
+ prompt: Follow-up review {taskId}
46
+ enabled: true
47
+ ---
48
+ `;
49
+ writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
50
+ }
51
+
52
+ function writeBlockingPreferences(basePath: string): void {
53
+ const content = `---
54
+ post_unit_hooks:
55
+ - name: review-arbiter
56
+ after:
57
+ - execute-task
58
+ prompt: Review {taskId}
59
+ agent: arbiter
60
+ artifact: REVIEW-DEBATE.md
61
+ criticality: blocking
62
+ max_cycles: 2
63
+ enabled: true
64
+ ---
65
+ `;
66
+ writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
67
+ }
68
+
31
69
  test("post-unit retry_on marks trigger unit as retry in orchestrator before redispatch", async () => {
32
70
  const originalCwd = process.cwd();
33
71
  const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-retry-"));
@@ -91,3 +129,144 @@ test("post-unit retry_on marks trigger unit as retry in orchestrator before redi
91
129
  rmSync(base, { recursive: true, force: true });
92
130
  }
93
131
  });
132
+
133
+ test("failed post-unit hook pauses auto-mode even when its artifact exists", async () => {
134
+ const originalCwd = process.cwd();
135
+ const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-hook-failed-"));
136
+ const taskDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
137
+ mkdirSync(taskDir, { recursive: true });
138
+
139
+ try {
140
+ process.chdir(base);
141
+ _clearGsdRootCache();
142
+ invalidateAllCaches();
143
+ resetHookState();
144
+ writeFailingHookPreferences(base);
145
+
146
+ const hookDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
147
+ assert.equal(hookDispatch?.hookName, "review-arbiter");
148
+
149
+ const artifactPath = resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-DEBATE.md");
150
+ writeFileSync(artifactPath, "partial review", "utf-8");
151
+ emitJournalEvent(base, {
152
+ ts: "2026-06-03T12:00:00.000Z",
153
+ flowId: "flow-hook-failed",
154
+ seq: 3,
155
+ eventType: "unit-end",
156
+ data: {
157
+ unitType: "hook/review-arbiter",
158
+ unitId: "M001/S01/T01",
159
+ status: "cancelled",
160
+ artifactVerified: false,
161
+ },
162
+ });
163
+
164
+ const pauseAuto = mock.fn(async () => {});
165
+ const notifications: string[] = [];
166
+ const s = new AutoSession();
167
+ s.basePath = base;
168
+ s.active = true;
169
+ s.currentUnit = { type: "hook/review-arbiter", id: "M001/S01/T01", startedAt: Date.now() };
170
+
171
+ const pctx: PostUnitContext = {
172
+ s,
173
+ ctx: {
174
+ ui: {
175
+ notify: (message: string) => { notifications.push(message); },
176
+ setStatus: () => {},
177
+ setWidget: () => {},
178
+ setFooter: () => {},
179
+ },
180
+ model: { id: "test-model" },
181
+ } as any,
182
+ pi: { sendMessage: async () => {}, setModel: async () => true } as any,
183
+ buildSnapshotOpts: () => ({}),
184
+ lockBase: () => base,
185
+ stopAuto: async () => {},
186
+ pauseAuto,
187
+ updateProgressWidget: () => {},
188
+ };
189
+
190
+ const result = await postUnitPostVerification(pctx);
191
+ assert.equal(result, "stopped");
192
+ assert.equal(pauseAuto.mock.callCount(), 1);
193
+ assert.ok(
194
+ notifications.some(message => message.includes("Post-unit hook review-arbiter failed")),
195
+ "pause notification should explain the failed hook",
196
+ );
197
+ } finally {
198
+ process.chdir(originalCwd);
199
+ resetHookState();
200
+ invalidateAllCaches();
201
+ _clearGsdRootCache();
202
+ rmSync(base, { recursive: true, force: true });
203
+ }
204
+ });
205
+
206
+ test("post-unit blocking gate pauses auto-mode on needs-attention verdict", async () => {
207
+ const originalCwd = process.cwd();
208
+ const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-gate-"));
209
+ const taskDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
210
+ mkdirSync(taskDir, { recursive: true });
211
+
212
+ try {
213
+ process.chdir(base);
214
+ _clearGsdRootCache();
215
+ invalidateAllCaches();
216
+ resetHookState();
217
+ writeBlockingPreferences(base);
218
+
219
+ const hookDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
220
+ assert.ok(hookDispatch, "hook should dispatch for execute-task");
221
+
222
+ const artifactPath = resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-DEBATE.md");
223
+ writeFileSync(artifactPath, "---\nverdict: needs-attention\n---\n\nManual review required.\n", "utf-8");
224
+
225
+ const pauseAuto = mock.fn(async () => {});
226
+ const s = new AutoSession();
227
+ s.basePath = base;
228
+ s.active = true;
229
+ s.currentUnit = { type: "hook/review-arbiter", id: "M001/S01/T01", startedAt: Date.now() };
230
+ s.orchestration = {
231
+ start: async () => ({ kind: "started" }),
232
+ advance: async () => ({ kind: "stopped", reason: "unused" }),
233
+ completeActiveUnit: async () => {},
234
+ retryActiveUnit: async () => {},
235
+ resume: async () => ({ kind: "resumed" }),
236
+ stop: async (reason: string) => ({ kind: "stopped", reason }),
237
+ getStatus: () => ({ phase: "running", transitionCount: 0 }),
238
+ };
239
+
240
+ const notifications: string[] = [];
241
+ const pctx: PostUnitContext = {
242
+ s,
243
+ ctx: {
244
+ ui: {
245
+ notify: (message: string) => { notifications.push(message); },
246
+ setStatus: () => {},
247
+ setWidget: () => {},
248
+ setFooter: () => {},
249
+ },
250
+ model: { id: "test-model" },
251
+ } as any,
252
+ pi: { sendMessage: async () => {}, setModel: async () => true } as any,
253
+ buildSnapshotOpts: () => ({}),
254
+ lockBase: () => base,
255
+ stopAuto: async () => {},
256
+ pauseAuto,
257
+ updateProgressWidget: () => {},
258
+ };
259
+
260
+ const result = await postUnitPostVerification(pctx);
261
+ assert.equal(result, "stopped");
262
+ assert.equal(pauseAuto.mock.callCount(), 1);
263
+ assert.match(notifications.join("\n"), /Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01/);
264
+ assert.match(notifications.join("\n"), /\/gsd status/);
265
+ } finally {
266
+ process.chdir(originalCwd);
267
+ resetHookState();
268
+ invalidateAllCaches();
269
+ _clearGsdRootCache();
270
+ rmSync(base, { recursive: true, force: true });
271
+ }
272
+ });
@@ -561,6 +561,35 @@ test("post-unit hook max_cycles clamping via validatePreferences", () => {
561
561
  assert.equal(p4.post_unit_hooks![0].max_cycles, 3, "valid value passes through");
562
562
  });
563
563
 
564
+ test("post-unit hook criticality and on_block validation", () => {
565
+ const base = { name: "h", after: ["execute-task"], prompt: "do something", artifact: "REVIEW.md" };
566
+
567
+ const { preferences, errors } = validatePreferences({
568
+ post_unit_hooks: [{
569
+ ...base,
570
+ criticality: "blocking",
571
+ on_block: { action: "retry-unit", artifact: "NEEDS-REWORK.md" },
572
+ }],
573
+ } as any);
574
+ assert.equal(errors.length, 0);
575
+ assert.equal(preferences.post_unit_hooks![0].criticality, "blocking");
576
+ assert.deepEqual(preferences.post_unit_hooks![0].on_block, {
577
+ action: "retry-unit",
578
+ artifact: "NEEDS-REWORK.md",
579
+ });
580
+
581
+ const missingArtifact = validatePreferences({
582
+ post_unit_hooks: [{ name: "blocking", after: ["execute-task"], prompt: "do something", criticality: "blocking" }],
583
+ } as any);
584
+ assert.match(missingArtifact.errors.join("\n"), /criticality blocking requires artifact/);
585
+ assert.equal(missingArtifact.preferences.post_unit_hooks, undefined);
586
+
587
+ const invalidAction = validatePreferences({
588
+ post_unit_hooks: [{ ...base, on_block: { action: "teleport" } }],
589
+ } as any);
590
+ assert.match(invalidAction.errors.join("\n"), /invalid on_block action/);
591
+ });
592
+
564
593
  test("pre-dispatch hook action validation via validatePreferences", () => {
565
594
  const base = { name: "h", before: ["execute-task"] };
566
595
 
@@ -2,6 +2,7 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.ts";
5
6
 
6
7
  const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
7
8
  const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
@@ -25,11 +26,15 @@ test("reactive-execute prompt keeps task summaries with subagents and avoids bat
25
26
  test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence", () => {
26
27
  const prompt = readPrompt("run-uat");
27
28
  assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`\{\{uatType\}\}`/);
28
- assert.match(prompt, /uatType:\s*\{\{uatType\}\}/);
29
+ assert.match(prompt, /uatType:\s*"\{\{uatType\}\}"/);
30
+ assert.match(prompt, /gsd_uat_result_save/);
31
+ assert.match(prompt, /presentedTools/);
32
+ assert.match(prompt, /blockedTools/);
29
33
  assert.match(prompt, /live-runtime/);
30
34
  assert.match(prompt, /browser\/runtime\/network/i);
31
35
  assert.match(prompt, /NEEDS-HUMAN/);
32
36
  assert.doesNotMatch(prompt, /uatType:\s*artifact-driven/);
37
+ assert.doesNotMatch(prompt, /Call `gsd_summary_save`/);
33
38
  });
34
39
 
35
40
  test("run-uat prompt lists canonical gsd_uat_exec intent values", () => {
@@ -42,6 +47,22 @@ test("run-uat prompt lists canonical gsd_uat_exec intent values", () => {
42
47
  assert.match(prompt, /do not use `artifact`, `runtime`, or `human-follow-up` as `intent`/i);
43
48
  });
44
49
 
50
+ test("run-uat prompt gives the complete UAT result-save presentation contract", () => {
51
+ const prompt = readPrompt("run-uat");
52
+ assert.match(prompt, /Call `gsd_uat_result_save` once after all checks are complete/);
53
+ assert.doesNotMatch(prompt, /Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`/);
54
+
55
+ for (const toolName of RUN_UAT_WORKFLOW_TOOL_NAMES) {
56
+ assert.ok(prompt.includes(`"${toolName}"`), `prompt should include required presented tool ${toolName}`);
57
+ }
58
+
59
+ for (const toolName of ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"] as const) {
60
+ assert.ok(prompt.includes(`name: "${toolName}"`), `prompt should include blocked tool ${toolName}`);
61
+ }
62
+
63
+ assert.ok(prompt.includes("forbidden during run-uat"), "prompt should explain blocked run-uat tools");
64
+ });
65
+
45
66
  test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
46
67
  const prompt = readPrompt("workflow-start");
47
68
  assert.match(prompt, /Keep moving by default/i);
@@ -475,6 +496,27 @@ test("reactive-execute prompt references tool calls instead of checkbox updates"
475
496
  assert.match(prompt, /completion tool calls/);
476
497
  });
477
498
 
499
+ test("parallel subagent prompts forbid serialized tasks arrays", () => {
500
+ const expectations = [
501
+ { name: "reactive-execute", agent: "worker" },
502
+ { name: "parallel-research-slices", agent: "scout" },
503
+ { name: "gate-evaluate", agent: "tester" },
504
+ ] as const;
505
+
506
+ for (const { name, agent } of expectations) {
507
+ const prompt = readPrompt(name);
508
+ assert.match(prompt, /tasks:\s*\[\.\.\.\]/, `${name} must show the native array placeholder`);
509
+ assert.match(prompt, /native JSON array/i, `${name} must explicitly require a native JSON array`);
510
+ assert.match(prompt, /Do NOT JSON\.stringify/i, `${name} must forbid JSON.stringify on tasks`);
511
+ assert.match(prompt, /must be array/i, `${name} must mention the subagent validation failure`);
512
+ assert.match(
513
+ prompt,
514
+ new RegExp(`tasks:\\s*\\[\\{\\s*agent:\\s*"${agent}"`, "i"),
515
+ `${name} must show the concrete ${agent} task call shape`,
516
+ );
517
+ }
518
+ });
519
+
478
520
  // ─── Project-shape classifier + 3-or-4-options-with-Other-hatch contract ──
479
521
 
480
522
  test("guided-discuss-project classifies project shape and persists the verdict to PROJECT.md", () => {
@@ -22,6 +22,20 @@ test("resolveExtensionDirFromCandidates prefers user-local dir when both trees a
22
22
  assert.equal(resolved, agentDir);
23
23
  });
24
24
 
25
+ test("resolveExtensionDirFromCandidates prefers source checkout dir when both trees are valid", () => {
26
+ const moduleDir = "/repo/src/resources/extensions/gsd";
27
+ const agentDir = "/home/user/.gsd/agent/extensions/gsd";
28
+ const paths = new Set<string>([
29
+ join(moduleDir, "prompts"),
30
+ join(moduleDir, "templates", "task-summary.md"),
31
+ join(agentDir, "prompts"),
32
+ join(agentDir, "templates", "task-summary.md"),
33
+ ]);
34
+
35
+ const resolved = resolveExtensionDirFromCandidates(moduleDir, agentDir, makeExists(paths));
36
+ assert.equal(resolved, moduleDir);
37
+ });
38
+
25
39
  test("resolveExtensionDirFromCandidates rejects module dir missing task-summary template", () => {
26
40
  const moduleDir = "/npm/global/gsd";
27
41
  const agentDir = "/home/user/.gsd/agent/extensions/gsd";