@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,63 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Regression tests for DB-backed closeout consistency before merge.
3
+
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import { execFileSync } from "node:child_process";
10
+
11
+ import { mergeMilestoneToMain } from "../auto-worktree.ts";
12
+ import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
13
+
14
+ function git(args: string[], cwd: string): string {
15
+ return execFileSync("git", args, {
16
+ cwd,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ encoding: "utf-8",
19
+ }).trim();
20
+ }
21
+
22
+ function createRepo(): string {
23
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "merge-closeout-gate-")));
24
+ git(["init"], dir);
25
+ git(["config", "user.email", "test@test.com"], dir);
26
+ git(["config", "user.name", "Test"], dir);
27
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
28
+ writeFileSync(join(dir, "README.md"), "# test\n");
29
+ git(["add", "."], dir);
30
+ git(["commit", "-m", "init"], dir);
31
+ git(["branch", "-M", "main"], dir);
32
+
33
+ git(["checkout", "-b", "milestone/M001"], dir);
34
+ writeFileSync(join(dir, "feature.ts"), "export const feature = true;\n");
35
+ git(["add", "feature.ts"], dir);
36
+ git(["commit", "-m", "feat: milestone work"], dir);
37
+ git(["checkout", "main"], dir);
38
+ return dir;
39
+ }
40
+
41
+ test("mergeMilestoneToMain blocks when project DB closeout is still open", () => {
42
+ const savedCwd = process.cwd();
43
+ const repo = createRepo();
44
+ try {
45
+ assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
46
+ insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
47
+
48
+ const mainHeadBefore = git(["rev-parse", "main"], repo);
49
+ process.chdir(repo);
50
+
51
+ assert.throws(
52
+ () => mergeMilestoneToMain(repo, "M001", "# M001\n- [x] **S01: Done**\n"),
53
+ /closeout-consistency-blocked/,
54
+ );
55
+
56
+ assert.equal(git(["rev-parse", "main"], repo), mainHeadBefore);
57
+ assert.equal(git(["branch", "--show-current"], repo), "main");
58
+ } finally {
59
+ closeDatabase();
60
+ process.chdir(savedCwd);
61
+ if (existsSync(repo)) rmSync(repo, { recursive: true, force: true });
62
+ }
63
+ });
@@ -15,7 +15,7 @@ import { delimiter, join } from "node:path";
15
15
  import { execFileSync } from "node:child_process";
16
16
 
17
17
  import { mergeMilestoneToMain } from "../auto-worktree.ts";
