@opengsd/gsd-pi 1.1.1-dev.616a1a1 → 1.1.1-dev.74e8dd1

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 (232) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +167 -16
  3. package/dist/resources/extensions/gsd/auto/phases.js +4 -3
  4. package/dist/resources/extensions/gsd/auto-dashboard.js +15 -4
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +39 -0
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +113 -7
  7. package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +4 -4
  9. package/dist/resources/extensions/gsd/auto-start.js +94 -15
  10. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +2 -1
  11. package/dist/resources/extensions/gsd/auto.js +22 -4
  12. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +79 -0
  13. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +43 -0
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +30 -9
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -10
  16. package/dist/resources/extensions/gsd/commands/catalog.js +6 -1
  17. package/dist/resources/extensions/gsd/commands/handlers/core.js +6 -2
  18. package/dist/resources/extensions/gsd/commands/handlers/ops.js +7 -3
  19. package/dist/resources/extensions/gsd/commands-maintenance.js +172 -2
  20. package/dist/resources/extensions/gsd/commands-mcp-status.js +107 -59
  21. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -1
  22. package/dist/resources/extensions/gsd/commands-verdict.js +1 -1
  23. package/dist/resources/extensions/gsd/config-overlay.js +2 -1
  24. package/dist/resources/extensions/gsd/error-classifier.js +2 -1
  25. package/dist/resources/extensions/gsd/exec-sandbox.js +2 -0
  26. package/dist/resources/extensions/gsd/gsd-db.js +37 -4
  27. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  28. package/dist/resources/extensions/gsd/mcp-filter.js +3 -0
  29. package/dist/resources/extensions/gsd/mcp-project-config.js +67 -8
  30. package/dist/resources/extensions/gsd/migration-auto-check.js +2 -2
  31. package/dist/resources/extensions/gsd/prompts/run-uat.md +10 -4
  32. package/dist/resources/extensions/gsd/prompts/system.md +3 -1
  33. package/dist/resources/extensions/gsd/safety/destructive-guard.js +3 -0
  34. package/dist/resources/extensions/gsd/skill-activation.js +20 -3
  35. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +4 -2
  36. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +1 -1
  37. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +18 -1
  38. package/dist/resources/extensions/gsd/state-reconciliation/index.js +6 -0
  39. package/dist/resources/extensions/gsd/state.js +15 -12
  40. package/dist/resources/extensions/gsd/tool-presentation-plan.js +120 -0
  41. package/dist/resources/extensions/gsd/tools/exec-tool.js +109 -0
  42. package/dist/resources/extensions/gsd/tools/plan-slice.js +14 -9
  43. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +2 -2
  44. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +366 -3
  45. package/dist/resources/extensions/gsd/unit-context-manifest.js +8 -3
  46. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -0
  47. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -1
  48. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -1
  49. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -0
  50. package/dist/resources/extensions/mcp-client/manager.js +31 -1
  51. package/dist/web/standalone/.next/BUILD_ID +1 -1
  52. package/dist/web/standalone/.next/app-path-routes-manifest.json +4 -4
  53. package/dist/web/standalone/.next/build-manifest.json +2 -2
  54. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  55. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.html +1 -1
  72. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app-paths-manifest.json +4 -4
  79. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  80. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  82. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  83. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  84. package/package.json +2 -2
  85. package/packages/cloud-mcp-gateway/package.json +2 -2
  86. package/packages/contracts/dist/workflow.d.ts +14 -0
  87. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  88. package/packages/contracts/dist/workflow.js +16 -0
  89. package/packages/contracts/dist/workflow.js.map +1 -1
  90. package/packages/contracts/package.json +1 -1
  91. package/packages/daemon/package.json +4 -4
  92. package/packages/gsd-agent-core/package.json +5 -5
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  94. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  95. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
  96. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
  98. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +72 -31
  100. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.d.ts.map +1 -1
  102. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.js +2 -0
  103. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
  104. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  106. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  108. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +1 -0
  110. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
  112. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +5 -0
  113. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
  114. package/packages/gsd-agent-modes/package.json +7 -7
  115. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  116. package/packages/mcp-server/dist/workflow-tools.js +82 -0
  117. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  118. package/packages/mcp-server/package.json +3 -3
  119. package/packages/native/package.json +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/image-models.generated.d.ts +15 -0
  122. package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/image-models.generated.js +15 -0
  124. package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
  125. package/packages/pi-ai/dist/models.generated.d.ts +338 -17
  126. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  127. package/packages/pi-ai/dist/models.generated.js +412 -112
  128. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  129. package/packages/pi-ai/package.json +1 -1
  130. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  131. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  133. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  134. package/packages/pi-coding-agent/package.json +7 -7
  135. package/packages/pi-tui/dist/terminal.d.ts +1 -0
  136. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  137. package/packages/pi-tui/dist/terminal.js +8 -4
  138. package/packages/pi-tui/dist/terminal.js.map +1 -1
  139. package/packages/pi-tui/package.json +1 -1
  140. package/packages/rpc-client/package.json +2 -2
  141. package/pkg/package.json +1 -1
  142. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +196 -16
  143. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +239 -63
  144. package/src/resources/extensions/gsd/auto/phases.ts +5 -3
  145. package/src/resources/extensions/gsd/auto-dashboard.ts +16 -4
  146. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -0
  147. package/src/resources/extensions/gsd/auto-post-unit.ts +138 -7
  148. package/src/resources/extensions/gsd/auto-prompts.ts +9 -0
  149. package/src/resources/extensions/gsd/auto-recovery.ts +4 -4
  150. package/src/resources/extensions/gsd/auto-start.ts +112 -17
  151. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +2 -1
  152. package/src/resources/extensions/gsd/auto.ts +35 -3
  153. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +86 -0
  154. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +51 -0
  155. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +51 -14
  156. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +21 -10
  157. package/src/resources/extensions/gsd/commands/catalog.ts +6 -1
  158. package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -2
  159. package/src/resources/extensions/gsd/commands/handlers/ops.ts +7 -3
  160. package/src/resources/extensions/gsd/commands-maintenance.ts +197 -2
  161. package/src/resources/extensions/gsd/commands-mcp-status.ts +134 -57
  162. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -1
  163. package/src/resources/extensions/gsd/commands-verdict.ts +1 -1
  164. package/src/resources/extensions/gsd/config-overlay.ts +3 -1
  165. package/src/resources/extensions/gsd/error-classifier.ts +2 -1
  166. package/src/resources/extensions/gsd/exec-sandbox.ts +4 -0
  167. package/src/resources/extensions/gsd/gsd-db.ts +41 -6
  168. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  169. package/src/resources/extensions/gsd/mcp-filter.ts +3 -0
  170. package/src/resources/extensions/gsd/mcp-project-config.ts +92 -10
  171. package/src/resources/extensions/gsd/migration-auto-check.ts +2 -2
  172. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  173. package/src/resources/extensions/gsd/prompts/run-uat.md +10 -4
  174. package/src/resources/extensions/gsd/prompts/system.md +3 -1
  175. package/src/resources/extensions/gsd/safety/destructive-guard.ts +3 -0
  176. package/src/resources/extensions/gsd/skill-activation.ts +20 -2
  177. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +4 -2
  178. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +1 -1
  179. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +20 -0
  180. package/src/resources/extensions/gsd/state-reconciliation/index.ts +6 -0
  181. package/src/resources/extensions/gsd/state-reconciliation/types.ts +1 -0
  182. package/src/resources/extensions/gsd/state.ts +16 -12
  183. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +51 -0
  184. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +86 -0
  185. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +143 -2
  186. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +24 -2
  187. package/src/resources/extensions/gsd/tests/commands-dispatcher-validation-block.test.ts +38 -3
  188. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +6 -2
  189. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +8 -0
  190. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +50 -13
  191. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +60 -0
  192. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +18 -0
  193. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +69 -0
  194. package/src/resources/extensions/gsd/tests/gsd-rebuild.test.ts +199 -0
  195. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +75 -0
  196. package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +13 -6
  197. package/src/resources/extensions/gsd/tests/mcp-filter.test.ts +15 -0
  198. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +68 -0
  199. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +177 -0
  200. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +3 -3
  201. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +54 -7
  202. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +39 -1
  203. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -0
  204. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  205. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +36 -0
  206. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +35 -0
  207. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +1 -1
  208. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +55 -0
  209. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +6 -2
  210. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  211. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +84 -10
  212. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +12 -2
  213. package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +29 -6
  214. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +29 -6
  215. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +21 -0
  216. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +17 -2
  217. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +83 -0
  218. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +25 -0
  219. package/src/resources/extensions/gsd/tool-presentation-plan.ts +167 -0
  220. package/src/resources/extensions/gsd/tools/exec-tool.ts +130 -0
  221. package/src/resources/extensions/gsd/tools/plan-slice.ts +14 -9
  222. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +2 -2
  223. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +440 -2
  224. package/src/resources/extensions/gsd/unit-context-manifest.ts +14 -5
  225. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -0
  226. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -1
  227. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -1
  228. package/src/resources/extensions/gsd/worktree-lifecycle.ts +26 -0
  229. package/src/resources/extensions/mcp-client/manager.ts +33 -1
  230. package/src/resources/extensions/mcp-client/tests/manager.test.ts +35 -0
  231. /package/dist/web/standalone/.next/static/{L9N5SPFi7f-Ne4u2uXzCe → eRWf-RI9bzbrwEurm_3uI}/_buildManifest.js +0 -0
  232. /package/dist/web/standalone/.next/static/{L9N5SPFi7f-Ne4u2uXzCe → eRWf-RI9bzbrwEurm_3uI}/_ssgManifest.js +0 -0
