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

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 (219) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  4. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  5. package/dist/resources/extensions/browser-tools/index.js +29 -2
  6. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +45 -3
  8. package/dist/resources/extensions/gsd/auto/session.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  13. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  14. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  15. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  16. package/dist/resources/extensions/gsd/auto.js +26 -4
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  21. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  23. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  24. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  25. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  26. package/dist/resources/extensions/gsd/guided-flow.js +93 -108
  27. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  28. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  29. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  30. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
  33. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  34. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  35. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  36. package/dist/resources/extensions/gsd/tool-contract.js +6 -1
  37. package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
  38. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  39. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
  40. package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
  42. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  43. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  73. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/package.json +1 -1
  81. package/packages/daemon/package.json +4 -4
  82. package/packages/gsd-agent-core/package.json +5 -5
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  86. package/packages/gsd-agent-modes/package.json +7 -7
  87. package/packages/mcp-server/package.json +3 -3
  88. package/packages/native/package.json +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -3
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  92. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  93. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  94. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  95. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  97. package/packages/pi-agent-core/dist/types.d.ts +3 -1
  98. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  99. package/packages/pi-agent-core/dist/types.js.map +1 -1
  100. package/packages/pi-agent-core/package.json +1 -1
  101. package/packages/pi-ai/dist/models.generated.d.ts +157 -18
  102. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  103. package/packages/pi-ai/dist/models.generated.js +159 -36
  104. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  105. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  107. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  108. package/packages/pi-ai/package.json +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  110. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  113. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  116. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  118. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  120. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  123. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  124. package/packages/pi-coding-agent/package.json +7 -7
  125. package/packages/pi-tui/package.json +1 -1
  126. package/packages/rpc-client/package.json +2 -2
  127. package/pkg/package.json +1 -1
  128. package/scripts/install/handoff.js +16 -3
  129. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  130. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  131. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  132. package/src/resources/extensions/browser-tools/index.ts +36 -5
  133. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  134. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  135. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  136. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  137. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  138. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  139. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
  140. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  141. package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  143. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  144. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  145. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  146. package/src/resources/extensions/gsd/auto.ts +28 -4
  147. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
  148. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
  149. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  150. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  151. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  152. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  153. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  154. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  155. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  156. package/src/resources/extensions/gsd/guided-flow.ts +128 -135
  157. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  158. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  159. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  160. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  161. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  162. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
  164. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  165. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  166. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  167. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  168. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  169. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  170. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  171. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  172. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  173. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  174. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  175. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  176. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  177. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  178. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  179. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  181. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  182. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  183. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  184. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  185. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  186. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  187. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +7 -1
  188. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  189. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  190. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  191. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  193. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  194. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  195. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  196. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
  197. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  198. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  199. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  200. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +56 -0
  201. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  202. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
  205. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  206. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tool-contract.ts +7 -1
  208. package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
  209. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  210. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
  211. package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
  212. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
  213. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  214. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  215. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  217. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  218. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  219. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * worktree-reentry.test.ts — Unit tests for reenterActiveWorktreeIfNeeded.
