@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
@@ -96,6 +96,10 @@ import { probeGitConflictState } from "./git-conflict-state.js";
96
96
  import { runTurnGitAction } from "./git-service.js";
97
97
  import { parseUnitId } from "./unit-id.js";
98
98
  import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
99
+ import {
100
+ checkCloseoutConsistencyGate,
101
+ formatCloseoutConsistencyBlock,
102
+ } from "./closeout-consistency-gate.js";
99
103
 
100
104
  // ─── Types ────────────────────────────────────────────────────────────────
101
105
 
@@ -1635,6 +1639,16 @@ export const DISPATCH_RULES: DispatchRule[] = [
1635
1639
  prompt: await buildCompleteMilestonePrompt(mid, midTitle, basePath),
1636
1640
  };
1637
1641
  }
1642
+ if (milestone) {
1643
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
1644
+ if (!closeoutGate.ok) {
1645
+ return {
1646
+ action: "stop",
1647
+ reason: formatCloseoutConsistencyBlock(closeoutGate),
1648
+ level: "warning",
1649
+ };
1650
+ }
1651
+ }
1638
1652
  }
1639
1653
  return {
1640
1654
  action: "stop",
@@ -46,6 +46,7 @@ import { hasBrowserRequiredText } from "./browser-evidence.js";
46
46
  import { debugLog } from "./debug-logger.js";
47
47
  import { buildSkillActivationBlock, buildSkillDiscoveryVars } from "./skill-activation.js";
48
48
  import { findMilestoneIds } from "./milestone-ids.js";
49
+ import { buildRunUatResultPresentation, RUN_UAT_TOOL_PRESENTATION_PLAN_ID } from "./tool-presentation-plan.js";
49
50
 
50
51
  export { buildSkillActivationBlock, buildSkillDiscoveryVars };
51
52
 
@@ -3384,6 +3385,7 @@ export async function buildRunUatPrompt(
3384
3385
 
3385
3386
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "ASSESSMENT"));
3386
3387
  const uatType = resolveEffectiveUatType(uatContent);
3388
+ const canonicalPresentation = JSON.stringify(buildRunUatResultPresentation(), null, 2);
3387
3389
 
