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

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 (145) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -0
  3. package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
  4. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  5. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  6. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  7. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
  8. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -8
  9. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  10. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  11. package/dist/resources/extensions/gsd/guided-flow.js +89 -107
  12. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  13. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  14. package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -17
  15. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  16. package/dist/resources/extensions/gsd/tool-contract.js +5 -0
  17. package/dist/resources/extensions/gsd/tool-presentation-plan.js +17 -7
  18. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -4
  19. package/dist/resources/extensions/gsd/unit-tool-contracts.js +169 -0
  20. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -75
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  23. package/dist/web/standalone/.next/build-manifest.json +2 -2
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.html +1 -1
  42. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  49. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  50. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  52. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  53. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  54. package/package.json +1 -1
  55. package/packages/cloud-mcp-gateway/package.json +2 -2
  56. package/packages/contracts/package.json +1 -1
  57. package/packages/daemon/package.json +4 -4
  58. package/packages/gsd-agent-core/package.json +5 -5
  59. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  60. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  61. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  62. package/packages/gsd-agent-modes/package.json +7 -7
  63. package/packages/mcp-server/package.json +3 -3
  64. package/packages/native/package.json +1 -1
  65. package/packages/pi-agent-core/dist/agent-loop.js +4 -3
  66. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  67. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  68. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  69. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  70. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  71. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  72. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  73. package/packages/pi-agent-core/dist/types.d.ts +3 -1
  74. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  75. package/packages/pi-agent-core/dist/types.js.map +1 -1
  76. package/packages/pi-agent-core/package.json +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +6 -6
  78. package/packages/pi-ai/dist/models.generated.js +6 -6
  79. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  80. package/packages/pi-ai/package.json +1 -1
  81. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  82. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  85. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  88. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  90. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  92. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  95. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  96. package/packages/pi-coding-agent/package.json +7 -7
  97. package/packages/pi-tui/package.json +1 -1
  98. package/packages/rpc-client/package.json +2 -2
  99. package/pkg/package.json +1 -1
  100. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -0
  101. package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
  102. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  103. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  104. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  105. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
  106. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +23 -8
  107. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  108. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  109. package/src/resources/extensions/gsd/guided-flow.ts +124 -134
  110. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  111. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  112. package/src/resources/extensions/gsd/prompts/run-uat.md +3 -17
  113. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  114. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  115. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  116. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  117. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  118. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  119. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  120. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  121. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  122. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  123. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  124. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  125. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  126. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +3 -0
  127. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  128. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  129. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  130. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +23 -5
  131. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  132. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  133. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +36 -0
  134. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  135. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +221 -0
  136. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  137. package/src/resources/extensions/gsd/tool-contract.ts +6 -0
  138. package/src/resources/extensions/gsd/tool-presentation-plan.ts +38 -8
  139. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +100 -5
  140. package/src/resources/extensions/gsd/unit-tool-contracts.ts +186 -0
  141. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -75
  142. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  143. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  144. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_buildManifest.js +0 -0
  145. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_ssgManifest.js +0 -0
@@ -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");
@@ -2,7 +2,12 @@ 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
+ buildRunUatResultPresentation,
7
+ RUN_UAT_READ_ONLY_TOOL_NAMES,
8
+ RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
9
+ RUN_UAT_WORKFLOW_TOOL_NAMES,
10
+ } from "../tool-presentation-plan.ts";
6
11
 
7
12
  const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
8
13
  const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
@@ -29,7 +34,7 @@ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence"
29
34
  assert.match(prompt, /uatType:\s*"\{\{uatType\}\}"/);
30
35
  assert.match(prompt, /gsd_uat_result_save/);
31
36
  assert.match(prompt, /presentedTools/);
32
- assert.match(prompt, /blockedTools/);
37
+ assert.match(prompt, /\{\{canonicalPresentation\}\}/);
33
38
  assert.match(prompt, /live-runtime/);
34
39
  assert.match(prompt, /browser\/runtime\/network/i);
35
40
  assert.match(prompt, /NEEDS-HUMAN/);