3
+ *
4
+ * Covers the cold-start (/quit + relaunch) path where cwd lands at the project
5
+ * root instead of the active milestone's worktree. The helper should chdir back
6
+ * into the worktree deterministically, and no-op when it shouldn't act.
7
+ */
8
+
9
+ import { describe, test, beforeEach } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+ import { execFileSync } from "node:child_process";
15
+
16
+ import { createAutoWorktree, _resetAutoWorktreeOriginalBaseForTests } from "../auto-worktree.ts";
17
+ import { reenterActiveWorktreeIfNeeded } from "../worktree-reentry.ts";
18
+
19
+ // Safe: all inputs below are hardcoded test strings, not user input.
20
+ function git(subArgs: string[], cwd: string): void {
21
+ execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
22
+ }
23
+
24
+ function createTempRepo(
25
+ t: { after: (fn: () => void) => void },
26
+ opts: { isolation?: "worktree" | "none" } = {},
27
+ ): string {
28
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-reentry-")));
29
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
30
+ git(["init"], dir);
31
+ git(["config", "user.email", "test@test.com"], dir);
32
+ git(["config", "user.name", "Test"], dir);
33
+ writeFileSync(join(dir, "README.md"), "# test\n");
34
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
35
+ if (opts.isolation === "worktree") {
36
+ writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\ngit:\n isolation: worktree\n---\n", "utf-8");
37
+ }
38
+ const msDir = join(dir, ".gsd", "milestones", "M001");
39
+ mkdirSync(msDir, { recursive: true });
40
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
41
+ git(["add", "."], dir);
42
+ git(["commit", "-m", "init"], dir);
43
+ git(["branch", "-M", "main"], dir);
44
+ return dir;
45
+ }
46
+
47
+ describe("reenterActiveWorktreeIfNeeded", () => {
48
+ const savedCwd = process.cwd();
49
+
50
+ beforeEach(() => {
51
+ _resetAutoWorktreeOriginalBaseForTests();
52
+ process.chdir(savedCwd);
53
+ });
54
+
55
+ test("re-enters the sole live worktree when sitting at the project root", async (t) => {
56
+ const dir = createTempRepo(t, { isolation: "worktree" });
57
+ t.after(() => process.chdir(savedCwd));
58
+
59
+ // createAutoWorktree chdir's INTO the worktree; simulate a cold start by
60
+ // returning to the project root with a clean workspace registry.
61
+ createAutoWorktree(dir, "M001");
62
+ process.chdir(dir);
63
+ _resetAutoWorktreeOriginalBaseForTests();
64
+
65
+ const entered = await reenterActiveWorktreeIfNeeded(dir);
66
+ assert.ok(entered, "re-entry returned a worktree path");
67
+ assert.strictEqual(realpathSync(process.cwd()), realpathSync(entered!), "cwd moved into the worktree");
68
+ assert.strictEqual(entered, join(dir, ".gsd", "worktrees", "M001"));
69
+ });
70
+
71
+ test("no-op when already inside a worktree", async (t) => {
72
+ const dir = createTempRepo(t, { isolation: "worktree" });
73
+ t.after(() => process.chdir(savedCwd));
74
+
75
+ createAutoWorktree(dir, "M001"); // leaves cwd inside the worktree
76
+ const cwdBefore = process.cwd();
77
+
78
+ const entered = await reenterActiveWorktreeIfNeeded(dir);
79
+ assert.strictEqual(entered, null, "no re-entry when already in a worktree");
80
+ assert.strictEqual(process.cwd(), cwdBefore, "cwd unchanged");
81
+ });
82
+
83
+ test("no-op when isolation is not worktree", async (t) => {
84
+ const dir = createTempRepo(t, { isolation: "none" });
85
+ t.after(() => process.chdir(savedCwd));
86
+ process.chdir(dir);
87
+
88
+ const entered = await reenterActiveWorktreeIfNeeded(dir);
89
+ assert.strictEqual(entered, null, "isolation=none never re-enters");
90
+ assert.strictEqual(realpathSync(process.cwd()), realpathSync(dir), "cwd stays at project root");
91
+ });
92
+
93
+ test("no-op when there are no worktrees", async (t) => {
94
+ const dir = createTempRepo(t, { isolation: "worktree" });
95
+ t.after(() => process.chdir(savedCwd));
96
+ process.chdir(dir);
97
+
98
+ const entered = await reenterActiveWorktreeIfNeeded(dir);
99
+ assert.strictEqual(entered, null, "nothing to re-enter");
100
+ assert.strictEqual(realpathSync(process.cwd()), realpathSync(dir), "cwd stays at project root");
101
+ });
102
+ });
@@ -9,6 +9,7 @@ import test from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import { join, sep } from 'node:path';
11
11
 
12
+ import { GSD_PHASE_SCOPE_DISPLAY_REASON } from '../auto-unit-tool-scope.ts';
12
13
  import { ALLOWED_PLANNING_DISPATCH_AGENTS, shouldBlockPlanningUnit } from '../bootstrap/write-gate.ts';
13
14
  import { extractSubagentAgentClasses } from '../bootstrap/subagent-input.ts';
14
15
  import { isDeterministicPolicyError } from '../auto-tool-tracking.ts';
@@ -65,6 +66,19 @@ test('planning-unit: deterministic block reason is suitable for retry short-circ
65
66
  assert.strictEqual(isDeterministicPolicyError(r.reason!), true);
66
67
  });
67
68
 