@@ -5,6 +5,27 @@ import { realpathSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { isContextModeEnabled } from "../preferences-types.js";
7
7
  import { contextModeDisabledResult } from "./context-mode-tool-result.js";
8
+ const UAT_EXEC_INTENTS = [
9
+ "uat-artifact-check",
10
+ "uat-runtime-check",
11
+ "uat-browser-check",
12
+ "uat-service-start",
13
+ "uat-log-inspection",
14
+ ];
15
+ const UAT_EXEC_INTENT_ALIASES = {
16
+ artifact: "uat-artifact-check",
17
+ "artifact-driven": "uat-artifact-check",
18
+ runtime: "uat-runtime-check",
19
+ "runtime-executable": "uat-runtime-check",
20
+ "live-runtime": "uat-runtime-check",
21
+ browser: "uat-browser-check",
22
+ "browser-executable": "uat-browser-check",
23
+ service: "uat-service-start",
24
+ "service-start": "uat-service-start",
25
+ log: "uat-log-inspection",
26
+ logs: "uat-log-inspection",
27
+ "log-inspection": "uat-log-inspection",
28
+ };
8
29
  export function buildExecOptions(baseDir, cfg, extras) {
9
30
  const allowlist = Array.isArray(cfg?.exec_env_allowlist) ? cfg.exec_env_allowlist : EXEC_DEFAULTS.envAllowlist;
10
31
  const stdoutCap = clampNumber(cfg?.exec_stdout_cap_bytes, EXEC_DEFAULTS.stdoutCapBytes, 4_096, 16_777_216);
@@ -73,6 +94,39 @@ function normalizeScript(params) {
73
94
  }
74
95
  return paramError("script is required and must be a non-empty string");
75
96
  }
97
+ function normalizeRequiredString(value, field) {
98
+ if (typeof value !== "string" || value.trim().length === 0) {
99
+ return paramError(`${field} is required and must be a non-empty string`);
100
+ }
101
+ return value.trim();
102
+ }
103
+ function normalizeUatIntent(value) {
104
+ if (typeof value !== "string") {
105
+ return paramError(`intent is required and must be one of: ${UAT_EXEC_INTENTS.join(", ")}`);
106
+ }
107
+ const normalized = value.trim().toLowerCase();
108
+ if (UAT_EXEC_INTENTS.includes(normalized))
109
+ return normalized;
110
+ const alias = UAT_EXEC_INTENT_ALIASES[normalized];
111
+ if (alias)
112
+ return alias;
113
+ return paramError(`invalid intent "${value}" — must be one of: ${UAT_EXEC_INTENTS.join(", ")}`);
114
+ }
115
+ function rejectUatScript(script) {
116
+ const patterns = [
117
+ { re: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|remove|update|upgrade)\b/i, reason: "package dependency mutation is not allowed during UAT" },
118
+ { re: /\b(?:pip|pip3|python\s+-m\s+pip)\s+install\b/i, reason: "package dependency mutation is not allowed during UAT" },
119
+ { re: /\bgit\s+(?:add|commit|push|reset|checkout|switch|merge|rebase|clean|rm|mv|tag|branch)\b/i, reason: "git mutations are not allowed during UAT" },
120
+ { re: /\brm\s+-[^\n\r;|&]*r[^\n\r;|&]*f\b/i, reason: "destructive filesystem cleanup is not allowed during UAT" },
121
+ { re: /\b(?:env|printenv)\b(?:\s|$)/i, reason: "dumping environment variables is not allowed during UAT" },
122
+ { re: /\bcat\s+\.env(?:\b|\.|$)/i, reason: "reading credential files is not allowed during UAT" },
123
+ ];
124
+ for (const pattern of patterns) {
125
+ if (pattern.re.test(script))
126
+ return pattern.reason;
127
+ }
128
+ return null;
129
+ }
76
130
  function isToolExecutionResult(value) {
77
131
  return typeof value === "object" && value !== null && Array.isArray(value.content);
78
132
  }