3388
3390
  return loadPrompt("run-uat", {
3389
3391
  workingDirectory: base,
@@ -3392,6 +3394,8 @@ export async function buildRunUatPrompt(
3392
3394
  uatPath,
3393
3395
  uatResultPath,
3394
3396
  uatType,
3397
+ toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
3398
+ canonicalPresentation,
3395
3399
  inlinedContext,
3396
3400
  skillActivation: buildSkillActivationBlock({
3397
3401
  base,
@@ -54,6 +54,7 @@ import { isGsdWorktreePath } from "./worktree-root.js";
54
54
  import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
55
55
  import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
56
56
  import { loadAllCaptures, loadPendingCaptures } from "./captures.js";
57
+ import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
57
58
 
58
59
  // Re-export so existing consumers of auto-recovery.ts keep working.
59
60
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
@@ -626,9 +627,8 @@ export function verifyExpectedArtifact(
626
627
  if (summaryOutcome === "failure") return false;
627
628
  const { milestone: mid } = parseUnitId(unitId);
628
629
  if (mid && isDbAvailable()) {
629
- const dbMilestone = getMilestone(mid);
630
- if (!dbMilestone) return false;
631
- if (!isClosedStatus(dbMilestone.status) && summaryOutcome !== "success") return false;
630
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
631
+ if (!closeoutGate.ok) return false;
632
632
  }
633
633
  if (hasImplementationArtifacts(base, mid) === "absent") return false;
634
634
  }
@@ -1,59 +1,13 @@
1
1
  import { parseUnitId } from "./unit-id.js";
2
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "./tool-presentation-plan.js";
3
-
4
- export const RUN_UAT_BROWSER_TOOL_NAMES = [
5
- "browser_navigate",
6
- "browser_click",
7
- "browser_type",
8
- "browser_fill_form",
9
- "browser_click_ref",
10
- "browser_fill_ref",
11
- "browser_wait_for",
12
- "browser_assert",
13
- "browser_verify",
14
- "browser_screenshot",
15
- "browser_snapshot_refs",
16
- "browser_find",
17
- "browser_get_console_logs",
18
- "browser_get_network_logs",
19
- "browser_evaluate",
20
- "browser_reload",
21
- "browser_batch",
22
- "browser_act",
23
- ] as const;
2
+ import {
3
+ AUTO_UNIT_SCOPED_TOOLS,
4
+ getForbiddenGsdToolReason,
5
+ } from "./unit-tool-contracts.js";
24
6
 
25
- export const AUTO_UNIT_SCOPED_TOOLS: Record<string, readonly string[]> = {
26
- "research-milestone": ["gsd_summary_save", "gsd_decision_save"],
27
- "plan-milestone": ["gsd_plan_milestone", "gsd_decision_save", "gsd_requirement_update"],
28
- "discuss-milestone": [
29
- "gsd_summary_save",
30
- "gsd_decision_save",
31
- "gsd_requirement_save",
32
- "gsd_requirement_update",
33
- "gsd_plan_milestone",
34
- "gsd_milestone_generate_id",
35
- ],
36
- "discuss-slice": ["gsd_summary_save", "gsd_decision_save"],
37
- "validate-milestone": ["gsd_validate_milestone", "gsd_reassess_roadmap", "subagent"],
38
- "complete-milestone": ["gsd_complete_milestone", "subagent"],
39
- "research-slice": ["gsd_summary_save", "gsd_decision_save"],
40
- "plan-slice": ["gsd_plan_slice", "gsd_plan_task", "gsd_decision_save"],
41
- "refine-slice": ["gsd_plan_slice", "gsd_plan_task", "gsd_decision_save"],
42
- "replan-slice": ["gsd_replan_slice", "gsd_plan_task", "gsd_decision_save"],
43
- "complete-slice": ["gsd_slice_complete", "gsd_task_reopen", "gsd_replan_slice", "gsd_decision_save", "gsd_requirement_update", "subagent"],
44
- "reassess-roadmap": ["gsd_reassess_roadmap"],
45
- "execute-task": ["gsd_task_complete", "gsd_decision_save"],
46
- "execute-task-simple": ["gsd_task_complete", "gsd_decision_save"],
47
- "reactive-execute": ["gsd_task_complete", "gsd_decision_save"],
48
- "run-uat": [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES],
49
- "gate-evaluate": ["gsd_save_gate_result"],
50
- "rewrite-docs": ["gsd_summary_save", "gsd_decision_save"],
51
- "workflow-preferences": ["gsd_summary_save"],
52
- "discuss-project": ["gsd_summary_save", "gsd_decision_save", "gsd_requirement_save"],
53
- "discuss-requirements": ["gsd_requirement_save", "gsd_summary_save"],
54
- "research-decision": ["gsd_summary_save"],
55
- "research-project": ["gsd_summary_save", "gsd_decision_save"],
56
- };
7
+ export {
8
+ AUTO_UNIT_SCOPED_TOOLS,
9
+ RUN_UAT_BROWSER_TOOL_NAMES,
10
+ } from "./unit-tool-contracts.js";
57
11
 
58
12
  const WORKFLOW_TOOL_ALIASES: Record<string, string> = {
59
13
  gsd_save_decision: "gsd_decision_save",
@@ -97,6 +51,14 @@ const SCOPED_GSD_LIFECYCLE_TOOLS = new Set(
97
51
  .map(canonicalWorkflowToolName),
98
52
  );
99
53
 
54
+ export const GSD_PHASE_SCOPE_DISPLAY_REASON = "This GSD phase only allows its scoped workflow tools.";
55
+
56
+ type AutoUnitToolScopeResult = {
57
+ block: boolean;
58
+ reason?: string;
59
+ displayReason?: string;
60
+ };
61
+
100
62
  function stripMcpToolPrefix(toolName: string): string {
101
63
  if (!toolName.startsWith("mcp__")) return toolName;
102
64
  const toolSeparator = toolName.indexOf("__", "mcp__".length);
@@ -114,12 +76,20 @@ export function isWorkflowAliasTool(toolName: string): boolean {
114
76
 
115
77
  function hardBlockReason(unitType: string, what: string): string {
116
78
  return [
117
- `HARD BLOCK: unit "${unitType}" is constrained by auto-unit tool scope — ${what}.`,
79
+ `HARD BLOCK: Tool Contract failure for unit "${unitType}" — ${what}.`,
118
80
  "This is a mechanical phase-boundary gate. You MUST NOT proceed, retry the same call,",
119
81
  "or route around this block; the orchestrator owns phase transitions.",
120
82
  ].join(" ");
121
83
  }
122
84
 
85
+ function hardBlock(unitType: string, what: string): AutoUnitToolScopeResult {
86
+ return {
87
+ block: true,
88
+ reason: hardBlockReason(unitType, what),
89
+ displayReason: GSD_PHASE_SCOPE_DISPLAY_REASON,
90
+ };
91
+ }
92
+
123
93
  function allowedGsdToolsForUnit(unitType: string): string[] {
124
94
  return [...new Set(
125
95
  (AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [])
@@ -144,7 +114,7 @@ function shouldBlockTaskCompletionScope(
144
114
  unitId: string | undefined,
145
115
  toolName: string,
146
116
  input: unknown,
147
- ): { block: boolean; reason?: string } {
117
+ ): AutoUnitToolScopeResult {
148
118
  if (!EXECUTE_TASK_UNIT_TYPES.has(unitType)) return { block: false };
149
119
  if (canonicalWorkflowToolName(toolName) !== "gsd_task_complete") return { block: false };
150
120
  if (!unitId) return { block: false };
@@ -165,13 +135,10 @@ function shouldBlockTaskCompletionScope(
165
135
  return { block: false };
166
136
  }
167
137
 
168
- return {
169
- block: true,
170
- reason: hardBlockReason(
171
- unitType,
172
- `gsd_task_complete may only complete the active task ${expected.milestone}/${expected.slice}/${expected.task}; requested ${actualMilestone}/${actualSlice}/${actualTask}`,
173
- ),
174
- };
138
+ return hardBlock(
139
+ unitType,
140
+ `gsd_task_complete may only complete the active task ${expected.milestone}/${expected.slice}/${expected.task}; requested ${actualMilestone}/${actualSlice}/${actualTask}`,
141
+ );
175
142
  }
176
143
 
177
144
  export function shouldBlockAutoUnitToolCall(
@@ -179,15 +146,12 @@ export function shouldBlockAutoUnitToolCall(
179
146
  toolName: string,
180
147
  input?: unknown,
181
148
  unitId?: string,
182
- ): { block: boolean; reason?: string } {
149
+ ): AutoUnitToolScopeResult {
183
150
  const scopedTools = AUTO_UNIT_SCOPED_TOOLS[unitType];
184
151
  if (!scopedTools) return { block: false };
185
152
 
186
153
  if (isNativeWorkflowTool(toolName)) {
187
- return {
188
- block: true,
189
- reason: hardBlockReason(unitType, "native Workflow is not permitted inside a dispatched GSD auto-mode unit"),
190
- };
154
+ return hardBlock(unitType, "native Workflow is not permitted inside a dispatched GSD auto-mode unit");
191
155
  }
192
156
 
193
157
  const taskScope = shouldBlockTaskCompletionScope(unitType, unitId, toolName, input);
@@ -199,11 +163,16 @@ export function shouldBlockAutoUnitToolCall(
199
163
  const allowedTools = allowedGsdToolsForUnit(unitType);
200
164
  if (allowedTools.includes(canonicalTool)) return { block: false };
201
165
 
202
- return {
203
- block: true,
204
- reason: hardBlockReason(
166
+ const forbiddenReason = getForbiddenGsdToolReason(unitType, canonicalTool);
167
+ if (forbiddenReason) {
168
+ return hardBlock(
205
169
  unitType,
206
- `GSD lifecycle tool "${canonicalTool}" is not permitted; allowed GSD tools: ${allowedTools.length > 0 ? allowedTools.join(", ") : "(none)"}`,
207
- ),
208
- };
170
+ `GSD lifecycle tool "${canonicalTool}" is not permitted; ${forbiddenReason} Fix unit-tool-contracts.ts or the ${unitType} prompt.`,
171
+ );
172
+ }
173
+
174
+ return hardBlock(
175
+ unitType,
176
+ `GSD lifecycle tool "${canonicalTool}" is not permitted; allowed GSD tools: ${allowedTools.length > 0 ? allowedTools.join(", ") : "(none)"}`,
177
+ );
209
178
  }
@@ -83,6 +83,11 @@ import {
83
83
  nativeWorktreeList,
84
84
  nativeLsFiles,
85
85
  } from "./native-git-bridge.js";
86
+ import {
87
+ CLOSEOUT_CONSISTENCY_BLOCKED_REASON,
88
+ checkCloseoutConsistencyGate,
89
+ formatCloseoutConsistencyBlock,
90
+ } from "./closeout-consistency-gate.js";
86
91
  import { gsdHome } from "./gsd-home.js";
87
92
  import { type MilestoneScope, type GsdWorkspace, createWorkspace } from "./workspace.js";
88
93
  import {
@@ -1771,16 +1776,29 @@ export function mergeMilestoneToMain(
1771
1776
  // symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
1772
1777
  // database (#2823).
1773
1778
  if (isDbAvailable()) {
1779
+ const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1780
+ const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1781
+ const mainDbPath = contract.projectDb;
1774
1782
  try {
1775
- const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1776
- const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1777
- const mainDbPath = contract.projectDb;
1783
+ const activeDbPath = getDbPath();
1784
+ if (activeDbPath && _shouldReconcileWorktreeDb(activeDbPath, mainDbPath)) {
1785
+ closeDatabase();
1786
+ if (!openDatabase(mainDbPath)) {
1787
+ throw new Error(`cannot open project DB at ${mainDbPath}`);
1788
+ }
1789
+ }
1778
1790
  if (_shouldReconcileWorktreeDb(worktreeDbPath, mainDbPath)) {
1779
1791
  reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1780
1792
  }
1781
1793
  } catch (err) {
1782
- /* non-fatal */
1783
- logError("worktree", `DB reconciliation failed: ${err instanceof Error ? err.message : String(err)}`);
1794
+ const message = `DB reconciliation failed before milestone ${milestoneId} merge: ${err instanceof Error ? err.message : String(err)}`;
1795
+ logError("worktree", message);
1796
+ throw new GSDError(GSD_GIT_ERROR, `${message}. Recovery reason: ${CLOSEOUT_CONSISTENCY_BLOCKED_REASON}.`);
1797
+ }
1798
+
1799
+ const closeoutGate = checkCloseoutConsistencyGate(milestoneId);
1800
+ if (!closeoutGate.ok) {
1801
+ throw new GSDError(GSD_GIT_ERROR, formatCloseoutConsistencyBlock(closeoutGate));
1784
1802
  }
1785
1803
  }
1786
1804
 
@@ -78,6 +78,9 @@ function readDetails(result: any): any {
78
78
 
79
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- result shape varies by tool
80
80
  function formatToolErrorText(result: any, details: any): string {
81
+ if (typeof details?.displayReason === "string" && details.displayReason) {
82
+ return details.displayReason;
83
+ }
81
84
  const message = details?.error
82
85
  ?? result?.content?.find((entry: { type?: string; text?: string }) => entry.type === "text")?.text
83
86
  ?? "unknown";
@@ -424,6 +427,9 @@ export function registerDbTools(pi: ExtensionAPI): void {
424
427
  kind: StringEnum(["gsd_uat_exec", "gsd_exec", "screenshot", "log", "url", "browser"], { description: "Evidence kind" }),
425
428
  ref: Type.String({ description: "Evidence ID, approved .gsd path, or URL" }),
426
429
  note: Type.Optional(Type.String({ description: "Short evidence note" })),
430
+ unitType: Type.Optional(Type.String({ description: "Unit that produced the evidence" })),
431
+ tool: Type.Optional(Type.String({ description: "Tool that produced the evidence" })),
432
+ executionId: Type.Optional(Type.String({ description: "Stable execution or artifact id" })),
427
433
  });
428
434
 
429
435
  const uatCheck = Type.Object({
@@ -437,17 +443,17 @@ export function registerDbTools(pi: ExtensionAPI): void {
437
443
  });
438
444
 
439
445
  const toolPresentationBlock = Type.Object({
440
- surface: StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" }),
446
+ surface: Type.Optional(StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" })),
441
447
  model: Type.Optional(Type.Object({
442
448
  provider: Type.Optional(Type.String()),
443
449
  api: Type.Optional(Type.String()),
444
450
  id: Type.Optional(Type.String()),
445
451
  })),
446
- presentedTools: Type.Array(Type.String(), { description: "Tool names actually presented to the model" }),
447
- blockedTools: Type.Array(Type.Object({
452
+ presentedTools: Type.Optional(Type.Array(Type.String(), { description: "Tool names actually presented to the model" })),
453
+ blockedTools: Type.Optional(Type.Array(Type.Object({
448
454
  name: Type.String(),
449
455
  reason: Type.String(),
450
- }), { description: "Tool names blocked from the model with reasons" }),
456
+ }), { description: "Tool names blocked from the model with reasons" })),
451
457
  aliases: Type.Optional(Type.Array(Type.Object({
452
458
  requested: Type.String(),
453
459
  canonical: Type.String(),
@@ -471,12 +477,12 @@ export function registerDbTools(pi: ExtensionAPI): void {
471
477
  "Do not use raw gsd_summary_save as a substitute for UAT results.",
472
478
  ],
473
479
  parameters: Type.Object({
474
- milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
475
- sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
476
- uatType: StringEnum(["artifact-driven", "browser-executable", "runtime-executable", "live-runtime", "mixed", "human-experience"], { description: "Declared UAT mode" }),
477
- verdict: StringEnum(["PASS", "FAIL", "PARTIAL"], { description: "Overall UAT verdict" }),
478
- checks: Type.Array(uatCheck, { description: "Structured check results" }),
479
- presentation: toolPresentationBlock,
480
+ milestoneId: Type.Optional(Type.String({ description: "Milestone ID (e.g. M001)" })),
481
+ sliceId: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
482
+ uatType: Type.Optional(Type.String({ description: "Declared UAT mode" })),
483
+ verdict: Type.Optional(Type.String({ description: "Overall UAT verdict: PASS, FAIL, or PARTIAL" })),
484
+ checks: Type.Optional(Type.Array(uatCheck, { description: "Structured check results" })),
485
+ presentation: Type.Optional(toolPresentationBlock),
480
486
  notes: Type.Optional(Type.String({ description: "Overall verdict rationale" })),
481
487
  attempt: Type.Optional(Type.String({ description: "Attempt number or auto" })),
482
488
  previousAttemptId: Type.Optional(Type.String({ description: "Prior attempt ID, when retrying" })),
@@ -38,7 +38,7 @@ import { getGuidedUnitContext } from "../guided-unit-context.js";
38
38
  import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
39
39
  import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
40
40
  import { filterToolsForProvider } from "../model-router.js";
41
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
41
+ import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
42
42
 
43
43
  let approvalQuestionAbortInFlight = false;
44
44
 
@@ -252,7 +252,12 @@ export function buildRunUatGsdToolSet(
252
252
  ): string[] {
253
253
  const scoped = resolveScopedToolNames(
254
254
  [...activeToolNames, ...registeredToolNames],
255
- [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES],
255
+ [
256
+ ...RUN_UAT_WORKFLOW_TOOL_NAMES,
257
+ ...RUN_UAT_READ_ONLY_TOOL_NAMES,
258
+ "subagent",
259
+ ...RUN_UAT_BROWSER_TOOL_NAMES,
260
+ ],
256
261
  );
257
262
  return [...new Set(scoped)];
258
263
  }
@@ -480,22 +485,30 @@ function isContextDraftSummarySave(toolName: string, input: unknown): boolean {
480
485
  return (input as { artifact_type?: unknown }).artifact_type === "CONTEXT-DRAFT";
481
486
  }
482
487
 
488
+ function withDepthGateDisplayReason<T extends { block: boolean; reason?: string }>(
489
+ result: T,
490
+ displayReason = "Depth confirmation is waiting for your answer.",
491
+ ): T & { displayReason?: string } {
492
+ if (!result.block) return result;
493
+ return { ...result, displayReason };
494
+ }
495
+
483
496
  function shouldBlockDeferredApprovalTool(
484
497
  toolName: string,
485
498
  input: unknown,
486
499
  basePath: string,
487
- ): { block: boolean; reason?: string } {
500
+ ): { block: boolean; reason?: string; displayReason?: string } {
488
501
  if (deferredApprovalGate?.basePath !== basePath) return { block: false };
489
502
  if (toolName === "ask_user_questions") return { block: false };
490
503
  if (isContextDraftSummarySave(toolName, input)) return { block: false };
491
- return {
504
+ return withDepthGateDisplayReason({
492
505
  block: true,
493
506
  reason: [
494
507
  `HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
495
508
  `Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
496
509
  `Wait for the user's answer before calling additional tools.`,
497
510
  ].join(" "),
498
- };
511
+ });
499
512
  }
500
513
 
501
514
  export function resolveNotificationStoreBasePath(basePath: string): string {
@@ -917,7 +930,7 @@ export function registerHooks(
917
930
  "Depth confirmation is waiting for your answer — pausing auto-mode.",
918
931
  );
919
932
  }
920
- return bashGuard;
933
+ return withDepthGateDisplayReason(bashGuard);
921
934
  }
922
935
  } else {
923
936
  const gateGuard = shouldBlockPendingGate(
@@ -935,7 +948,7 @@ export function registerHooks(
935
948
  "Depth confirmation is waiting for your answer — pausing auto-mode.",
936
949
  );
937
950
  }
938
- return gateGuard;
951
+ return withDepthGateDisplayReason(gateGuard);
939
952
  }
940
953
  }
941
954
  }
@@ -1040,7 +1053,9 @@ export function registerHooks(
1040
1053
  isQueuePhaseActive(discussionBasePath),
1041
1054
  discussionBasePath,
1042
1055
  );
1043
- if (result.block) return result;
1056
+ if (result.block) {
1057
+ return withDepthGateDisplayReason(result, "Depth check required before writing milestone context.");
1058
+ }
1044
1059
  });
1045
1060
 
1046
1061
  // ── Safety harness: evidence collection + destructive command blocking ──
@@ -4,7 +4,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
 
5
5
  import { minimatch } from "minimatch";
6
6
 
7
- import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
7
+ import { GSD_PHASE_SCOPE_DISPLAY_REASON, shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
8
8
  import { getIsolationMode } from "../preferences.js";
9
9
  import { compileSubagentPermissionContract, type ToolsPolicy } from "../unit-context-manifest.js";
10
10
  import { logWarning } from "../workflow-logger.js";
@@ -772,6 +772,20 @@ function blockReason(unitType: string, mode: string, what: string): string {
772
772
  ].join(" ");
773
773
  }
774
774
 
775
+ function planningBlock(unitType: string, mode: string, what: string): PlanningUnitBlockResult {
776
+ return {
777
+ block: true,
778
+ reason: blockReason(unitType, mode, what),
779
+ displayReason: GSD_PHASE_SCOPE_DISPLAY_REASON,
780
+ };
781
+ }
782
+
783
+ type PlanningUnitBlockResult = {
784
+ block: boolean;
785
+ reason?: string;
786
+ displayReason?: string;
787
+ };
788
+
775
789
  /**
776
790
  * Planning-unit tool-policy enforcement. Returns { block } per the policy
777
791
  * resolved from the active unit's manifest:
@@ -812,7 +826,7 @@ export function shouldBlockPlanningUnit(
812
826
  agentClasses?: readonly string[],
813
827
  toolInput?: unknown,
814
828
  unitId?: string,
815
- ): { block: boolean; reason?: string } {
829
+ ): PlanningUnitBlockResult {
816
830
  const tool = canonicalToolName(toolName);
817
831
  const autoScopeGuard = shouldBlockAutoUnitToolCall(unitType, toolName, toolInput, unitId);
818
832
  if (autoScopeGuard.block) return autoScopeGuard;
@@ -825,10 +839,10 @@ export function shouldBlockPlanningUnit(
825
839
  if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
826
840
  if (tool.startsWith("gsd_")) return { block: false };
827
841
  if (PLANNING_WRITE_TOOLS.has(tool) || tool === "bash" || PLANNING_SUBAGENT_TOOLS.has(tool)) {
828
- return { block: true, reason: blockReason(unitType, policy.mode, `${tool} is not permitted (read-only)`) };
842
+ return planningBlock(unitType, policy.mode, `${tool} is not permitted (read-only)`);
829
843
  }
830
844
  // Unknown tool in read-only mode — block by default.
831
- return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
845
+ return planningBlock(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`);
832
846
  }
833
847
 
834
848
  // planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
@@ -846,14 +860,11 @@ export function shouldBlockPlanningUnit(
846
860
  // instead of silently bypassing the gate.
847
861
  if (agentClasses === undefined) {
848
862
  warnMissingControlledDispatchAgentClasses(unitType, policy.mode, tool);
849
- return {
850
- block: true,
851
- reason: blockReason(
852
- unitType,
853
- policy.mode,
854
- `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`,
855
- ),
856
- };
863
+ return planningBlock(
864
+ unitType,
865
+ policy.mode,
866
+ `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`,
867
+ );
857
868
  }
858
869
  // agentClasses was explicitly provided but resolved to an empty list (for
859
870
  // example, a bare tool call with no agent field). Pass through; no agents
@@ -863,57 +874,45 @@ export function shouldBlockPlanningUnit(
863
874
  }
864
875
  const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
865
876
  if (globallyDisallowed) {
866
- return {
867
- block: true,
868
- reason: blockReason(
869
- unitType,
870
- policy.mode,
871
- `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`,
872
- ),
873
- };
877
+ return planningBlock(
878
+ unitType,
879
+ policy.mode,
880
+ `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`,
881
+ );
874
882
  }
875
883
  const disallowedByPolicy = requested.find(a => !allowed.has(a));
876
884
  if (disallowedByPolicy) {
877
- return {
878
- block: true,
879
- reason: blockReason(
880
- unitType,
881
- policy.mode,
882
- `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`,
883
- ),
884
- };
885
+ return planningBlock(
886
+ unitType,
887
+ policy.mode,
888
+ `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`,
889
+ );
885
890
  }
886
891
  return { block: false };
887
892
  }
888
- return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
893
+ return planningBlock(unitType, policy.mode, "subagent dispatch is not permitted in planning units");
889
894
  }
890
895
 
891
896
  if (tool === "bash") {
892
897
  if (policy.mode === "verification") {
893
898
  if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
894
- return {
895
- block: true,
896
- reason: blockReason(
897
- unitType,
898
- policy.mode,
899
- `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
900
- ),
901
- };
902
- }
903
- if (BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
904
- return {
905
- block: true,
906
- reason: blockReason(
899
+ return planningBlock(
907
900
  unitType,
908
901
  policy.mode,
909
- `bash is restricted to read-only commands (cat/grep/git log/etc); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
910
- ),
911
- };
902
+ `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
903
+ );
904
+ }
905
+ if (BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
906
+ return planningBlock(
907
+ unitType,
908
+ policy.mode,
909
+ `bash is restricted to read-only commands (cat/grep/git log/etc); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
910
+ );
912
911
  }
913
912
 
914
913
  if (PLANNING_WRITE_TOOLS.has(tool)) {
915
914
  if (!pathOrCommand) {
916
- return { block: true, reason: blockReason(unitType, policy.mode, `${tool} called with empty path`) };
915
+ return planningBlock(unitType, policy.mode, `${tool} called with empty path`);
917
916
  }
918
917
  const absPath = isAbsolute(pathOrCommand) ? pathOrCommand : resolve(basePath, pathOrCommand);
919
918
 
@@ -925,14 +924,11 @@ export function shouldBlockPlanningUnit(
925
924
  return { block: false };
926
925
  }
927
926
 
928
- return {
929
- block: true,
930
- reason: blockReason(
931
- unitType,
932
- policy.mode,
933
- `cannot ${tool} "${pathOrCommand}" — writes are restricted to .gsd/${policy.mode === "docs" ? " and " + policy.allowedPathGlobs.join(", ") : ""}`,
934
- ),
935
- };
927
+ return planningBlock(
928
+ unitType,
929
+ policy.mode,
930
+ `cannot ${tool} "${pathOrCommand}" — writes are restricted to .gsd/${policy.mode === "docs" ? " and " + policy.allowedPathGlobs.join(", ") : ""}`,
931
+ );
936
932
  }
937
933
 
938
934
  // Unknown tool name — pass through. Other layers (queue, pending-gate,