69
+ test('planning-unit: blocked tool-policy calls include UI-safe display reason', () => {
70
+ const r = shouldBlockPlanningUnit(
71
+ 'edit',
72
+ 'src/main.ts',
73
+ BASE,
74
+ 'discuss-milestone',
75
+ PLANNING,
76
+ );
77
+ assert.strictEqual(r.block, true);
78
+ assert.match(r.reason!, /HARD BLOCK/);
79
+ assert.strictEqual(r.displayReason, GSD_PHASE_SCOPE_DISPLAY_REASON);
80
+ });
81
+
68
82
  test('planning-unit: blocks write to user source via relative path', () => {
69
83
  const r = shouldBlockPlanningUnit('write', 'src/main.ts', BASE, 'plan-milestone', PLANNING);
70
84
  assert.strictEqual(r.block, true);
@@ -367,6 +381,7 @@ test('auto-unit scope: execute-task allows only its task completion lifecycle to
367
381
  assert.strictEqual(blocked.block, true);
368
382
  assert.match(blocked.reason!, /HARD BLOCK/);
369
383
  assert.match(blocked.reason!, /gsd_save_gate_result/);
384
+ assert.strictEqual(blocked.displayReason, GSD_PHASE_SCOPE_DISPLAY_REASON);
370
385
  assert.strictEqual(isDeterministicPolicyError(blocked.reason!), true);
371
386
  });
372
387
 
@@ -8,12 +8,14 @@ import {
8
8
  type ToolsPolicy,
9
9
  } from "./unit-context-manifest.js";
10
10
  import { getRequiredWorkflowToolsForAutoUnit } from "./workflow-mcp.js";
11
+ import { getUnitToolSurfaceContract } from "./unit-tool-contracts.js";
11
12
 
12
13
  export interface UnitToolContract {
13
14
  unitType: string;
14
15
  contextMode: ContextModePolicy;
15
16
  toolsPolicy: ToolsPolicy;
16
17
  requiredWorkflowTools: readonly string[];
18
+ forbiddenWorkflowTools: readonly { name: string; reason: string }[];
17
19
  promptObligations: readonly string[];
18
20
  validationRules: readonly string[];
19
21
  closeoutTools: readonly string[];
@@ -30,6 +32,7 @@ export type ToolContractResult =
30
32
 
31
33
  export function compileUnitToolContract(unitType: string): ToolContractResult {
32
34
  const manifest = resolveManifest(unitType);
35
+ const surfaceContract = getUnitToolSurfaceContract(unitType);
33
36
  if (!manifest) {
34
37
  return {
35
38
  ok: false,
@@ -39,8 +42,10 @@ export function compileUnitToolContract(unitType: string): ToolContractResult {
39
42
  }
40
43
 
41
44
  const requiredWorkflowTools = getRequiredWorkflowToolsForAutoUnit(unitType);
45
+ const forbiddenWorkflowTools = Object.entries(surfaceContract?.forbiddenGsdTools ?? {})
46
+ .map(([name, reason]) => ({ name, reason }));
42
47
  const closeoutTools = requiredWorkflowTools.filter((tool) =>
43
- /^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool),
48
+ /^gsd_(?:task|slice|milestone|complete|validate|save|summary|uat)/.test(tool),
44
49
  );
45
50
 
46
51
  if (requiresCloseoutTool(unitType) && closeoutTools.length === 0) {
@@ -58,6 +63,7 @@ export function compileUnitToolContract(unitType: string): ToolContractResult {
58
63
  contextMode: manifest.contextMode,
59
64
  toolsPolicy: manifest.tools,
60
65
  requiredWorkflowTools,
66
+ forbiddenWorkflowTools,
61
67
  promptObligations: [
62
68
  `context-mode:${manifest.contextMode}`,
63
69
  `tools-policy:${manifest.tools.mode}`,
@@ -1,6 +1,20 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Resolve phase-aware tool surfaces for GSD model presentations.
3
3
 
4
+ import {
5
+ RUN_UAT_BROWSER_TOOL_NAMES,
6
+ RUN_UAT_READ_ONLY_TOOL_NAMES,
7
+ RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
8
+ RUN_UAT_WORKFLOW_TOOL_NAMES,
9
+ } from "./unit-tool-contracts.js";
10
+
11
+ export {
12
+ RUN_UAT_BROWSER_TOOL_NAMES,
13
+ RUN_UAT_READ_ONLY_TOOL_NAMES,
14
+ RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
15
+ RUN_UAT_WORKFLOW_TOOL_NAMES,
16
+ } from "./unit-tool-contracts.js";
17
+
4
18
  export type ToolPresentationSurface = "provider-tools" | "claude-code-sdk" | "mcp" | "hybrid";
5
19
 
6
20
  export interface ToolPresentationModel {
@@ -20,13 +34,12 @@ export interface ToolPresentationPlan {
20
34
  diagnostics: string[];
21
35
  }
22
36
 
23
- export const RUN_UAT_WORKFLOW_TOOL_NAMES = [
24
- "gsd_uat_exec",
25
- "gsd_uat_result_save",
26
- "gsd_resume",
27
- "gsd_milestone_status",
28
- "gsd_journal_query",
29
- ] as const;
37
+ export interface RunUatResultPresentation {
38
+ surface: ToolPresentationSurface;
39
+ presentedTools: string[];
40
+ blockedTools: Array<{ name: string; reason: string }>;
41
+ toolPresentationPlanId: string;
42
+ }
30
43
 
31
44
  export const RUN_UAT_FORBIDDEN_TOOL_NAMES = [
32
45
  "edit",
@@ -105,10 +118,72 @@ function addBlockedTool(
105
118
  export function buildRunUatCanonicalToolNames(options: { includeBrowserTools?: readonly string[] } = {}): string[] {
106
119
  return dedupe([
107
120
  ...RUN_UAT_WORKFLOW_TOOL_NAMES,
121
+ ...RUN_UAT_READ_ONLY_TOOL_NAMES,
108
122
  ...(options.includeBrowserTools ?? []),
109
123
  ]);
110
124
  }
111
125
 
126
+ // UAT modes whose run-uat instructions direct the runner to exercise the live
127
+ // app in a browser. These modes receive the browser tool surface so the runner
128
+ // can actually drive the page instead of silently deferring browser checks to a
129
+ // human. See run-uat.md automation rules: `browser-executable`, `live-runtime`,
130
+ // and `mixed` are all told to drive a browser/runtime path, and
131
+ // `human-experience` is told to capture screenshots. Without this, a webpage
132
+ // UAT classified as anything but `browser-executable` had no browser tools and
133
+ // downgraded its live checks to NEEDS-HUMAN (M001/S03 regression).
134
+ export const BROWSER_INCLUSIVE_UAT_TYPES: readonly string[] = [
135
+ "browser-executable",
136
+ "live-runtime",
137
+ "mixed",
138
+ "human-experience",
139
+ ];
140
+
141
+ function uatTypeIncludesBrowser(uatType: string | undefined): boolean {
142
+ return uatType !== undefined && BROWSER_INCLUSIVE_UAT_TYPES.includes(uatType);
143
+ }
144
+
145
+ export function runUatBrowserToolsForType(uatType: string | undefined): readonly string[] {
146
+ return uatTypeIncludesBrowser(uatType) ? RUN_UAT_BROWSER_TOOL_NAMES : [];
147
+ }
148
+
149
+ export function runUatPresentationSurfaceForType(uatType: string | undefined): ToolPresentationSurface {
150
+ return uatTypeIncludesBrowser(uatType) ? "hybrid" : "mcp";
151
+ }
152
+
153
+ export function buildRunUatPresentationForType(
154
+ uatType: string | undefined,
155
+ options: {
156
+ surface?: ToolPresentationSurface;
157
+ presentedTools?: readonly string[];
158
+ } = {},
159
+ ): RunUatResultPresentation {
160
+ return buildRunUatResultPresentation({
161
+ ...options,
162
+ surface: options.surface ?? runUatPresentationSurfaceForType(uatType),
163
+ includeBrowserTools: runUatBrowserToolsForType(uatType),
164
+ });
165
+ }
166
+
167
+ export function buildRunUatResultPresentation(options: {
168
+ surface?: ToolPresentationSurface;
169
+ includeBrowserTools?: readonly string[];
170
+ presentedTools?: readonly string[];
171
+ } = {}): RunUatResultPresentation {
172
+ const presentedTools = options.presentedTools
173
+ ? dedupe(options.presentedTools)
174
+ : buildRunUatCanonicalToolNames({ includeBrowserTools: options.includeBrowserTools });
175
+ const blockedTools = RUN_UAT_FORBIDDEN_TOOL_NAMES
176
+ .filter((toolName) => !toolName.includes("*"))
177
+ .map((name) => ({ name, reason: "forbidden during run-uat" }));
178
+
179
+ return {
180
+ surface: options.surface ?? "mcp",
181
+ presentedTools,
182
+ blockedTools,
183
+ toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
184
+ };
185
+ }
186
+
112
187
  export function resolveToolPresentationPlan(options: {
113
188
  phase: string;
114
189
  surface: ToolPresentationSurface;
@@ -34,7 +34,8 @@ import { getGatesForTurn } from "../gate-registry.js";
34
34
  import { gsdProjectionRoot, clearPathCache, resolveMilestoneFile } from "../paths.js";
35
35
  import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
36
36
  import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
37
- import { saveFile, clearParseCache } from "../files.js";
37
+ import { saveFile, clearParseCache, extractUatType } from "../files.js";
38
+ import { hasBrowserRequiredText } from "../browser-evidence.js";
38
39
  import { invalidateStateCache } from "../state.js";
39
40
  import { renderRoadmapFromDb } from "../markdown-renderer.js";
40
41
  import { parseRoadmap } from "../parsers-legacy.js";
@@ -342,6 +343,33 @@ export async function handleCompleteSlice(
342
343
  return { error: `slice verification indicates blocked/failed state — do not complete a slice that has not passed verification. Address the blockers and re-verify first.` };
343
344
  }
344
345
 
346
+ // ── Browser/web UAT classification gate ────────────────────────────────
347
+ // A UAT that drives a running web UI (opening a page in a browser,
348
+ // navigating to a page/localhost) must declare a browser-capable mode so the
349
+ // run-uat runner surfaces browser tools and actually launches a browser.
350
+ // Otherwise the browser checks get silently deferred to a human and the slice
351
+ // passes on static checks alone (M001/S03 regression). `browser-executable`,
352
+ // `live-runtime`, and `mixed` all receive browser tools (see
353
+ // BROWSER_INCLUSIVE_UAT_TYPES); only the non-browser modes are rejected here.
354
+ //
355
+ // Reuse the canonical hasBrowserRequiredText detector (also used by dispatch
356
+ // and milestone validation): it skips Not-Proven/Out-of-Scope disclaimer
357
+ // sections and only treats verbs like navigate/open as web when they sit next
358
+ // to browser/page/localhost — avoiding false positives on CLI/file/API steps.
359
+ //
360
+ // Only `artifact-driven` is gated. It is the one mode that performs no
361
+ // execution at all (static/file checks), so a browser-requiring UAT under it
362
+ // genuinely defers verification to a human. Every other mode has a real
363
+ // verification path: `runtime-executable` runs browser test commands like
364
+ // `npx playwright test` via gsd_uat_exec, and live-runtime/mixed/
365
+ // browser-executable receive browser tools (BROWSER_INCLUSIVE_UAT_TYPES).
366
+ const declaredUatMode = extractUatType(params.uatContent || "") ?? "artifact-driven";
367
+ if (declaredUatMode === "artifact-driven" && hasBrowserRequiredText(params.uatContent || "")) {
368
+ return {
369
+ error: `UAT requires browser verification (opening a page in a browser, navigating to a page or localhost, screenshots) but declares "UAT mode: artifact-driven", which only runs static/file checks and would defer the browser work to a human. Use a mode that actually verifies the UI: "browser-executable" (interactive browser tools), "runtime-executable" (a browser test command such as playwright), or a browser-inclusive "mixed"/"live-runtime". Re-author the UAT Type section and complete the slice again.`,
370
+ };
371
+ }
372
+
345
373
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
346
374
  const completedAt = new Date().toISOString();
347
375
  let guardError: string | null = null;
@@ -17,8 +17,9 @@ import {
17
17
  } from "../gsd-db.js";
18
18
  import { GATE_REGISTRY } from "../gate-registry.js";
19
19
  import { generateRequirementsMd, saveArtifactToDb } from "../db-writer.js";
20
- import { clearPathCache, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
20
+ import { clearPathCache, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
21
21
  import { saveFile, clearParseCache } from "../files.js";
22
+ import { buildManualValidationGuidance, resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
22
23
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
23
24
  import { isAbsolute, join, resolve } from "node:path";
24
25
  import type { CompleteMilestoneParams } from "./complete-milestone.js";
@@ -48,9 +49,11 @@ import { loadEffectiveGSDPreferences } from "../preferences.js";
48
49
  import { parseProject } from "../schemas/parsers.js";
49
50
  import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
50
51
  import {
52
+ buildRunUatPresentationForType,
51
53
  canonicalWorkflowToolName,
52
54
  parseMcpToolName,
53
55
  RUN_UAT_FORBIDDEN_TOOL_NAMES,
56
+ RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
54
57
  RUN_UAT_WORKFLOW_TOOL_NAMES,
55
58
  } from "../tool-presentation-plan.js";
56
59
 
@@ -90,7 +93,7 @@ function blockIfWrongAutoUnit(requiredUnitType: string, operation: string): Tool
90
93
  if (!snapshot.active || !snapshot.currentUnit) return null;
91
94
  if (snapshot.currentUnit.type === requiredUnitType) return null;
92
95
 
93
- const error = `HARD BLOCK: ${operation} may only run from ${requiredUnitType}; active unit is ${snapshot.currentUnit.type}. The orchestrator owns phase transitions.`;
96
+ const error = `HARD BLOCK: Tool Contract failure: ${operation} may only run from ${requiredUnitType}; active unit is ${snapshot.currentUnit.type}. Fix unit-tool-contracts.ts or the active Unit prompt. The orchestrator owns phase transitions.`;
94
97
  return {
95
98
  content: [{ type: "text", text: error }],
96
99
  details: { operation, error },
@@ -178,7 +181,11 @@ export async function executeSummarySave(
178
181
  if (rootArtifactGuard.block) {
179
182
  return {
180
183
  content: [{ type: "text", text: `Error saving artifact: ${rootArtifactGuard.reason ?? "root artifact write blocked"}` }],
181
- details: { operation: "save_summary", error: "root_artifact_write_blocked" },
184
+ details: {
185
+ operation: "save_summary",
186
+ error: "root_artifact_write_blocked",
187
+ displayReason: "Approval confirmation required before saving final project setup artifacts.",
188
+ },
182
189
  isError: true,
183
190
  };
184
191
  }
@@ -191,9 +198,13 @@ export async function executeSummarySave(
191
198
  if (contextGuard.block) {
192
199
  return {
193
200
  content: [{ type: "text", text: `Error saving artifact: ${contextGuard.reason ?? "context write blocked"}` }],
194
- details: { operation: "save_summary", error: "context_write_blocked" },
195
- isError: true,
196
- };
201
+ details: {
202
+ operation: "save_summary",
203
+ error: "context_write_blocked",
204
+ displayReason: "Depth check required before writing milestone context.",
205
+ },
206
+ isError: true,
207
+ };
197
208
  }
198
209
  try {
199
210
  let relativePath: string;
@@ -441,6 +452,9 @@ export interface UatEvidenceRef {
441
452
  kind: "gsd_uat_exec" | "gsd_exec" | "screenshot" | "log" | "url" | "browser";
442
453
  ref: string;
443
454
  note?: string;
455
+ unitType?: string;
456
+ tool?: string;
457
+ executionId?: string;
444
458
  }
445
459
 
446
460
  export interface UatCheckResultInput {
@@ -1008,10 +1022,68 @@ function isNonEmptyString(value: unknown): value is string {
1008
1022
  return typeof value === "string" && value.trim().length > 0;
1009
1023
  }
1010
1024
 
1025
+ function mergeBlockedTools(
1026
+ current: UatPresentationInput["blockedTools"] | undefined,
1027
+ canonical: UatPresentationInput["blockedTools"],
1028
+ ): UatPresentationInput["blockedTools"] {
1029
+ const merged = new Map<string, { name: string; reason: string }>();
1030
+ for (const entry of [...(current ?? []), ...canonical]) {
1031
+ merged.set(canonicalWorkflowToolName(parseMcpToolName(entry.name)?.tool ?? entry.name), entry);
1032
+ }
1033
+ return [...merged.values()];
1034
+ }
1035
+
1036
+ function mergePresentedTools(current: readonly string[] | undefined, canonical: readonly string[]): string[] {
1037
+ return [...new Set([...(current ?? []), ...canonical])];
1038
+ }
1039
+
1040
+ function normalizeUatVerdict(params: UatResultSaveParams): UatResultSaveParams {
1041
+ const raw = params as Partial<UatResultSaveParams> & Record<string, unknown>;
1042
+ if (typeof raw.verdict === "string") {
1043
+ return { ...params, verdict: raw.verdict.toUpperCase() as UatVerdict };
1044
+ }
1045
+ return params;
1046
+ }
1047
+
1048
+ function supplyDefaultPresentation(params: UatResultSaveParams): UatResultSaveParams {
1049
+ const raw = params as Partial<UatResultSaveParams> & Record<string, unknown>;
1050
+ if (!raw.presentation) {
1051
+ return { ...params, presentation: buildRunUatPresentationForType(params.uatType) };
1052
+ }
1053
+ return params;
1054
+ }
1055
+
1056
+ function mergeCanonicalPresentation(params: UatResultSaveParams): UatResultSaveParams {
1057
+ const canonicalPresentation = buildRunUatPresentationForType(params.uatType);
1058
+ const providedPresentation = params.presentation as Partial<UatPresentationInput>;
1059
+ return {
1060
+ ...params,
1061
+ presentation: {
1062
+ ...providedPresentation,
1063
+ surface: providedPresentation.surface ?? canonicalPresentation.surface,
1064
+ presentedTools: mergePresentedTools(providedPresentation.presentedTools, canonicalPresentation.presentedTools),
1065
+ blockedTools: mergeBlockedTools(providedPresentation.blockedTools, canonicalPresentation.blockedTools),
1066
+ toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
1067
+ } as UatPresentationInput,
1068
+ };
1069
+ }
1070
+
1071
+ const VALID_UAT_TYPES: readonly UatType[] = [
1072
+ "artifact-driven",
1073
+ "browser-executable",
1074
+ "runtime-executable",
1075
+ "live-runtime",
1076
+ "mixed",
1077
+ "human-experience",
1078
+ ];
1079
+
1011
1080
  function ensureUatRequiredFields(params: UatResultSaveParams): string | null {
1012
1081
  if (!isNonEmptyString(params.milestoneId)) return "milestoneId is required";
1013
1082
  if (!isNonEmptyString(params.sliceId)) return "sliceId is required";
1014
1083
  if (!isNonEmptyString(params.uatType)) return "uatType is required";
1084
+ if (!(VALID_UAT_TYPES as readonly string[]).includes(params.uatType)) {
1085
+ return `uatType must be one of: ${VALID_UAT_TYPES.join(", ")}`;
1086
+ }
1015
1087
  if (!["PASS", "FAIL", "PARTIAL"].includes(params.verdict)) return "verdict must be PASS, FAIL, or PARTIAL";
1016
1088
  if (!Array.isArray(params.checks) || params.checks.length === 0) return "checks must contain at least one UAT check";
1017
1089
  if (!params.presentation || !Array.isArray(params.presentation.presentedTools)) return "presentation.presentedTools is required";
@@ -1147,6 +1219,15 @@ function validateUatChecks(basePath: string, params: UatResultSaveParams): strin
1147
1219
  return null;
1148
1220
  }
1149
1221
 
1222
+ function validateFreshUatOwnedEvidence(params: UatResultSaveParams): string | null {
1223
+ const hasFreshUatEvidence = params.checks.some((check) =>
1224
+ (check.evidence ?? []).some((evidence) => evidence.kind === "gsd_uat_exec")
1225
+ );
1226
+ return hasFreshUatEvidence
1227
+ ? null
1228
+ : "UAT Assessment requires at least one fresh gsd_uat_exec evidence reference from run-uat";
1229
+ }
1230
+
1150
1231
  function validateUatMode(params: UatResultSaveParams): string | null {
1151
1232
  const modes = new Set(params.checks.map((check) => check.mode));
1152
1233
  const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
@@ -1257,7 +1338,12 @@ function escapeMarkdownTableCell(value: unknown): string {
1257
1338
  .replace(/\r?\n/g, "<br>");
1258
1339
  }
1259
1340
 
1260
- function renderUatAssessment(params: UatResultSaveParams, attempt: number, gateVerdict: "pass" | "flag"): string {
1341
+ function renderUatAssessment(
1342
+ params: UatResultSaveParams,
1343
+ attempt: number,
1344
+ gateVerdict: "pass" | "flag",
1345
+ basePath: string,
1346
+ ): string {
1261
1347
  const lines = [
1262
1348
  "---",
1263
1349
  `sliceId: ${params.sliceId}`,
@@ -1292,6 +1378,27 @@ function renderUatAssessment(params: UatResultSaveParams, attempt: number, gateV
1292
1378
  "",
1293
1379
  `Aggregate UAT gate saved as ${gateVerdict}.`,
1294
1380
  ];
1381
+
1382
+ // When any check still needs a human, point them at the exact checkout to
1383
+ // validate — critical for worktree milestones whose code sits under a hidden
1384
+ // `.gsd/worktrees/` path the reviewer would otherwise have to hunt for.
1385
+ const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
1386
+ if (hasHuman) {
1387
+ const guidance = buildManualValidationGuidance(basePath, params.milestoneId, {
1388
+ uatPath: relSliceFile(basePath, params.milestoneId, params.sliceId, "UAT"),
1389
+ });
1390
+ if (guidance) {
1391
+ lines.push(
1392
+ "",
1393
+ "## Manual Validation",
1394
+ "",
1395
+ "One or more checks are marked `NEEDS-HUMAN` and require a person to validate:",
1396
+ "",
1397
+ ...guidance.split("\n").map((line) => `- ${line}`),
1398
+ );
1399
+ }
1400
+ }
1401
+
1295
1402
  return `${lines.join("\n")}\n`;
1296
1403
  }
1297
1404
 
@@ -1306,15 +1413,30 @@ export async function executeUatResultSave(
1306
1413
  params: UatResultSaveParams,
1307
1414
  basePath: string = process.cwd(),
1308
1415
  ): Promise<ToolExecutionResult> {
1416
+ const unitGuard = blockIfWrongAutoUnit("run-uat", "save_uat_result");
1417
+ if (unitGuard) return unitGuard;
1418
+
1419
+ // Phase 1: normalize verdict and supply the canonical presentation when none was provided.
1420
+ params = normalizeUatVerdict(params);
1421
+ params = supplyDefaultPresentation(params);
1422
+
1309
1423
  const dbAvailable = await ensureDbOpen(basePath);
1310
1424
  if (!dbAvailable) return errorResult("save_uat_result", "GSD database is not available.", "db_unavailable");
1311
1425
 
1426
+ // Phase 2: validate the submitted presentation before the canonical merge so that
1427
+ // presentations missing required workflow tools are rejected rather than silently patched.
1312
1428
  const requiredError = ensureUatRequiredFields(params);
1313
1429
  if (requiredError) return errorResult("save_uat_result", requiredError, "invalid_params");
1314
1430
  const presentationError = validateCanonicalPresentation(params);
1315
1431
  if (presentationError) return errorResult("save_uat_result", presentationError, "alias_tool_name");
1432
+
1433
+ // Phase 3: merge in the canonical plan ID and read-only audit tools so the persisted
1434
+ // artifact always carries the full audit surface even when the provider omitted them.
1435
+ params = mergeCanonicalPresentation(params);
1316
1436
  const checkError = validateUatChecks(basePath, params);
1317
1437
  if (checkError) return errorResult("save_uat_result", checkError, "invalid_evidence");
1438
+ const freshEvidenceError = validateFreshUatOwnedEvidence(params);
1439
+ if (freshEvidenceError) return errorResult("save_uat_result", freshEvidenceError, "missing_fresh_uat_evidence");
1318
1440
  const modeError = validateUatMode(params);
1319
1441
  if (modeError) return errorResult("save_uat_result", modeError, "uat_mode_mismatch");
1320
1442
 
@@ -1329,7 +1451,7 @@ export async function executeUatResultSave(
1329
1451
  }
1330
1452
  const gateVerdict = params.verdict === "PASS" ? "pass" : "flag";
1331
1453
  const rationale = params.notes ?? `UAT ${params.verdict} for ${params.sliceId}.`;
1332
- const assessment = renderUatAssessment(params, attempt, gateVerdict);
1454
+ const assessment = renderUatAssessment(params, attempt, gateVerdict, basePath);
1333
1455
  const summary = await executeSummarySave(
1334
1456
  {
1335
1457
  milestone_id: params.milestoneId,
@@ -1373,8 +1495,20 @@ export async function executeUatResultSave(
1373
1495
  evaluatedAt,
1374
1496
  });
1375
1497
  invalidateStateCache();
1498
+ // Surface where to validate when checks are left for a human, so the path
1499
+ // (often a buried worktree checkout) reaches the reviewer, not just the file.
1500
+ const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
1501
+ const manualGuidance = hasHuman
1502
+ ? buildManualValidationGuidance(basePath, params.milestoneId, {
1503
+ uatPath: relSliceFile(basePath, params.milestoneId, params.sliceId, "UAT"),
1504
+ })
1505
+ : null;
1506
+ const savedText = `UAT result saved for ${params.milestoneId}/${params.sliceId}: ${params.verdict}`;
1376
1507
  return {
1377
- content: [{ type: "text", text: `UAT result saved for ${params.milestoneId}/${params.sliceId}: ${params.verdict}` }],
1508
+ content: [{
1509
+ type: "text",
1510
+ text: manualGuidance ? `${savedText}\n\nManual validation needed:\n${manualGuidance}` : savedText,
1511
+ }],
1378
1512
  details: {
1379
1513
  operation: "save_uat_result",
1380
1514
  milestoneId: params.milestoneId,
@@ -1384,6 +1518,9 @@ export async function executeUatResultSave(
1384
1518
  attempt,
1385
1519
  attemptPath,
1386
1520
  recommendedNextUnit: params.verdict === "PASS" ? null : "reactive-execute",
1521
+ ...(hasHuman
1522
+ ? { manualValidationPath: resolveCanonicalMilestoneRoot(basePath, params.milestoneId) }
1523
+ : {}),
1387
1524
  },
1388
1525
  };
1389
1526
  } catch (err) {