@@ -207,6 +261,7 @@ export async function executeGsdExec(params, deps) {
207
261
  runtime,
208
262
  script,
209
263
  ...(typeof params.purpose === "string" ? { purpose: params.purpose } : {}),
264
+ ...(params.metadata && typeof params.metadata === "object" ? { metadata: params.metadata } : {}),
210
265
  ...(typeof params.timeout_ms === "number" ? { timeout_ms: params.timeout_ms } : {}),
211
266
  }, opts);
212
267
  return formatResult(result);
@@ -220,6 +275,60 @@ export async function executeGsdExec(params, deps) {
220
275
  };
221
276
  }
222
277
  }
278
+ export async function executeUatExec(params, deps) {
279
+ const milestoneId = normalizeRequiredString(params.milestoneId, "milestoneId");
280
+ if (isToolExecutionResult(milestoneId))
281
+ return milestoneId;
282
+ const sliceId = normalizeRequiredString(params.sliceId, "sliceId");
283
+ if (isToolExecutionResult(sliceId))
284
+ return sliceId;
285
+ const checkId = normalizeRequiredString(params.checkId, "checkId");
286
+ if (isToolExecutionResult(checkId))
287
+ return checkId;
288
+ const intent = normalizeUatIntent(params.intent);
289
+ if (isToolExecutionResult(intent))
290
+ return intent;
291
+ const script = normalizeScript(params);
292
+ if (isToolExecutionResult(script))
293
+ return script;
294
+ const rejected = rejectUatScript(script);
295
+ if (rejected) {
296
+ return {
297
+ content: [{ type: "text", text: `Error: gsd_uat_exec blocked command — ${rejected}` }],
298
+ details: { operation: "gsd_uat_exec", error: "uat_exec_policy_block", reason: rejected },
299
+ isError: true,
300
+ };
301
+ }
302
+ const result = await executeGsdExec({
303
+ ...params,
304
+ script,
305
+ purpose: typeof params.purpose === "string" && params.purpose.trim().length > 0
306
+ ? params.purpose
307
+ : `UAT ${milestoneId}/${sliceId}/${checkId} (${intent})`,
308
+ metadata: {
309
+ kind: "uat_exec",
310
+ milestoneId,
311
+ sliceId,
312
+ checkId,
313
+ intent,
314
+ ...(typeof params.expected === "string" && params.expected.trim().length > 0
315
+ ? { expected: params.expected.trim() }
316
+ : {}),
317
+ },
318
+ }, deps);
319
+ const details = result.details ?? {};
320
+ return {
321
+ ...result,
322
+ details: {
323
+ ...details,
324
+ operation: "gsd_uat_exec",
325
+ milestoneId,
326
+ sliceId,
327
+ checkId,
328
+ intent,
329
+ },
330
+ };
331
+ }
223
332
  function formatResult(result) {
224
333
  const headerLines = [
225
334
  `gsd_exec[${result.id}] runtime=${result.runtime} exit=${formatExit(result)} duration=${result.duration_ms}ms`,
@@ -268,15 +268,9 @@ export async function handlePlanSlice(rawParams, basePath) {
268
268
  for (const taskId of omittedTaskIds) {
269
269
  deleteTask(params.milestoneId, params.sliceId, taskId);
270
270
  }
271
+ const existingTaskById = new Map(existingTasks.map((task) => [task.id, task]));
271
272
  for (const task of params.tasks) {
272
- insertTask({
273
- id: task.taskId,
274
- sliceId: params.sliceId,
275
- milestoneId: params.milestoneId,
276
- title: task.title,
277
- status: "pending",
278
- });
279
- upsertTaskPlanning(params.milestoneId, params.sliceId, task.taskId, {
273
+ const planning = {
280
274
  title: task.title,
281
275
  description: task.description,
282
276
  estimate: task.estimate,
@@ -287,7 +281,18 @@ export async function handlePlanSlice(rawParams, basePath) {
287
281
  observabilityImpact: task.observabilityImpact ?? "",
288
282
  fullPlanMd: task.fullPlanMd,
289
283
  targetRepositories: task.targetRepositories ?? params.targetRepositories ?? defaultTargets,
290
- });
284
+ };
285
+ const existingTask = existingTaskById.get(task.taskId);
286
+ if (!existingTask || !isClosedStatus(existingTask.status)) {
287
+ insertTask({
288
+ id: task.taskId,
289
+ sliceId: params.sliceId,
290
+ milestoneId: params.milestoneId,
291
+ title: task.title,
292
+ status: "pending",
293
+ });
294
+ }
295
+ upsertTaskPlanning(params.milestoneId, params.sliceId, task.taskId, planning);
291
296
  }
292
297
  // Seed quality gate rows inside the transaction — all-or-nothing with
293
298
  // the plan data so a crash can't leave orphaned gates without tasks.
@@ -7,7 +7,7 @@
7
7
  * artifacts so the DB-filesystem reconciler does not auto-correct
8
8
  * entities back to "complete".
9
9
  */
10
- import { getMilestone, getMilestoneSlices, getSliceTasks, updateMilestoneStatus, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
10
+ import { getMilestone, getMilestoneSlices, getSliceTasks, reopenMilestoneStatus, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
11
11
  import { invalidateStateCache } from "../state.js";
12
12
  import { isClosedStatus } from "../status-guards.js";
13
13
  import { renderAllProjections } from "../workflow-projections.js";
@@ -37,7 +37,7 @@ export async function handleReopenMilestone(params, basePath) {
37
37
  guardError = `milestone ${params.milestoneId} is not closed (status: ${milestone.status}) — nothing to reopen`;
38
38
  return;
39
39
  }
40
- updateMilestoneStatus(params.milestoneId, "active", null);
40
+ reopenMilestoneStatus(params.milestoneId);
41
41
  const slices = getMilestoneSlices(params.milestoneId);
42
42
  slicesResetCount = slices.length;
43
43
  for (const slice of slices) {
@@ -3,13 +3,13 @@
3
3
  import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
4
4
  import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-milestone.js";
5
5
  import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot, shouldBlockRootArtifactSaveInSnapshot } from "../bootstrap/write-gate.js";
6
- import { getActiveRequirements, insertMilestone, getMilestone, getSliceStatusSummary, getSliceTaskCounts, readTransaction, saveGateResult, } from "../gsd-db.js";
6
+ import { getActiveRequirements, insertMilestone, getMilestone, getSliceStatusSummary, getSliceTaskCounts, insertGateRun, readTransaction, saveGateResult, upsertQualityGate, } from "../gsd-db.js";
7
7
  import { GATE_REGISTRY } from "../gate-registry.js";
8
8
  import { generateRequirementsMd, saveArtifactToDb } from "../db-writer.js";
9
9
  import { clearPathCache, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
10
10
  import { saveFile, clearParseCache } from "../files.js";
11
- import { unlinkSync } from "node:fs";
12
- import { join } from "node:path";
11
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
12
+ import { isAbsolute, join, resolve } from "node:path";
13
13
  import { handleCompleteMilestone } from "./complete-milestone.js";
14
14
  import { handleCompleteTask } from "./complete-task.js";
15
15
  import { handleCompleteSlice } from "./complete-slice.js";
@@ -26,6 +26,7 @@ import { invalidateStateCache } from "../state.js";
26
26
  import { loadEffectiveGSDPreferences } from "../preferences.js";
27
27
  import { parseProject } from "../schemas/parsers.js";
28
28
  import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
29
+ import { canonicalWorkflowToolName, parseMcpToolName, RUN_UAT_FORBIDDEN_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES, } from "../tool-presentation-plan.js";
29
30
  export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = [
30
31
  "SUMMARY",
31
32
  "RESEARCH",
@@ -775,6 +776,368 @@ export async function executeSaveGateResult(params, basePath = process.cwd()) {
775
776
  };
776
777
  }
777
778
  }
779
+ function errorResult(operation, message, error) {
780
+ return {
781
+ content: [{ type: "text", text: `Error: ${message}` }],
782
+ details: { operation, error },
783
+ isError: true,
784
+ };
785
+ }
786
+ function isNonEmptyString(value) {
787
+ return typeof value === "string" && value.trim().length > 0;
788
+ }
789
+ function ensureUatRequiredFields(params) {
790
+ if (!isNonEmptyString(params.milestoneId))
791
+ return "milestoneId is required";
792
+ if (!isNonEmptyString(params.sliceId))
793
+ return "sliceId is required";
794
+ if (!isNonEmptyString(params.uatType))
795
+ return "uatType is required";
796
+ if (!["PASS", "FAIL", "PARTIAL"].includes(params.verdict))
797
+ return "verdict must be PASS, FAIL, or PARTIAL";
798
+ if (!Array.isArray(params.checks) || params.checks.length === 0)
799
+ return "checks must contain at least one UAT check";
800
+ if (!params.presentation || !Array.isArray(params.presentation.presentedTools))
801
+ return "presentation.presentedTools is required";
802
+ if (!Array.isArray(params.presentation.blockedTools))
803
+ return "presentation.blockedTools is required";
804
+ return null;
805
+ }
806
+ function approvedEvidenceRoots(basePath) {
807
+ const contract = resolveGsdPathContract(basePath);
808
+ return [contract.worktreeGsd, contract.projectGsd].filter((root) => typeof root === "string");
809
+ }
810
+ function approvedBrowserArtifactRoots(basePath) {
811
+ const contract = resolveGsdPathContract(basePath);
812
+ const roots = [contract.workRoot, contract.projectRoot].map((root) => join(root, ".artifacts", "browser"));
813
+ return [...new Set(roots)];
814
+ }
815
+ function pathStartsWithin(parent, target) {
816
+ const normalizedParent = parent.replace(/\\/g, "/").replace(/\/+$/, "");
817
+ const normalizedTarget = target.replace(/\\/g, "/").replace(/\/+$/, "");
818
+ return normalizedTarget === normalizedParent || normalizedTarget.startsWith(`${normalizedParent}/`);
819
+ }
820
+ function pushUnique(paths, candidate) {
821
+ if (!paths.includes(candidate))
822
+ paths.push(candidate);
823
+ }
824
+ function execMetaPathCandidates(basePath, ref) {
825
+ const trimmed = ref.trim();
826
+ const candidates = [];
827
+ const execDirs = approvedEvidenceRoots(basePath).map((root) => join(root, "exec"));
828
+ const normalizedRef = trimmed.replace(/\\/g, "/");
829
+ const pathLike = normalizedRef.endsWith(".meta.json") || normalizedRef.includes("/.gsd/exec/");
830
+ if (pathLike) {
831
+ const rawPath = isAbsolute(trimmed) ? resolve(trimmed) : resolve(basePath, trimmed);
832
+ pushUnique(candidates, rawPath);
833
+ const relativeExecMarker = ".gsd/exec/";
834
+ const markerIndex = normalizedRef.indexOf(relativeExecMarker);
835
+ if (markerIndex >= 0) {
836
+ const execRelative = normalizedRef.slice(markerIndex + relativeExecMarker.length);
837
+ for (const execDir of execDirs) {
838
+ pushUnique(candidates, join(execDir, execRelative));
839
+ }
840
+ }
841
+ return candidates.filter((candidate) => execDirs.some((execDir) => pathStartsWithin(execDir, candidate)));
842
+ }
843
+ for (const execDir of execDirs) {
844
+ pushUnique(candidates, join(execDir, `${trimmed}.meta.json`));
845
+ }
846
+ return candidates;
847
+ }
848
+ function resolveExecMetaPath(basePath, ref) {
849
+ for (const candidate of execMetaPathCandidates(basePath, ref)) {
850
+ if (existsSync(candidate))
851
+ return candidate;
852
+ }
853
+ return null;
854
+ }
855
+ function evidencePathIsApproved(basePath, ref) {
856
+ const normalizedRef = ref.replace(/\\/g, "/");
857
+ if (normalizedRef.startsWith(".gsd/exec/") || normalizedRef.startsWith(".gsd/uat/"))
858
+ return true;
859
+ if (normalizedRef.startsWith(".artifacts/browser/")) {
860
+ const resolvedRef = resolve(basePath, ref);
861
+ return approvedBrowserArtifactRoots(basePath).some((root) => pathStartsWithin(root, resolvedRef));
862
+ }
863
+ const gsdEvidenceApproved = approvedEvidenceRoots(basePath).some((root) => {
864
+ return pathStartsWithin(join(root, "exec"), ref) || pathStartsWithin(join(root, "uat"), ref);
865
+ });
866
+ if (gsdEvidenceApproved)
867
+ return true;
868
+ return approvedBrowserArtifactRoots(basePath).some((root) => pathStartsWithin(root, ref));
869
+ }
870
+ function validateEvidenceRef(basePath, evidence) {
871
+ if (!isNonEmptyString(evidence.ref))
872
+ return "evidence.ref is required";
873
+ if (evidence.kind === "gsd_uat_exec" || evidence.kind === "gsd_exec") {
874
+ const path = resolveExecMetaPath(basePath, evidence.ref.trim());
875
+ if (!path)
876
+ return `missing gsd_exec metadata for evidence id "${evidence.ref}"`;
877
+ if (evidence.kind === "gsd_uat_exec") {
878
+ try {
879
+ const meta = JSON.parse(readFileSync(path, "utf-8"));
880
+ if (meta.metadata?.kind !== "uat_exec")
881
+ return `evidence id "${evidence.ref}" is not typed as uat_exec`;
882
+ }
883
+ catch {
884
+ return `invalid gsd_exec metadata JSON for evidence id "${evidence.ref}"`;
885
+ }
886
+ }
887
+ return null;
888
+ }
889
+ if (evidence.kind === "url") {
890
+ try {
891
+ const parsed = new URL(evidence.ref);
892
+ return parsed.protocol === "http:" || parsed.protocol === "https:"
893
+ ? null
894
+ : `invalid URL evidence ref "${evidence.ref}"`;
895
+ }
896
+ catch {
897
+ return `invalid URL evidence ref "${evidence.ref}"`;
898
+ }
899
+ }
900
+ return evidencePathIsApproved(basePath, evidence.ref)
901
+ ? null
902
+ : `evidence ref "${evidence.ref}" is outside approved evidence locations`;
903
+ }
904
+ function validateUatChecks(basePath, params) {
905
+ for (const check of params.checks) {
906
+ if (!isNonEmptyString(check.id))
907
+ return "every check must have a non-empty id";
908
+ if (!isNonEmptyString(check.description))
909
+ return `check ${check.id} must have a description`;
910
+ if (!["artifact", "runtime", "browser", "human-follow-up"].includes(check.mode)) {
911
+ return `check ${check.id} has invalid mode "${check.mode}"`;
912
+ }
913
+ if (!["PASS", "FAIL", "NEEDS-HUMAN"].includes(check.result)) {
914
+ return `check ${check.id} has invalid result "${check.result}"`;
915
+ }
916
+ if (check.result === "PASS" || check.result === "FAIL") {
917
+ if (!Array.isArray(check.evidence) || check.evidence.length === 0) {
918
+ return `check ${check.id} is ${check.result} but has no objective evidence`;
919
+ }
920
+ for (const evidence of check.evidence) {
921
+ const error = validateEvidenceRef(basePath, evidence);
922
+ if (error)
923
+ return `check ${check.id}: ${error}`;
924
+ }
925
+ }
926
+ else if (!isNonEmptyString(check.notes)) {
927
+ return `check ${check.id} is NEEDS-HUMAN but has no manual instruction or reason`;
928
+ }
929
+ }
930
+ return null;
931
+ }
932
+ function validateUatMode(params) {
933
+ const modes = new Set(params.checks.map((check) => check.mode));
934
+ const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
935
+ if (hasHuman &&
936
+ params.verdict === "PASS" &&
937
+ !["human-experience", "mixed", "live-runtime"].includes(params.uatType) &&
938
+ !params.checks.every((check) => check.result !== "NEEDS-HUMAN" || check.nonAutomatable === true)) {
939
+ return "NEEDS-HUMAN checks can only coexist with PASS for human-experience, mixed, live-runtime, or explicitly non-automatable checks";
940
+ }
941
+ if (params.uatType === "runtime-executable" && !modes.has("runtime")) {
942
+ return "runtime-executable UAT requires at least one runtime check";
943
+ }
944
+ if (params.uatType === "browser-executable" && !modes.has("browser")) {
945
+ return "browser-executable UAT requires at least one browser check";
946
+ }
947
+ if (params.uatType === "live-runtime" && !modes.has("runtime") && !modes.has("browser")) {
948
+ return "live-runtime UAT requires runtime or browser evidence";
949
+ }
950
+ if (params.uatType === "artifact-driven" && hasHuman && params.verdict === "PASS") {
951
+ return "artifact-driven UAT cannot PASS with human-only checks";
952
+ }
953
+ return null;
954
+ }
955
+ function validateCanonicalPresentation(params) {
956
+ const aliasHints = {
957
+ gsd_save_summary: "gsd_summary_save",
958
+ gsd_complete_task: "gsd_task_complete",
959
+ gsd_complete_slice: "gsd_slice_complete",
960
+ gsd_milestone_complete: "gsd_complete_milestone",
961
+ };
962
+ for (const toolName of params.presentation.presentedTools) {
963
+ const baseName = parseMcpToolName(toolName)?.tool ?? toolName;
964
+ const canonical = aliasHints[baseName];
965
+ if (canonical)
966
+ return `presentation tool "${toolName}" uses an alias; use canonical "${canonical}"`;
967
+ }
968
+ const presentedCanonical = new Set(params.presentation.presentedTools.map((toolName) => canonicalWorkflowToolName(parseMcpToolName(toolName)?.tool ?? toolName)));
969
+ for (const requiredTool of RUN_UAT_WORKFLOW_TOOL_NAMES) {
970
+ if (!presentedCanonical.has(requiredTool)) {
971
+ return `presentation is missing required UAT tool "${requiredTool}"`;
972
+ }
973
+ }
974
+ const forbiddenCanonical = new Set(RUN_UAT_FORBIDDEN_TOOL_NAMES
975
+ .filter((toolName) => !toolName.includes("*"))
976
+ .map((toolName) => canonicalWorkflowToolName(parseMcpToolName(toolName)?.tool ?? toolName)));
977
+ for (const toolName of params.presentation.presentedTools) {
978
+ const canonical = canonicalWorkflowToolName(parseMcpToolName(toolName)?.tool ?? toolName);
979
+ if (toolName === "mcp__gsd-workflow__*" || forbiddenCanonical.has(canonical)) {
980
+ return `presentation includes forbidden run-uat tool "${toolName}"`;
981
+ }
982
+ }
983
+ const blockedCanonical = new Set(params.presentation.blockedTools.map((entry) => canonicalWorkflowToolName(parseMcpToolName(entry.name)?.tool ?? entry.name)));
984
+ for (const blockedTool of ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"]) {
985
+ if (!blockedCanonical.has(blockedTool)) {
986
+ return `presentation must record "${blockedTool}" as blocked during run-uat`;
987
+ }
988
+ }
989
+ return null;
990
+ }
991
+ function nextUatAttempt(basePath, milestoneId, sliceId) {
992
+ const contract = resolveGsdPathContract(basePath);
993
+ const dir = join(contract.projectGsd, "uat", milestoneId, sliceId);
994
+ if (!existsSync(dir))
995
+ return 1;
996
+ let max = 0;
997
+ for (const entry of readdirSync(dir)) {
998
+ const match = /^attempt-(\d+)\.json$/.exec(entry);
999
+ if (match)
1000
+ max = Math.max(max, Number(match[1]));
1001
+ }
1002
+ return max + 1;
1003
+ }
1004
+ function escapeMarkdownTableCell(value) {
1005
+ return String(value ?? "")
1006
+ .replace(/[\\|]/g, (char) => `\\${char}`)
1007
+ .replace(/\r?\n/g, "<br>");
1008
+ }
1009
+ function renderUatAssessment(params, attempt, gateVerdict) {
1010
+ const lines = [
1011
+ "---",
1012
+ `sliceId: ${params.sliceId}`,
1013
+ `uatType: ${params.uatType}`,
1014
+ `verdict: ${params.verdict}`,
1015
+ `attempt: ${attempt}`,
1016
+ `date: ${new Date().toISOString()}`,
1017
+ "---",
1018
+ "",
1019
+ `# UAT Result - ${params.sliceId}`,
1020
+ "",
1021
+ "## Checks",
1022
+ "",
1023
+ "| Check | Mode | Result | Evidence | Notes |",
1024
+ "|-------|------|--------|----------|-------|",
1025
+ ...params.checks.map((check) => {
1026
+ const evidence = (check.evidence ?? []).map((entry) => `${entry.kind}:${entry.ref}`).join("<br>") || "-";
1027
+ return `| ${escapeMarkdownTableCell(check.description)} | ${escapeMarkdownTableCell(check.mode)} | ${escapeMarkdownTableCell(check.result)} | ${escapeMarkdownTableCell(evidence)} | ${escapeMarkdownTableCell(check.notes)} |`;
1028
+ }),
1029
+ "",
1030
+ "## Overall Verdict",
1031
+ "",
1032
+ `${params.verdict} - ${params.notes ?? "UAT result saved."}`,
1033
+ "",
1034
+ "## Tool Presentation",
1035
+ "",
1036
+ "```json",
1037
+ JSON.stringify(params.presentation, null, 2),
1038
+ "```",
1039
+ "",
1040
+ "## Gate",
1041
+ "",
1042
+ `Aggregate UAT gate saved as ${gateVerdict}.`,
1043
+ ];
1044
+ return `${lines.join("\n")}\n`;
1045
+ }
1046
+ async function saveUatAttemptArtifact(basePath, params, attempt) {
1047
+ const contract = resolveGsdPathContract(basePath);
1048
+ const relativePath = `uat/${params.milestoneId}/${params.sliceId}/attempt-${attempt}.json`;
1049
+ await saveFile(join(contract.projectGsd, relativePath), `${JSON.stringify({ ...params, attempt }, null, 2)}\n`);
1050
+ return relativePath;
1051
+ }
1052
+ export async function executeUatResultSave(params, basePath = process.cwd()) {
1053
+ const dbAvailable = await ensureDbOpen(basePath);
1054
+ if (!dbAvailable)
1055
+ return errorResult("save_uat_result", "GSD database is not available.", "db_unavailable");
1056
+ const requiredError = ensureUatRequiredFields(params);
1057
+ if (requiredError)
1058
+ return errorResult("save_uat_result", requiredError, "invalid_params");
1059
+ const presentationError = validateCanonicalPresentation(params);
1060
+ if (presentationError)
1061
+ return errorResult("save_uat_result", presentationError, "alias_tool_name");
1062
+ const checkError = validateUatChecks(basePath, params);
1063
+ if (checkError)
1064
+ return errorResult("save_uat_result", checkError, "invalid_evidence");
1065
+ const modeError = validateUatMode(params);
1066
+ if (modeError)
1067
+ return errorResult("save_uat_result", modeError, "uat_mode_mismatch");
1068
+ try {
1069
+ const attempt = params.attempt === "auto" || params.attempt === undefined
1070
+ ? nextUatAttempt(basePath, params.milestoneId, params.sliceId)
1071
+ : typeof params.attempt === "string"
1072
+ ? Number.parseInt(params.attempt, 10)
1073
+ : params.attempt;
1074
+ if (!Number.isInteger(attempt) || attempt < 1) {
1075
+ return errorResult("save_uat_result", "attempt must be a positive integer or auto", "invalid_attempt");
1076
+ }
1077
+ const gateVerdict = params.verdict === "PASS" ? "pass" : "flag";
1078
+ const rationale = params.notes ?? `UAT ${params.verdict} for ${params.sliceId}.`;
1079
+ const assessment = renderUatAssessment(params, attempt, gateVerdict);
1080
+ const summary = await executeSummarySave({
1081
+ milestone_id: params.milestoneId,
1082
+ slice_id: params.sliceId,
1083
+ artifact_type: "ASSESSMENT",
1084
+ content: assessment,
1085
+ }, basePath);
1086
+ if (summary.isError)
1087
+ return summary;
1088
+ const attemptPath = await saveUatAttemptArtifact(basePath, params, attempt);
1089
+ const evaluatedAt = new Date().toISOString();
1090
+ upsertQualityGate({
1091
+ milestoneId: params.milestoneId,
1092
+ sliceId: params.sliceId,
1093
+ gateId: "UAT",
1094
+ scope: "slice",
1095
+ taskId: "",
1096
+ status: "complete",
1097
+ verdict: gateVerdict,
1098
+ rationale,
1099
+ findings: assessment,
1100
+ evaluatedAt,
1101
+ });
1102
+ insertGateRun({
1103
+ traceId: `uat:${params.milestoneId}:${params.sliceId}`,
1104
+ turnId: `uat:${params.sliceId}:attempt-${attempt}`,
1105
+ gateId: "UAT",
1106
+ gateType: "uat",
1107
+ unitType: "run-uat",
1108
+ unitId: `run-uat:${params.milestoneId}/${params.sliceId}`,
1109
+ milestoneId: params.milestoneId,
1110
+ sliceId: params.sliceId,
1111
+ outcome: params.verdict === "PASS" ? "pass" : "fail",
1112
+ failureClass: params.verdict === "PASS" ? "none" : "verification",
1113
+ rationale,
1114
+ findings: assessment,
1115
+ attempt,
1116
+ maxAttempts: attempt,
1117
+ retryable: params.verdict !== "PASS",
1118
+ evaluatedAt,
1119
+ });
1120
+ invalidateStateCache();
1121
+ return {
1122
+ content: [{ type: "text", text: `UAT result saved for ${params.milestoneId}/${params.sliceId}: ${params.verdict}` }],
1123
+ details: {
1124
+ operation: "save_uat_result",
1125
+ milestoneId: params.milestoneId,
1126
+ sliceId: params.sliceId,
1127
+ verdict: params.verdict,
1128
+ gateVerdict,
1129
+ attempt,
1130
+ attemptPath,
1131
+ recommendedNextUnit: params.verdict === "PASS" ? null : "reactive-execute",
1132
+ },
1133
+ };
1134
+ }
1135
+ catch (err) {
1136
+ const msg = err instanceof Error ? err.message : String(err);
1137
+ logError("tool", `gsd_uat_result_save failed: ${msg}`, { tool: "gsd_uat_result_save", error: String(err) });
1138
+ return errorResult("save_uat_result", `saving UAT result failed: ${msg}`, msg);
1139
+ }
1140
+ }
778
1141
  export async function executePlanMilestone(params, basePath = process.cwd()) {
779
1142
  const dbAvailable = await ensureDbOpen(basePath);
780
1143
  if (!dbAvailable) {
@@ -83,6 +83,10 @@ const COMMON_BUDGET_SMALL = 250_000; // ~65K tokens
83
83
  const TOOLS_ALL = { mode: "all" };
84
84
  const TOOLS_PLANNING = { mode: "planning" };
85
85
  const TOOLS_VERIFICATION = { mode: "verification" };
86
+ const TOOLS_VERIFICATION_DISPATCH_UAT = {
87
+ mode: "verification",
88
+ allowedSubagents: ["mnemo", "scout", "reviewer", "tester"],
89
+ };
86
90
  // Like TOOLS_PLANNING but permits dispatch to read-only recon/planning
87
91
  // specialists. Runtime-enforced by write-gate.ts before the subagent tool runs.
88
92
  const TOOLS_PLANNING_DISPATCH_RECON = {
@@ -372,7 +376,7 @@ export const UNIT_MANIFESTS = {
372
376
  codebaseMap: false,
373
377
  preferences: "active-only",
374
378
  contextMode: "verification",
375
- tools: TOOLS_VERIFICATION,
379
+ tools: TOOLS_VERIFICATION_DISPATCH_UAT,
376
380
  artifacts: {
377
381
  inline: ["slice-uat"],
378
382
  excerpt: ["slice-summary"],
@@ -551,9 +555,10 @@ export function compileSubagentPermissionContract(policy) {
551
555
  if (policy.mode === "all") {
552
556
  return { allowed: true, allowedSubagents: ["*"], toolsMode: policy.mode };
553
557
  }
554
- if (policy.mode === "planning-dispatch") {
558
+ if ((policy.mode === "planning-dispatch" || policy.mode === "verification") &&
559
+ Array.isArray(policy.allowedSubagents)) {
555
560
  return {
556
- allowed: true,
561
+ allowed: policy.allowedSubagents.length > 0,
557
562
  allowedSubagents: [...policy.allowedSubagents],
558
563
  toolsMode: policy.mode,
559
564
  };
@@ -8,6 +8,8 @@ import { deriveState } from "./state.js";
8
8
  import { detectWorktreeName } from "./worktree.js";
9
9
  const VALIDATION_BLOCK_RE = /milestone validation returned needs-(?:attention|remediation)|validation verdict is needs-(?:attention|remediation)/i;
10
10
  const VALIDATION_SAFE_DISPATCH_COMMANDS = new Set([
11
+ "reassess",
12
+ "reassess-roadmap",
11
13
  "validate",
12
14
  "validate-milestone",
13
15
  ]);
@@ -33,6 +33,8 @@ export function shouldAutoPrepareWorkflowMcp(ctx) {
33
33
  const authMode = getAuthModeSafe(ctx, provider);
34
34
  if (provider !== "claude-code")
35
35
  return false;
36
+ if (authMode === undefined)
37
+ return true;
36
38
  return usesWorkflowMcpTransport(authMode, baseUrl) || authMode === "externalCli";
37
39
  }
38
40
  export function prepareWorkflowMcpForProject(ctx, projectRoot, modelOverride) {
@@ -42,7 +44,7 @@ export function prepareWorkflowMcpForProject(ctx, projectRoot, modelOverride) {
42
44
  try {
43
45
  const result = ensureProjectWorkflowMcpConfig(projectRoot);
44
46
  if (result.status !== "unchanged") {
45
- prepCtx.ui?.notify?.(`Claude Code MCP prepared at ${result.configPath}`, "info");
47
+ prepCtx.ui?.notify?.(`GSD MCP Server Prepared at ${result.configPath}`, "info");
46
48
  }
47
49
  return result;
48
50
  }
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
2
2
  import { existsSync, realpathSync } from "node:fs";
3
3
  import { dirname, resolve, sep } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "./tool-presentation-plan.js";
5
6
  /** Session cwd may be a milestone worktree; MCP config and server discovery use the project root. */
6
7
  export function resolveWorkflowMcpProjectRoot(sessionCwd) {
7
8
  let resolved;
@@ -77,6 +78,8 @@ const MCP_WORKFLOW_TOOL_SURFACE = new Set([
77
78
  "gsd_task_complete",
78
79
  "gsd_task_reopen",
79
80
  "gsd_update_requirement",
81
+ "gsd_uat_exec",
82
+ "gsd_uat_result_save",
80
83
  "gsd_validate_milestone",
81
84
  ]);
82
85
  /** Workflow MCP tools are validated by transport compatibility, not pi tool-compat profiles. */
@@ -397,8 +400,9 @@ export function getRequiredWorkflowToolsForAutoUnit(unitType) {
397
400
  ];
398
401
  case "research-milestone":
399
402
  case "research-slice":
400
- case "run-uat":
401
403
  return ["gsd_summary_save"];
404
+ case "run-uat":
405
+ return [...RUN_UAT_WORKFLOW_TOOL_NAMES];
402
406
  case "plan-milestone":
403
407
  return ["gsd_plan_milestone"];
404
408
  case "plan-slice":