@@ -51,16 +56,29 @@ test("run-uat prompt gives the complete UAT result-save presentation contract",
51
56
  const prompt = readPrompt("run-uat");
52
57
  assert.match(prompt, /Call `gsd_uat_result_save` once after all checks are complete/);
53
58
  assert.doesNotMatch(prompt, /Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`/);
59
+ assert.match(prompt, /\{\{canonicalPresentation\}\}/);
60
+ assert.match(prompt, /\{\{toolPresentationPlanId\}\}/);
54
61
 
62
+ const presentation = buildRunUatResultPresentation();
63
+ assert.equal(presentation.toolPresentationPlanId, RUN_UAT_TOOL_PRESENTATION_PLAN_ID);
55
64
  for (const toolName of RUN_UAT_WORKFLOW_TOOL_NAMES) {
56
- assert.ok(prompt.includes(`"${toolName}"`), `prompt should include required presented tool ${toolName}`);
65
+ assert.ok(presentation.presentedTools.includes(toolName), `presentation should include required tool ${toolName}`);
66
+ }
67
+ for (const toolName of RUN_UAT_READ_ONLY_TOOL_NAMES) {
68
+ assert.ok(presentation.presentedTools.includes(toolName), `presentation should include read-only tool ${toolName}`);
57
69
  }
58
70
 
59
71
  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}`);
72
+ assert.ok(
73
+ presentation.blockedTools.some((entry) => entry.name === toolName),
74
+ `presentation should include blocked tool ${toolName}`,
75
+ );
61
76
  }
62
77
 
63
- assert.ok(prompt.includes("forbidden during run-uat"), "prompt should explain blocked run-uat tools");
78
+ assert.ok(
79
+ presentation.blockedTools.every((entry) => entry.reason === "forbidden during run-uat"),
80
+ "presentation should explain blocked run-uat tools",
81
+ );
64
82
  });
65
83
 
66
84
  test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