18
- import { closeDatabase, openDatabase } from "../gsd-db.ts";
18
+ import { closeDatabase, insertAssessment, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
19
19
  import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
20
20
  import { _clearGsdRootCache } from "../paths.ts";
21
21
  import { _resetServiceCache } from "../worktree.ts";
@@ -145,6 +145,15 @@ test("mergeMilestoneToMain keeps the Windows DB cycle closed through squash merg
145
145
 
146
146
  withPlatform("win32", () => {
147
147
  assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
148
+ insertMilestone({ id: "M001", title: "Windows DB cycle", status: "complete" });
149
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Done Slice", status: "complete" });
150
+ insertAssessment({
151
+ path: "milestones/M001/M001-VALIDATION.md",
152
+ milestoneId: "M001",
153
+ status: "pass",
154
+ scope: "milestone-validation",
155
+ fullContent: "verdict: pass",
156
+ });
148
157
  assert.equal(existsSync(join(repo, ".gsd", "gsd.db-shm")), true);
149
158
 
150
159
  process.env.PATH = `${bin}${delimiter}${originalPath}`;
@@ -6,7 +6,7 @@ import assert from "node:assert/strict";
6
6
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { tmpdir } from "node:os";
9
- import { openDatabase, insertMilestone, closeDatabase } from "../gsd-db.js";
9
+ import { openDatabase, insertAssessment, insertMilestone, insertSlice, closeDatabase } from "../gsd-db.js";
10
10
  import {
11
11
  isMilestoneCloseoutSettled,
12
12
  evaluateCompleteMilestoneDispatch,
@@ -39,6 +39,14 @@ test("isMilestoneCloseoutSettled requires DB closed and summary artifact", async
39
39
  mkdirSync(join(base, ".gsd"), { recursive: true });
40
40
  openDatabase(join(base, ".gsd", "gsd.db"));
41
41
  insertMilestone({ id: "M001", title: "Done", status: "complete" });
42
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Done Slice", status: "complete" });
43
+ insertAssessment({
44
+ path: "milestones/M001/M001-VALIDATION.md",
45
+ milestoneId: "M001",
46
+ status: "pass",
47
+ scope: "milestone-validation",
48
+ fullContent: "verdict: pass",
49
+ });
42
50
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
43
51
  mkdirSync(milestoneDir, { recursive: true });
44
52
  writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\n");
@@ -0,0 +1,100 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { GSD_COMMAND_DESCRIPTION, getGsdArgumentCompletions, TOP_LEVEL_SUBCOMMANDS } from "../commands/catalog.ts";
8
+ import { handleCoreCommand } from "../commands/handlers/core.ts";
9
+ import { DISPATCH_RULES } from "../auto-dispatch.ts";
10
+ import {
11
+ buildGsdPlannerSpawnPlan,
12
+ formatGsdPlannerCommand,
13
+ hasPlannerHandoffBeenOffered,
14
+ markPlannerHandoffOffered,
15
+ PLANNER_HANDOFF_RULE_NAME,
16
+ } from "../planner-handoff.ts";
17
+
18
+ describe("planner handoff command catalog", () => {
19
+ test("/gsd planner is hidden from description and completions", () => {
20
+ assert.doesNotMatch(GSD_COMMAND_DESCRIPTION, /\|planner(?:\||$)/);
21
+ assert.equal(
22
+ TOP_LEVEL_SUBCOMMANDS.some((command) => command.cmd === "planner"),
23
+ false,
24
+ "planner should not appear in top-level commands",
25
+ );
26
+
27
+ const completions = getGsdArgumentCompletions("pla");
28
+
29
+ assert.equal(
30
+ completions.some((completion) => completion.value === "planner"),
31
+ false,
32
+ "planner should not appear in top-level completions",
33
+ );
34
+
35
+ assert.deepEqual(
36
+ getGsdArgumentCompletions("planner --"),
37
+ [],
38
+ "planner should not expose nested completions",
39
+ );
40
+ });
41
+ });
42
+
43
+ describe("planner handoff command handler", () => {
44
+ test("/gsd planner falls through to the unknown-command path", async () => {
45
+ const notifications: Array<{ message: string; level?: string }> = [];
46
+ const ctx = {
47
+ ui: {
48
+ notify(message: string, level?: string) {
49
+ notifications.push({ message, level });
50
+ },
51
+ },
52
+ };
53
+
54
+ const handled = await handleCoreCommand("planner M001 --dry-run --inspect", ctx as any);
55
+
56
+ assert.equal(handled, false);
57
+ assert.deepEqual(notifications, []);
58
+ });
59
+ });
60
+
61
+ describe("planner handoff launcher", () => {
62
+ test("builds gsd-planner command with project and milestone context", () => {
63
+ const plan = buildGsdPlannerSpawnPlan({
64
+ basePath: "/tmp/project with spaces",
65
+ milestoneId: "M001",
66
+ extraArgs: ["--inspect"],
67
+ });
68
+
69
+ assert.deepEqual(plan, {
70
+ command: "gsd-planner",
71
+ args: ["--project", "/tmp/project with spaces", "--milestone", "M001", "--inspect"],
72
+ cwd: "/tmp/project with spaces",
73
+ });
74
+ assert.equal(
75
+ formatGsdPlannerCommand(plan),
76
+ 'gsd-planner --project "/tmp/project with spaces" --milestone M001 --inspect',
77
+ );
78
+ });
79
+
80
+ test("records one-shot handoff markers per milestone", () => {
81
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-planner-marker-"));
82
+ try {
83
+ assert.equal(hasPlannerHandoffBeenOffered(basePath, "M001"), false);
84
+ markPlannerHandoffOffered(basePath, "M001");
85
+ assert.equal(hasPlannerHandoffBeenOffered(basePath, "M001"), true);
86
+ assert.equal(hasPlannerHandoffBeenOffered(basePath, "M002"), false);
87
+ } finally {
88
+ rmSync(basePath, { recursive: true, force: true });
89
+ }
90
+ });
91
+ });
92
+
93
+ describe("planner handoff dispatch rule", () => {
94
+ test("rule is not registered while /gsd planner is disabled", () => {
95
+ assert.equal(
96
+ DISPATCH_RULES.some((rule) => rule.name === PLANNER_HANDOFF_RULE_NAME),
97
+ false,
98
+ );
99
+ });
100
+ });
@@ -2,7 +2,21 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.ts";
5
+ import {
6
+ RUN_UAT_BROWSER_TOOL_NAMES,
7
+ buildRunUatResultPresentation,
8
+ buildRunUatPresentationForType,
9
+ RUN_UAT_READ_ONLY_TOOL_NAMES,
10
+ RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
11
+ RUN_UAT_WORKFLOW_TOOL_NAMES,
12
+ } from "../tool-presentation-plan.ts";
13
+ import {
14
+ buildMinimalAutoGsdToolSet,
15
+ MINIMAL_AUTO_BASE_TOOL_NAMES,
16
+ MINIMAL_GSD_TOOL_NAMES,
17
+ } from "../bootstrap/register-hooks.ts";
18
+ import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.ts";
19
+ import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.ts";
6
20
 
7
21
  const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
8
22
  const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
@@ -15,6 +29,84 @@ function readTemplate(name: string): string {
15
29
  return readFileSync(join(templatesDir, `${name}.md`), "utf-8");
16
30
  }
17
31
 
32
+ function escapeRegExp(value: string): string {
33
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+ }
35
+
36
+ const registeredPhaseToolNames = [
37
+ ...new Set([
38
+ ...MINIMAL_AUTO_BASE_TOOL_NAMES,
39
+ ...MINIMAL_GSD_TOOL_NAMES,
40
+ ...Object.values(UNIT_TOOL_CONTRACTS).flatMap((contract) => contract.allowedGsdTools),
41
+ ]),
42
+ ];
43
+
44
+ const PHASE_PROMPT_TOOL_CALLS: Record<string, readonly string[]> = {
45
+ "research-milestone": ["gsd_summary_save"],
46
+ "plan-milestone": [
47
+ "gsd_milestone_status",
48
+ "gsd_plan_milestone",
49
+ "gsd_plan_slice",
50
+ "gsd_decision_save",
51
+ ],
52
+ "research-slice": ["gsd_summary_save"],
53
+ "plan-slice": ["gsd_reassess_roadmap", "gsd_plan_slice", "gsd_decision_save"],
54
+ "refine-slice": ["gsd_plan_slice", "gsd_decision_save"],
55
+ "replan-slice": ["gsd_replan_slice"],
56
+ "execute-task": ["gsd_task_complete"],
57
+ "reactive-execute": ["gsd_summary_save"],
58
+ "complete-slice": [
59
+ "gsd_exec",
60
+ "gsd_task_reopen",
61
+ "gsd_replan_slice",
62
+ "gsd_requirement_update",
63
+ "capture_thought",
64
+ "gsd_slice_complete",
65
+ "gsd_summary_save",
66
+ ],
67
+ "reassess-roadmap": ["gsd_milestone_status", "gsd_reassess_roadmap"],
68
+ "validate-milestone": ["gsd_milestone_status", "gsd_validate_milestone", "gsd_reassess_roadmap"],
69
+ "run-uat": ["gsd_uat_exec", "gsd_uat_result_save"],
70
+ "gate-evaluate": ["gsd_save_gate_result"],
71
+ "complete-milestone": [
72
+ "gsd_milestone_status",
73
+ "gsd_requirement_update",
74
+ "gsd_summary_save",
75
+ "capture_thought",
76
+ "gsd_complete_milestone",
77
+ ],
78
+ };
79
+
80
+ test("auto phase prompt tool calls are available in scoped tool surfaces", () => {
81
+ for (const [unitType, promptTools] of Object.entries(PHASE_PROMPT_TOOL_CALLS)) {
82
+ const prompt = readPrompt(unitType);
83
+ const activeTools = buildMinimalAutoGsdToolSet(
84
+ registeredPhaseToolNames,
85
+ unitType,
86
+ registeredPhaseToolNames,
87
+ );
88
+
89
+ for (const toolName of promptTools) {
90
+ assert.match(
91
+ prompt,
92
+ new RegExp(`\\b${escapeRegExp(toolName)}\\b`),
93
+ `${unitType} prompt should mention ${toolName}`,
94
+ );
95
+ assert.ok(
96
+ activeTools.includes(toolName),
97
+ `${unitType} prompt mentions ${toolName}, but scoped tools are ${activeTools.join(", ")}`,
98
+ );
99
+
100
+ const scopeResult = shouldBlockAutoUnitToolCall(unitType, toolName);
101
+ assert.equal(
102
+ scopeResult.block,
103
+ false,
104
+ `${unitType} phase gate blocked ${toolName}: ${scopeResult.reason ?? "unknown reason"}`,
105
+ );
106
+ }
107
+ }
108
+ });
109
+
18
110
  test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
19
111
  const prompt = readPrompt("reactive-execute");
20
112
  assert.match(prompt, /subagent-written summary as authoritative/i);
@@ -29,7 +121,7 @@ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence"
29
121
  assert.match(prompt, /uatType:\s*"\{\{uatType\}\}"/);
30
122
  assert.match(prompt, /gsd_uat_result_save/);
31
123
  assert.match(prompt, /presentedTools/);
32
- assert.match(prompt, /blockedTools/);
124
+ assert.match(prompt, /\{\{canonicalPresentation\}\}/);
33
125
  assert.match(prompt, /live-runtime/);
34
126
  assert.match(prompt, /browser\/runtime\/network/i);
35
127
  assert.match(prompt, /NEEDS-HUMAN/);
@@ -51,16 +143,66 @@ test("run-uat prompt gives the complete UAT result-save presentation contract",
51
143
  const prompt = readPrompt("run-uat");
52
144
  assert.match(prompt, /Call `gsd_uat_result_save` once after all checks are complete/);
53
145
  assert.doesNotMatch(prompt, /Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`/);
146
+ assert.match(prompt, /\{\{canonicalPresentation\}\}/);
147
+ assert.match(prompt, /\{\{toolPresentationPlanId\}\}/);
54
148
 
149
+ const presentation = buildRunUatResultPresentation();
150
+ assert.equal(presentation.toolPresentationPlanId, RUN_UAT_TOOL_PRESENTATION_PLAN_ID);
55
151
  for (const toolName of RUN_UAT_WORKFLOW_TOOL_NAMES) {
56
- assert.ok(prompt.includes(`"${toolName}"`), `prompt should include required presented tool ${toolName}`);
152
+ assert.ok(presentation.presentedTools.includes(toolName), `presentation should include required tool ${toolName}`);
153
+ }
154
+ for (const toolName of RUN_UAT_READ_ONLY_TOOL_NAMES) {
155
+ assert.ok(presentation.presentedTools.includes(toolName), `presentation should include read-only tool ${toolName}`);
57
156
  }
58
157
 
59
158
  for (const toolName of ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"] as const) {
60
- assert.ok(prompt.includes(`name: "${toolName}"`), `prompt should include blocked tool ${toolName}`);
159
+ assert.ok(
160
+ presentation.blockedTools.some((entry) => entry.name === toolName),
161
+ `presentation should include blocked tool ${toolName}`,
162
+ );
163
+ }
164
+
165
+ assert.ok(
166
+ presentation.blockedTools.every((entry) => entry.reason === "forbidden during run-uat"),
167
+ "presentation should explain blocked run-uat tools",
168
+ );
169
+ });
170
+
171
+ test("browser-executable UAT presentation uses direct browser tools", () => {
172
+ const presentation = buildRunUatPresentationForType("browser-executable");
173
+
174
+ assert.equal(presentation.surface, "hybrid");
175
+ for (const toolName of RUN_UAT_BROWSER_TOOL_NAMES) {
176
+ assert.ok(presentation.presentedTools.includes(toolName), `presentation should include browser tool ${toolName}`);
177
+ }
178
+ assert.ok(!presentation.presentedTools.some((toolName) => toolName.startsWith("mcp__gsd-browser__")));
179
+ });
180
+
181
+ test("live-runtime and mixed UAT presentations also surface browser tools", () => {
182
+ // Regression (M001/S03): the run-uat prompt tells live-runtime and mixed to
183
+ // drive a browser, so the runner must actually receive the browser tools and
184
+ // a hybrid surface — otherwise live checks silently downgrade to NEEDS-HUMAN.
185
+ for (const uatType of ["live-runtime", "mixed", "human-experience"] as const) {
186
+ const presentation = buildRunUatPresentationForType(uatType);
187
+ assert.equal(presentation.surface, "hybrid", `${uatType} should use the hybrid surface`);
188
+ for (const toolName of RUN_UAT_BROWSER_TOOL_NAMES) {
189
+ assert.ok(
190
+ presentation.presentedTools.includes(toolName),
191
+ `${uatType} presentation should include browser tool ${toolName}`,
192
+ );
193
+ }
61
194
  }
195
+ });
62
196
 
63
- assert.ok(prompt.includes("forbidden during run-uat"), "prompt should explain blocked run-uat tools");
197
+ test("artifact-driven and runtime-executable UAT presentations stay browser-free", () => {
198
+ for (const uatType of ["artifact-driven", "runtime-executable"] as const) {
199
+ const presentation = buildRunUatPresentationForType(uatType);
200
+ assert.equal(presentation.surface, "mcp", `${uatType} should use the mcp surface`);
201
+ assert.ok(
202
+ !RUN_UAT_BROWSER_TOOL_NAMES.some((toolName) => presentation.presentedTools.includes(toolName)),
203
+ `${uatType} presentation should not include browser tools`,
204
+ );
205
+ }
64
206
  });
65
207
 
66
208
  test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
@@ -210,6 +210,61 @@ test("end-to-end: audit event is emitted when an auto trace is active", async ()
210
210
  }
211
211
  });
212
212
 
213
+ test("same-API transform with changes does not fire the observer (no real provider switch)", async () => {
214
+ const { basePath, cleanup } = withTempBasePath();
215
+ try {
216
+ initNotificationStore(basePath);
217
+ installProviderSwitchObserver();
218
+
219
+ // Target api === source api. The conversation ends on an unresolved tool
220
+ // call, so a synthetic tool result IS backfilled (a non-empty report) — but
221
+ // this is a within-provider normalization, not a cross-provider switch.
222
+ // `sourceApi` is omitted (the common case), so fromApi defaults to the
223
+ // target api and equals toApi. The observer must stay silent.
224
+ const sameApiModel = {
225
+ id: "gpt-5",
226
+ name: "GPT-5",
227
+ api: "openai-responses",
228
+ provider: "openai",
229
+ baseUrl: "",
230
+ reasoning: false,
231
+ input: ["text"],
232
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
233
+ contextWindow: 128000,
234
+ maxTokens: 8192,
235
+ } as Parameters<typeof transformMessagesWithReport>[1];
236
+
237
+ const messages = [
238
+ {
239
+ role: "assistant" as const,
240
+ content: [
241
+ { type: "toolCall" as const, id: "call_orphan_1", name: "bash", arguments: {} },
242
+ ],
243
+ api: "openai-responses",
244
+ provider: "openai",
245
+ model: "gpt-5",
246
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
247
+ stopReason: "stop" as const,
248
+ timestamp: Date.now(),
249
+ },
250
+ ];
251
+
252
+ transformMessagesWithReport(
253
+ messages as Parameters<typeof transformMessagesWithReport>[0],
254
+ sameApiModel,
255
+ );
256
+
257
+ assert.equal(getProviderSwitchStats().totalSwitches, 0, "same→same transform must not count as a provider switch");
258
+ assert.equal(
259
+ readNotifications(basePath).filter((n) => n.message.includes("Provider switch")).length,
260
+ 0,
261
+ "same→same transform must not emit a provider-switch notification",
262
+ );
263
+ } finally {
264
+ cleanup();
265
+ }
266
+ });
267
+
213
268
  test("empty report does not bump counter or emit a notification", async () => {
214
269
  const { basePath, cleanup } = withTempBasePath();
215
270
  try {
@@ -70,6 +70,50 @@ test("register-hooks hard-blocks destructive bash commands outside auto-mode", a
70
70
  assert.match(block?.reason ?? "", /IaC apply\/destroy/);
71
71
  });
72
72
 
73
+ test("register-hooks keeps depth-gate reason model-facing and adds displayReason", async (t) => {
74
+ const dir = makeTempDir("display-reason");
75
+ const originalCwd = process.cwd();
76
+ process.chdir(dir);
77
+ resetWriteGateState(dir);
78
+
79
+ t.after(() => {
80
+ try {
81
+ resetWriteGateState(dir);
82
+ } finally {
83
+ process.chdir(originalCwd);
84
+ rmSync(dir, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
89
+ const pi = {
90
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
91
+ const existing = handlers.get(event) ?? [];
92
+ existing.push(handler);
93
+ handlers.set(event, existing);
94
+ },
95
+ } as any;
96
+
97
+ registerHooks(pi, []);
98
+
99
+ let block: any;
100
+ for (const handler of handlers.get("tool_call") ?? []) {
101
+ const result = await handler({
102
+ toolName: "write",
103
+ input: {
104
+ path: join(dir, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
105
+ content: "# M001 Context\n",
106
+ },
107
+ });
108
+ if (result?.block) block = result;
109
+ }
110
+
111
+ assert.equal(block?.block, true);
112
+ assert.match(block?.reason ?? "", /HARD BLOCK: Cannot write to milestone CONTEXT\.md/);
113
+ assert.match(block?.reason ?? "", /ask_user_questions/);
114
+ assert.equal(block?.displayReason, "Depth check required before writing milestone context.");
115
+ });
116
+
73
117
  test("register-hooks unlocks milestone depth verification from question id without guided-flow state (#4047)", async (t) => {
74
118
  const dir = makeTempDir("manual");
75
119
  const originalCwd = process.cwd();
@@ -123,6 +123,10 @@ test("#4782 phase 3: buildRunUatPrompt inlines UAT and keeps summary/project con
123
123
  // Project path is advertised on-demand; full project body is not inlined.
124
124
  assert.match(prompt, /\.gsd\/PROJECT\.md/);
125
125
  assert.ok(!prompt.includes("Run-UAT composer fixture project"), "run-uat should not inline full project context");
126
+
127
+ assert.match(prompt, /"toolPresentationPlanId": "run-uat\/default-v1"/);
128
+ assert.match(prompt, /"gsd_uat_result_save"/);
129
+ assert.match(prompt, /"read"/);
126
130
  });
127
131
 
128
132
  test("#4782 phase 3: buildRunUatPrompt omits optional slice summary when file is missing", async (t) => {
@@ -7,6 +7,7 @@ import assert from "node:assert/strict";
7
7
  import { classifyFailure } from "../recovery-classification.js";
8
8
  import { reconcileBeforeDispatch } from "../state-reconciliation.js";
9
9
  import { compileUnitToolContract } from "../tool-contract.js";
10
+ import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
10
11
  import type { GSDState } from "../types.js";
11
12
 
12
13
  function makeState(overrides: Partial<GSDState> = {}): GSDState {
@@ -63,10 +64,35 @@ test("Tool Contract compiles known Unit prompt and tool policy", () => {
63
64
  assert.equal(result.ok, true);
64
65
  assert.equal(result.ok && result.contract.unitType, "execute-task");
65
66
  assert.deepEqual(result.ok && result.contract.requiredWorkflowTools, ["gsd_task_complete"]);
67
+ assert.deepEqual(result.ok && result.contract.forbiddenWorkflowTools, []);
66
68
  assert.equal(result.ok && result.contract.toolsPolicy.mode, "all");
67
69
  assert.ok(result.ok && result.contract.validationRules.includes("closeout-tool-present"));
68
70
  });
69
71
 
72
+ test("Tool Contract records high-risk cross-phase tool boundaries without single-owning every tool", () => {
73
+ const completeSlice = compileUnitToolContract("complete-slice");
74
+ const runUat = compileUnitToolContract("run-uat");
75
+
76
+ assert.equal(completeSlice.ok, true);
77
+ assert.ok(
78
+ completeSlice.ok &&
79
+ completeSlice.contract.forbiddenWorkflowTools.some((tool) => tool.name === "gsd_uat_result_save"),
80
+ "complete-slice should explicitly forbid saving UAT Assessments",
81
+ );
82
+
83
+ assert.equal(runUat.ok, true);
84
+ assert.ok(
85
+ runUat.ok &&
86
+ runUat.contract.requiredWorkflowTools.includes("gsd_uat_result_save"),
87
+ "run-uat should own the UAT result-save tool",
88
+ );
89
+ assert.ok(
90
+ runUat.ok &&
91
+ runUat.contract.forbiddenWorkflowTools.some((tool) => tool.name === "gsd_exec"),
92
+ "run-uat should prefer typed UAT execution over generic gsd_exec",
93
+ );
94
+ });
95
+
70
96
  test("Tool Contract fails closed for unknown Units", () => {
71
97
  const result = compileUnitToolContract("custom-step");
72
98
 
@@ -74,10 +100,40 @@ test("Tool Contract fails closed for unknown Units", () => {
74
100
  assert.equal(!result.ok && result.reason, "unknown-unit-type");
75
101
  });
76
102
 
103
+ test("auto Unit tool scope blocks complete-slice from saving UAT Assessment", () => {
104
+ const result = shouldBlockAutoUnitToolCall("complete-slice", "gsd_uat_result_save");
105
+
106
+ assert.equal(result.block, true);
107
+ assert.match(result.reason ?? "", /Tool Contract failure/);
108
+ assert.match(result.reason ?? "", /Run UAT owns persisted UAT Assessment/);
109
+ });
110
+
111
+ test("auto Unit tool scope allows plan-slice to reassess invalid roadmap assumptions", () => {
112
+ const result = shouldBlockAutoUnitToolCall("plan-slice", "gsd_reassess_roadmap");
113
+
114
+ assert.equal(result.block, false);
115
+ });
116
+
117
+ test("auto Unit tool scope allows status/read helpers named by closeout prompts", () => {
118
+ for (const unitType of ["plan-milestone", "validate-milestone", "complete-milestone", "reassess-roadmap"]) {
119
+ const result = shouldBlockAutoUnitToolCall(unitType, "gsd_milestone_status");
120
+ assert.equal(result.block, false, `${unitType} should be able to call gsd_milestone_status`);
121
+ }
122
+ });
123
+
124
+ test("auto Unit tool scope blocks stale per-task planner in slice planning phases", () => {
125
+ for (const unitType of ["plan-slice", "refine-slice", "replan-slice"]) {
126
+ const result = shouldBlockAutoUnitToolCall(unitType, "gsd_plan_task");
127
+ assert.equal(result.block, true, `${unitType} should not call stale gsd_plan_task`);
128
+ }
129
+ });
130
+
77
131
  test("Recovery Classification covers ADR-015 failure families", () => {
78
132
  const cases = [
79
133
  ["invalid tool schema enum", "tool-schema", "stop"],
134
+ ["Tool Contract failure: complete-slice cannot use gsd_uat_result_save", "tool-contract", "stop"],
80
135
  ["deterministic policy rejection", "deterministic-policy", "stop"],
136
+ ["cannot legally advance because required UAT Assessment artifact is missing", "lifecycle-progression", "stop"],
81
137
  ["stale worker lease", "stale-worker", "stop"],
82
138
  ["worktree root missing .git", "worktree-invalid", "stop"],
83
139
  ["verification drift in state snapshot", "verification-drift", "escalate"],
@@ -3,8 +3,8 @@
3
3
  // Focused tests for `resolveSkillManifest` and `filterSkillsByManifest`.
4
4
  // Covers the wildcard semantics, the newly seeded unit-type entries
5
5
  // (complete-milestone, validate-milestone, reassess-roadmap, research-slice,
6
- // plan-slice, refine-slice, replan-slice, run-uat), and the deliberate
7
- // wildcard fallback for the execute-task hot path (RFC #4779).
6
+ // plan-slice, refine-slice, replan-slice, run-uat, complete-slice), and the
7
+ // deliberate wildcard fallback for the execute-task hot path (RFC #4779).
8
8
 
9
9
  import test from "node:test";
10
10
  import assert from "node:assert/strict";
@@ -23,6 +23,7 @@ const NEWLY_WIRED_UNIT_TYPES = [
23
23
  "refine-slice",
24
24
  "replan-slice",
25
25
  "run-uat",
26
+ "complete-slice",
26
27
  ] as const;
27
28
 
28
29
  test("resolveSkillManifest returns null for undefined unit type (wildcard)", () => {
@@ -65,7 +66,7 @@ test("resolveSkillManifest: slice-level manifests include decompose-into-slices"
65
66
  });
66
67
 
67
68
  test("resolveSkillManifest: validation / completion flows include verify-before-complete", () => {
68
- for (const unitType of ["complete-milestone", "validate-milestone", "run-uat"] as const) {
69
+ for (const unitType of ["complete-milestone", "validate-milestone", "run-uat", "complete-slice"] as const) {
69
70
  const allowlist = resolveSkillManifest(unitType);
70
71
  assert.ok(
71
72
  allowlist?.includes("verify-before-complete"),