@@ -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,20 @@ 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
+
77
111
  test("Recovery Classification covers ADR-015 failure families", () => {
78
112
  const cases = [
79
113
  ["invalid tool schema enum", "tool-schema", "stop"],
114
+ ["Tool Contract failure: complete-slice cannot use gsd_uat_result_save", "tool-contract", "stop"],
80
115
  ["deterministic policy rejection", "deterministic-policy", "stop"],
116
+ ["cannot legally advance because required UAT Assessment artifact is missing", "lifecycle-progression", "stop"],
81
117
  ["stale worker lease", "stale-worker", "stop"],
82
118
  ["worktree root missing .git", "worktree-invalid", "stop"],
83
119
  ["verification drift in state snapshot", "verification-drift", "escalate"],
@@ -101,7 +101,7 @@ test("buildMinimalAutoGsdToolSet keeps unit-specific completion tools without al
101
101
  assert.ok(!result.includes("gsd_complete_slice"));
102
102
  });
103
103
 
104
- test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific tools", () => {
104
+ test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific and read-only tools", () => {
105
105
  const active = ["ask_user_questions", "bash", "read", "edit", "write", "gsd_summary_save"];
106
106
  const registered = [
107
107
  ...active,
@@ -123,11 +123,11 @@ test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific tools", () => {
123
123
  assert.ok(result.includes("gsd_resume"));
124
124
  assert.ok(result.includes("gsd_milestone_status"));
125
125
  assert.ok(result.includes("gsd_journal_query"));
126
+ assert.ok(result.includes("read"));
126
127
  assert.ok(result.includes("browser_navigate"), "run-uat needs browser_navigate");
127
128
  assert.ok(result.includes("browser_click"), "run-uat needs browser_click");
128
129
  assert.ok(!result.includes("ToolSearch"));
129
130
  assert.ok(!result.includes("bash"));
130
- assert.ok(!result.includes("read"));
131
131
  assert.ok(!result.includes("edit"));
132
132
  assert.ok(!result.includes("write"));
133
133
  assert.ok(!result.includes("gsd_exec"));
@@ -230,9 +230,9 @@ test("buildMinimalAutoGsdToolSet preserves compatible browser add-ons for run-ua
230
230
  assert.ok(result.includes("gsd_uat_exec"));
231
231
  assert.ok(result.includes("gsd_uat_result_save"));
232
232
  assert.ok(result.includes("subagent"));
233
+ assert.ok(result.includes("read"));
233
234
  assert.ok(!result.includes("ToolSearch"));
234
235
  assert.ok(!result.includes("bash"));
235
- assert.ok(!result.includes("read"));
236
236
  assert.ok(!result.includes("edit"));
237
237
  assert.ok(!result.includes("write"));
238
238
  assert.ok(!result.includes("gsd_exec"));
@@ -281,12 +281,12 @@ test("buildMinimalAutoGsdToolSet honors provider-compatible registered tools for
281
281
 
282
282
  assert.ok(result.includes("gsd_uat_exec"));
283
283
  assert.ok(result.includes("gsd_uat_result_save"));
284
+ assert.ok(result.includes("read"));
284
285
  assert.ok(result.includes("browser_navigate"));
285
286
  assert.ok(result.includes("browser_click"));
286
287
  assert.ok(!result.includes("browser_screenshot"), "provider-filtered screenshot tool must stay filtered");
287
288
  assert.ok(!result.includes("ToolSearch"));
288
289
  assert.ok(!result.includes("bash"));
289
- assert.ok(!result.includes("read"));
290
290
  assert.ok(!result.includes("gsd_exec"));
291
291
  assert.ok(!result.includes("gsd_summary_save"));
292
292
  assert.ok(!result.includes("gsd_save_gate_result"));
@@ -75,6 +75,11 @@ test("closeout executors reject phase escalation from the wrong active auto unit
75
75
  const milestone = await executeCompleteMilestone({} as Parameters<typeof executeCompleteMilestone>[0], "/tmp/project");
76
76
  assert.equal(milestone.isError, true);
77
77
  assert.match(String(milestone.details.error), /complete_milestone may only run from complete-milestone/);
78
+
79
+ const uat = await executeUatResultSave({} as Parameters<typeof executeUatResultSave>[0], "/tmp/project");
80
+ assert.equal(uat.isError, true);
81
+ assert.match(String(uat.details.error), /save_uat_result may only run from run-uat/);
82
+ assert.match(String(uat.details.error), /Tool Contract failure/);
78
83
  } finally {
79
84
  autoSession.reset();
80
85
  }
@@ -643,6 +648,210 @@ test("executeUatResultSave accepts gsd_uat_exec evidence written in a milestone
643
648
  }
644
649
  });
645
650
 
651
+ test("executeUatResultSave supplies canonical presentation and normalizes verdict casing", async () => {
652
+ const base = makeTmpBase();
653
+ const worktree = join(base, ".gsd", "worktrees", "M001");
654
+ const worktreeExecDir = join(worktree, ".gsd", "exec");
655
+ const evidenceId = "uat-lowercase-verdict";
656
+ try {
657
+ openTestDb(base);
658
+ seedMilestone("M001", "Milestone One");
659
+ seedSlice("M001", "S03", "complete");
660
+ mkdirSync(worktreeExecDir, { recursive: true });
661
+ writeFileSync(
662
+ join(worktreeExecDir, `${evidenceId}.meta.json`),
663
+ JSON.stringify({
664
+ id: evidenceId,
665
+ metadata: {
666
+ kind: "uat_exec",
667
+ milestoneId: "M001",
668
+ sliceId: "S03",
669
+ checkId: "UAT-01",
670
+ intent: "uat-artifact-check",
671
+ },
672
+ }),
673
+ "utf-8",
674
+ );
675
+
676
+ const result = await inProjectDir(worktree, () => executeUatResultSave({
677
+ milestoneId: "M001",
678
+ sliceId: "S03",
679
+ uatType: "artifact-driven",
680
+ verdict: "pass",
681
+ checks: [{
682
+ id: "UAT-01",
683
+ description: "Static artifact contract passes",
684
+ mode: "artifact",
685
+ result: "PASS",
686
+ evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
687
+ notes: "Artifact check passed.",
688
+ }],
689
+ notes: "UAT passed with canonical presentation supplied by the executor.",
690
+ } as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
691
+
692
+ assert.equal(result.isError, undefined);
693
+ assert.equal(result.details.verdict, "PASS");
694
+
695
+ const attempt = JSON.parse(readFileSync(
696
+ join(base, ".gsd", "uat", "M001", "S03", "attempt-1.json"),
697
+ "utf-8",
698
+ )) as { presentation?: { toolPresentationPlanId?: string; presentedTools?: string[] } };
699
+ assert.equal(attempt.presentation?.toolPresentationPlanId, "run-uat/default-v1");
700
+ assert.ok(attempt.presentation?.presentedTools?.includes("gsd_uat_result_save"));
701
+ assert.ok(attempt.presentation?.presentedTools?.includes("read"));
702
+ } finally {
703
+ closeDatabase();
704
+ cleanup(base);
705
+ }
706
+ });
707
+
708
+ test("executeUatResultSave merges canonical plan ID and read-only tools when presentation lacks plan ID", async () => {
709
+ const base = makeTmpBase();
710
+ const worktree = join(base, ".gsd", "worktrees", "M001");
711
+ const worktreeExecDir = join(worktree, ".gsd", "exec");
712
+ const evidenceId = "uat-no-plan-id-evidence";
713
+ try {
714
+ openTestDb(base);
715
+ seedMilestone("M001", "Milestone One");
716
+ seedSlice("M001", "S05", "complete");
717
+ mkdirSync(worktreeExecDir, { recursive: true });
718
+ writeFileSync(
719
+ join(worktreeExecDir, `${evidenceId}.meta.json`),
720
+ JSON.stringify({
721
+ id: evidenceId,
722
+ metadata: {
723
+ kind: "uat_exec",
724
+ milestoneId: "M001",
725
+ sliceId: "S05",
726
+ checkId: "UAT-01",
727
+ intent: "uat-artifact-check",
728
+ },
729
+ }),
730
+ "utf-8",
731
+ );
732
+
733
+ const result = await inProjectDir(worktree, () => executeUatResultSave({
734
+ milestoneId: "M001",
735
+ sliceId: "S05",
736
+ uatType: "artifact-driven",
737
+ verdict: "PASS",
738
+ checks: [{
739
+ id: "UAT-01",
740
+ description: "Presentation plan ID absent from provider call",
741
+ mode: "artifact",
742
+ result: "PASS",
743
+ evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
744
+ notes: "Canonical merge should apply even when toolPresentationPlanId is absent.",
745
+ }],
746
+ presentation: {
747
+ surface: "mcp",
748
+ presentedTools: [
749
+ "gsd_uat_exec",
750
+ "gsd_uat_result_save",
751
+ "gsd_resume",
752
+ "gsd_milestone_status",
753
+ "gsd_journal_query",
754
+ ],
755
+ blockedTools: [
756
+ { name: "gsd_exec", reason: "forbidden during run-uat" },
757
+ { name: "gsd_summary_save", reason: "forbidden during run-uat" },
758
+ { name: "gsd_save_gate_result", reason: "forbidden during run-uat" },
759
+ ],
760
+ },
761
+ notes: "Provider omitted toolPresentationPlanId; executor must canonicalize.",
762
+ } as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
763
+
764
+ assert.equal(result.isError, undefined);
765
+ assert.equal(result.details.verdict, "PASS");
766
+
767
+ const attempt = JSON.parse(readFileSync(
768
+ join(base, ".gsd", "uat", "M001", "S05", "attempt-1.json"),
769
+ "utf-8",
770
+ )) as { presentation?: { toolPresentationPlanId?: string; presentedTools?: string[] } };
771
+ assert.equal(attempt.presentation?.toolPresentationPlanId, "run-uat/default-v1");
772
+ assert.ok(attempt.presentation?.presentedTools?.includes("read"), "read-only tool must be merged in");
773
+ assert.ok(attempt.presentation?.presentedTools?.includes("gsd_uat_result_save"));
774
+ } finally {
775
+ closeDatabase();
776
+ cleanup(base);
777
+ }
778
+ });
779
+
780
+ test("executeUatResultSave rejects saved UAT without fresh UAT-owned evidence", async () => {
781
+ const base = makeTmpBase();
782
+ const worktree = join(base, ".gsd", "worktrees", "M001");
783
+ const worktreeExecDir = join(worktree, ".gsd", "exec");
784
+ const evidenceId = "generic-exec-evidence";
785
+ try {
786
+ openTestDb(base);
787
+ seedMilestone("M001", "Milestone One");
788
+ seedSlice("M001", "S04", "complete");
789
+ mkdirSync(worktreeExecDir, { recursive: true });
790
+ writeFileSync(
791
+ join(worktreeExecDir, `${evidenceId}.meta.json`),
792
+ JSON.stringify({
793
+ id: evidenceId,
794
+ metadata: { kind: "exec" },
795
+ }),
796
+ "utf-8",
797
+ );
798
+
799
+ const result = await inProjectDir(worktree, () => executeUatResultSave({
800
+ milestoneId: "M001",
801
+ sliceId: "S04",
802
+ uatType: "artifact-driven",
803
+ verdict: "PASS",
804
+ checks: [{
805
+ id: "UAT-01",
806
+ description: "Static artifact contract passes",
807
+ mode: "artifact",
808
+ result: "PASS",
809
+ evidence: [{ kind: "gsd_exec", ref: evidenceId }],
810
+ notes: "Generic evidence should not satisfy fresh UAT evidence.",
811
+ }],
812
+ notes: "UAT should not pass without fresh UAT-owned evidence.",
813
+ } as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
814
+
815
+ assert.equal(result.isError, true);
816
+ assert.match(String(result.content[0]?.text), /fresh gsd_uat_exec evidence/);
817
+ } finally {
818
+ closeDatabase();
819
+ cleanup(base);
820
+ }
821
+ });
822
+
823
+ test("executeUatResultSave rejects an unrecognized uatType", async () => {
824
+ const base = makeTmpBase();
825
+ const worktree = join(base, ".gsd", "worktrees", "M001");
826
+ try {
827
+ openTestDb(base);
828
+ mkdirSync(worktree, { recursive: true });
829
+ seedMilestone("M001", "Milestone One");
830
+ seedSlice("M001", "S06", "complete");
831
+
832
+ const result = await inProjectDir(worktree, () => executeUatResultSave({
833
+ milestoneId: "M001",
834
+ sliceId: "S06",
835
+ uatType: "hallucinated-mode",
836
+ verdict: "PASS",
837
+ checks: [{
838
+ id: "UAT-01",
839
+ description: "Static artifact contract passes",
840
+ mode: "artifact",
841
+ result: "PASS",
842
+ evidence: [{ kind: "gsd_uat_exec", ref: "some-ref" }],
843
+ }],
844
+ notes: "Should fail before evidence validation.",
845
+ } as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
846
+
847
+ assert.equal(result.isError, true);
848
+ assert.match(String(result.content[0]?.text), /uatType must be one of/);
849
+ } finally {
850
+ closeDatabase();
851
+ cleanup(base);
852
+ }
853
+ });
854
+
646
855
  test("executeUatResultSave rejects artifact-driven PASS with human follow-up checks", async () => {
647
856
  const base = makeTmpBase();
648
857
  const worktree = join(base, ".gsd", "worktrees", "M001");
@@ -1409,6 +1618,10 @@ test("executeSummarySave blocks final root artifacts while approval gate is pend
1409
1618
 
1410
1619
  assert.equal(result.isError, true);
1411
1620
  assert.equal(result.details.error, "root_artifact_write_blocked");
1621
+ assert.equal(
1622
+ result.details.displayReason,
1623
+ "Approval confirmation required before saving final project setup artifacts.",
1624
+ );
1412
1625
  assert.match(result.content[0].text, /has not been confirmed/);
1413
1626
  assert.equal(existsSync(join(base, ".gsd", "REQUIREMENTS.md")), false);
1414
1627
 
@@ -1451,6 +1664,10 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
1451
1664
 
1452
1665
  assert.equal(blocked.isError, true);
1453
1666
  assert.equal(blocked.details.error, "root_artifact_write_blocked");
1667
+ assert.equal(
1668
+ blocked.details.displayReason,
1669
+ "Approval confirmation required before saving final project setup artifacts.",
1670
+ );
1454
1671
  assert.match(blocked.content[0].text, /fail-closed/);
1455
1672
  assert.equal(existsSync(join(base, ".gsd", "PROJECT.md")), false);
1456
1673
 
@@ -1667,6 +1884,10 @@ test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file i
1667
1884
  content: "# Context\n\ncontent",
1668
1885
  }, base));
1669
1886
  assert.equal(blocked.isError, true, "should be blocked without depth verification");
1887
+ assert.equal(
1888
+ blocked.details.displayReason,
1889
+ "Depth check required before writing milestone context.",
1890
+ );
1670
1891
  assert.match(
1671
1892
  blocked.content[0].text,
1672
1893
  /HARD BLOCK/,
@@ -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,6 +42,8 @@ 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
48
  /^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool),
44
49
  );
@@ -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}`,