@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
@@ -1 +1 @@
1
- ba1c57462cf67a0e
1
+ fe5e2f79a21b7d4e
@@ -35,6 +35,7 @@ import { probeGitConflictState } from "./git-conflict-state.js";
35
35
  import { runTurnGitAction } from "./git-service.js";
36
36
  import { parseUnitId } from "./unit-id.js";
37
37
  import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
38
+ import { checkCloseoutConsistencyGate, formatCloseoutConsistencyBlock, } from "./closeout-consistency-gate.js";
38
39
  function resolveExistingExpectedArtifact(unitType, unitId, basePath) {
39
40
  const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
40
41
  return artifactPath && existsSync(artifactPath) ? artifactPath : null;
@@ -1355,6 +1356,16 @@ export const DISPATCH_RULES = [
1355
1356
  prompt: await buildCompleteMilestonePrompt(mid, midTitle, basePath),
1356
1357
  };
1357
1358
  }
1359
+ if (milestone) {
1360
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
1361
+ if (!closeoutGate.ok) {
1362
+ return {
1363
+ action: "stop",
1364
+ reason: formatCloseoutConsistencyBlock(closeoutGate),
1365
+ level: "warning",
1366
+ };
1367
+ }
1368
+ }
1358
1369
  }
1359
1370
  return {
1360
1371
  action: "stop",
@@ -31,6 +31,7 @@ import { hasBrowserRequiredText } from "./browser-evidence.js";
31
31
  import { debugLog } from "./debug-logger.js";
32
32
  import { buildSkillActivationBlock, buildSkillDiscoveryVars } from "./skill-activation.js";
33
33
  import { findMilestoneIds } from "./milestone-ids.js";
34
+ import { buildRunUatResultPresentation, RUN_UAT_TOOL_PRESENTATION_PLAN_ID } from "./tool-presentation-plan.js";
34
35
  export { buildSkillActivationBlock, buildSkillDiscoveryVars };
35
36
  // ─── Preamble Cap ─────────────────────────────────────────────────────────────
36
37
  /**
@@ -2939,6 +2940,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
2939
2940
  emitPromptContextTelemetry("run-uat", contextTelemetry, inlinedContext);
2940
2941
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "ASSESSMENT"));
2941
2942
  const uatType = resolveEffectiveUatType(uatContent);
2943
+ const canonicalPresentation = JSON.stringify(buildRunUatResultPresentation(), null, 2);
2942
2944
  return loadPrompt("run-uat", {
2943
2945
  workingDirectory: base,
2944
2946
  milestoneId: mid,
@@ -2946,6 +2948,8 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
2946
2948
  uatPath,
2947
2949
  uatResultPath,
2948
2950
  uatType,
2951
+ toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
2952
+ canonicalPresentation,
2949
2953
  inlinedContext,
2950
2954
  skillActivation: buildSkillActivationBlock({
2951
2955
  base,
@@ -32,6 +32,7 @@ import { isGsdWorktreePath } from "./worktree-root.js";
32
32
  import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
33
33
  import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
34
34
  import { loadAllCaptures, loadPendingCaptures } from "./captures.js";
35
+ import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
35
36
  // Re-export so existing consumers of auto-recovery.ts keep working.
36
37
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
37
38
  export { classifyMilestoneSummaryContent, } from "./milestone-summary-classifier.js";
@@ -571,10 +572,8 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
571
572
  return false;
572
573
  const { milestone: mid } = parseUnitId(unitId);
573
574
  if (mid && isDbAvailable()) {
574
- const dbMilestone = getMilestone(mid);
575
- if (!dbMilestone)
576
- return false;
577
- if (!isClosedStatus(dbMilestone.status) && summaryOutcome !== "success")
575
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
576
+ if (!closeoutGate.ok)
578
577
  return false;
579
578
  }
580
579
  if (hasImplementationArtifacts(base, mid) === "absent")
@@ -1,57 +1,6 @@
1
1
  import { parseUnitId } from "./unit-id.js";
2
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "./tool-presentation-plan.js";
3
- export const RUN_UAT_BROWSER_TOOL_NAMES = [
4
- "browser_navigate",
5
- "browser_click",
6
- "browser_type",
7
- "browser_fill_form",
8
- "browser_click_ref",
9
- "browser_fill_ref",
10
- "browser_wait_for",
11
- "browser_assert",
12
- "browser_verify",
13
- "browser_screenshot",
14
- "browser_snapshot_refs",
15
- "browser_find",
16
- "browser_get_console_logs",
17
- "browser_get_network_logs",
18
- "browser_evaluate",
19
- "browser_reload",
20
- "browser_batch",
21
- "browser_act",
22
- ];
23
- export const AUTO_UNIT_SCOPED_TOOLS = {
24
- "research-milestone": ["gsd_summary_save", "gsd_decision_save"],
25
- "plan-milestone": ["gsd_plan_milestone", "gsd_decision_save", "gsd_requirement_update"],
26
- "discuss-milestone": [
27
- "gsd_summary_save",
28
- "gsd_decision_save",
29
- "gsd_requirement_save",
30
- "gsd_requirement_update",
31
- "gsd_plan_milestone",
32
- "gsd_milestone_generate_id",
33
- ],
34
- "discuss-slice": ["gsd_summary_save", "gsd_decision_save"],
35
- "validate-milestone": ["gsd_validate_milestone", "gsd_reassess_roadmap", "subagent"],
36
- "complete-milestone": ["gsd_complete_milestone", "subagent"],
37
- "research-slice": ["gsd_summary_save", "gsd_decision_save"],
38
- "plan-slice": ["gsd_plan_slice", "gsd_plan_task", "gsd_decision_save"],
39
- "refine-slice": ["gsd_plan_slice", "gsd_plan_task", "gsd_decision_save"],
40
- "replan-slice": ["gsd_replan_slice", "gsd_plan_task", "gsd_decision_save"],
41
- "complete-slice": ["gsd_slice_complete", "gsd_task_reopen", "gsd_replan_slice", "gsd_decision_save", "gsd_requirement_update", "subagent"],
42
- "reassess-roadmap": ["gsd_reassess_roadmap"],
43
- "execute-task": ["gsd_task_complete", "gsd_decision_save"],
44
- "execute-task-simple": ["gsd_task_complete", "gsd_decision_save"],
45
- "reactive-execute": ["gsd_task_complete", "gsd_decision_save"],
46
- "run-uat": [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES],
47
- "gate-evaluate": ["gsd_save_gate_result"],
48
- "rewrite-docs": ["gsd_summary_save", "gsd_decision_save"],
49
- "workflow-preferences": ["gsd_summary_save"],
50
- "discuss-project": ["gsd_summary_save", "gsd_decision_save", "gsd_requirement_save"],
51
- "discuss-requirements": ["gsd_requirement_save", "gsd_summary_save"],
52
- "research-decision": ["gsd_summary_save"],
53
- "research-project": ["gsd_summary_save", "gsd_decision_save"],
54
- };
2
+ import { AUTO_UNIT_SCOPED_TOOLS, getForbiddenGsdToolReason, } from "./unit-tool-contracts.js";
3
+ export { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, } from "./unit-tool-contracts.js";
55
4
  const WORKFLOW_TOOL_ALIASES = {
56
5
  gsd_save_decision: "gsd_decision_save",
57
6
  gsd_update_requirement: "gsd_requirement_update",
@@ -88,6 +37,7 @@ const SCOPED_GSD_LIFECYCLE_TOOLS = new Set([
88
37
  ]
89
38
  .filter((tool) => tool.startsWith("gsd_"))
90
39
  .map(canonicalWorkflowToolName));
40
+ export const GSD_PHASE_SCOPE_DISPLAY_REASON = "This GSD phase only allows its scoped workflow tools.";
91
41
  function stripMcpToolPrefix(toolName) {
92
42
  if (!toolName.startsWith("mcp__"))
93
43
  return toolName;
@@ -103,11 +53,18 @@ export function isWorkflowAliasTool(toolName) {
103
53
  }
104
54
  function hardBlockReason(unitType, what) {
105
55
  return [
106
- `HARD BLOCK: unit "${unitType}" is constrained by auto-unit tool scope — ${what}.`,
56
+ `HARD BLOCK: Tool Contract failure for unit "${unitType}" — ${what}.`,
107
57
  "This is a mechanical phase-boundary gate. You MUST NOT proceed, retry the same call,",
108
58
  "or route around this block; the orchestrator owns phase transitions.",
109
59
  ].join(" ");
110
60
  }
61
+ function hardBlock(unitType, what) {
62
+ return {
63
+ block: true,
64
+ reason: hardBlockReason(unitType, what),
65
+ displayReason: GSD_PHASE_SCOPE_DISPLAY_REASON,
66
+ };
67
+ }
111
68
  function allowedGsdToolsForUnit(unitType) {
112
69
  return [...new Set((AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [])
113
70
  .filter((tool) => tool.startsWith("gsd_"))
@@ -143,20 +100,14 @@ function shouldBlockTaskCompletionScope(unitType, unitId, toolName, input) {
143
100
  actualTask === expected.task) {
144
101
  return { block: false };
145
102
  }
146
- return {
147
- block: true,
148
- reason: hardBlockReason(unitType, `gsd_task_complete may only complete the active task ${expected.milestone}/${expected.slice}/${expected.task}; requested ${actualMilestone}/${actualSlice}/${actualTask}`),
149
- };
103
+ return hardBlock(unitType, `gsd_task_complete may only complete the active task ${expected.milestone}/${expected.slice}/${expected.task}; requested ${actualMilestone}/${actualSlice}/${actualTask}`);
150
104
  }
151
105
  export function shouldBlockAutoUnitToolCall(unitType, toolName, input, unitId) {
152
106
  const scopedTools = AUTO_UNIT_SCOPED_TOOLS[unitType];
153
107
  if (!scopedTools)
154
108
  return { block: false };
155
109
  if (isNativeWorkflowTool(toolName)) {
156
- return {
157
- block: true,
158
- reason: hardBlockReason(unitType, "native Workflow is not permitted inside a dispatched GSD auto-mode unit"),
159
- };
110
+ return hardBlock(unitType, "native Workflow is not permitted inside a dispatched GSD auto-mode unit");
160
111
  }
161
112
  const taskScope = shouldBlockTaskCompletionScope(unitType, unitId, toolName, input);
162
113
  if (taskScope.block)
@@ -167,8 +118,9 @@ export function shouldBlockAutoUnitToolCall(unitType, toolName, input, unitId) {
167
118
  const allowedTools = allowedGsdToolsForUnit(unitType);
168
119
  if (allowedTools.includes(canonicalTool))
169
120
  return { block: false };
170
- return {
171
- block: true,
172
- reason: hardBlockReason(unitType, `GSD lifecycle tool "${canonicalTool}" is not permitted; allowed GSD tools: ${allowedTools.length > 0 ? allowedTools.join(", ") : "(none)"}`),
173
- };
121
+ const forbiddenReason = getForbiddenGsdToolReason(unitType, canonicalTool);
122
+ if (forbiddenReason) {
123
+ return hardBlock(unitType, `GSD lifecycle tool "${canonicalTool}" is not permitted; ${forbiddenReason} Fix unit-tool-contracts.ts or the ${unitType} prompt.`);
124
+ }
125
+ return hardBlock(unitType, `GSD lifecycle tool "${canonicalTool}" is not permitted; allowed GSD tools: ${allowedTools.length > 0 ? allowedTools.join(", ") : "(none)"}`);
174
126
  }
@@ -26,6 +26,7 @@ import { MILESTONE_ID_RE } from "./milestone-ids.js";
26
26
  import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
27
27
  import { classifyProject } from "./detection.js";
28
28
  import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchForceReset, nativeBranchExists, nativeDiffNumstat, nativeUpdateRef, nativeIsAncestor, nativeMergeAbort, nativeWorktreeList, nativeLsFiles, } from "./native-git-bridge.js";
29
+ import { CLOSEOUT_CONSISTENCY_BLOCKED_REASON, checkCloseoutConsistencyGate, formatCloseoutConsistencyBlock, } from "./closeout-consistency-gate.js";
29
30
  import { gsdHome } from "./gsd-home.js";
30
31
  import { createWorkspace } from "./workspace.js";
31
32
  import { _finalizeProjectionForMergeImpl, _projectRootToWorktreeImpl, _projectWorktreeToRootImpl, } from "./worktree-state-projection.js";
@@ -1513,17 +1514,29 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1513
1514
  // symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
1514
1515
  // database (#2823).
1515
1516
  if (isDbAvailable()) {
1517
+ const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1518
+ const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1519
+ const mainDbPath = contract.projectDb;
1516
1520
  try {
1517
- const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1518
- const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1519
- const mainDbPath = contract.projectDb;
1521
+ const activeDbPath = getDbPath();
1522
+ if (activeDbPath && _shouldReconcileWorktreeDb(activeDbPath, mainDbPath)) {
1523
+ closeDatabase();
1524
+ if (!openDatabase(mainDbPath)) {
1525
+ throw new Error(`cannot open project DB at ${mainDbPath}`);
1526
+ }
1527
+ }
1520
1528
  if (_shouldReconcileWorktreeDb(worktreeDbPath, mainDbPath)) {
1521
1529
  reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1522
1530
  }
1523
1531
  }
1524
1532
  catch (err) {
1525
- /* non-fatal */
1526
- logError("worktree", `DB reconciliation failed: ${err instanceof Error ? err.message : String(err)}`);
1533
+ const message = `DB reconciliation failed before milestone ${milestoneId} merge: ${err instanceof Error ? err.message : String(err)}`;
1534
+ logError("worktree", message);
1535
+ throw new GSDError(GSD_GIT_ERROR, `${message}. Recovery reason: ${CLOSEOUT_CONSISTENCY_BLOCKED_REASON}.`);
1536
+ }
1537
+ const closeoutGate = checkCloseoutConsistencyGate(milestoneId);
1538
+ if (!closeoutGate.ok) {
1539
+ throw new GSDError(GSD_GIT_ERROR, formatCloseoutConsistencyBlock(closeoutGate));
1527
1540
  }
1528
1541
  }
1529
1542
  // 2. Get completed slices for commit message
@@ -65,6 +65,9 @@ function readDetails(result) {
65
65
  }
66
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- result shape varies by tool
67
67
  function formatToolErrorText(result, details) {
68
+ if (typeof details?.displayReason === "string" && details.displayReason) {
69
+ return details.displayReason;
70
+ }
68
71
  const message = details?.error
69
72
  ?? result?.content?.find((entry) => entry.type === "text")?.text
70
73
  ?? "unknown";
@@ -405,6 +408,9 @@ export function registerDbTools(pi) {
405
408
  kind: StringEnum(["gsd_uat_exec", "gsd_exec", "screenshot", "log", "url", "browser"], { description: "Evidence kind" }),
406
409
  ref: Type.String({ description: "Evidence ID, approved .gsd path, or URL" }),
407
410
  note: Type.Optional(Type.String({ description: "Short evidence note" })),
411
+ unitType: Type.Optional(Type.String({ description: "Unit that produced the evidence" })),
412
+ tool: Type.Optional(Type.String({ description: "Tool that produced the evidence" })),
413
+ executionId: Type.Optional(Type.String({ description: "Stable execution or artifact id" })),
408
414
  });
409
415
  const uatCheck = Type.Object({
410
416
  id: Type.String({ description: "Stable check ID from the UAT spec" }),
@@ -416,17 +422,17 @@ export function registerDbTools(pi) {
416
422
  nonAutomatable: Type.Optional(Type.Boolean({ description: "True when the check is explicitly non-automatable" })),
417
423
  });
418
424
  const toolPresentationBlock = Type.Object({
419
- surface: StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" }),
425
+ surface: Type.Optional(StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" })),
420
426
  model: Type.Optional(Type.Object({
421
427
  provider: Type.Optional(Type.String()),
422
428
  api: Type.Optional(Type.String()),
423
429
  id: Type.Optional(Type.String()),
424
430
  })),
425
- presentedTools: Type.Array(Type.String(), { description: "Tool names actually presented to the model" }),
426
- blockedTools: Type.Array(Type.Object({
431
+ presentedTools: Type.Optional(Type.Array(Type.String(), { description: "Tool names actually presented to the model" })),
432
+ blockedTools: Type.Optional(Type.Array(Type.Object({
427
433
  name: Type.String(),
428
434
  reason: Type.String(),
429
- }), { description: "Tool names blocked from the model with reasons" }),
435
+ }), { description: "Tool names blocked from the model with reasons" })),
430
436
  aliases: Type.Optional(Type.Array(Type.Object({
431
437
  requested: Type.String(),
432
438
  canonical: Type.String(),
@@ -448,12 +454,12 @@ export function registerDbTools(pi) {
448
454
  "Do not use raw gsd_summary_save as a substitute for UAT results.",
449
455
  ],
450
456
  parameters: Type.Object({
451
- milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
452
- sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
453
- uatType: StringEnum(["artifact-driven", "browser-executable", "runtime-executable", "live-runtime", "mixed", "human-experience"], { description: "Declared UAT mode" }),
454
- verdict: StringEnum(["PASS", "FAIL", "PARTIAL"], { description: "Overall UAT verdict" }),
455
- checks: Type.Array(uatCheck, { description: "Structured check results" }),
456
- presentation: toolPresentationBlock,
457
+ milestoneId: Type.Optional(Type.String({ description: "Milestone ID (e.g. M001)" })),
458
+ sliceId: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
459
+ uatType: Type.Optional(Type.String({ description: "Declared UAT mode" })),
460
+ verdict: Type.Optional(Type.String({ description: "Overall UAT verdict: PASS, FAIL, or PARTIAL" })),
461
+ checks: Type.Optional(Type.Array(uatCheck, { description: "Structured check results" })),
462
+ presentation: Type.Optional(toolPresentationBlock),
457
463
  notes: Type.Optional(Type.String({ description: "Overall verdict rationale" })),
458
464
  attempt: Type.Optional(Type.String({ description: "Attempt number or auto" })),
459
465
  previousAttemptId: Type.Optional(Type.String({ description: "Prior attempt ID, when retrying" })),
@@ -30,7 +30,7 @@ import { getGuidedUnitContext } from "../guided-unit-context.js";
30
30
  import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
31
31
  import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
32
32
  import { filterToolsForProvider } from "../model-router.js";
33
- import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
33
+ import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
34
34
  let approvalQuestionAbortInFlight = false;
35
35
  async function loadWelcomeScreenModule() {
36
36
  const candidates = [];
@@ -204,7 +204,12 @@ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registered
204
204
  return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
205
205
  }
206
206
  export function buildRunUatGsdToolSet(activeToolNames, registeredToolNames = activeToolNames) {
207
- const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES]);
207
+ const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [
208
+ ...RUN_UAT_WORKFLOW_TOOL_NAMES,
209
+ ...RUN_UAT_READ_ONLY_TOOL_NAMES,
210
+ "subagent",
211
+ ...RUN_UAT_BROWSER_TOOL_NAMES,
212
+ ]);
208
213
  return [...new Set(scoped)];
209
214
  }
210
215
  export function buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames = activeToolNames) {
@@ -382,6 +387,11 @@ function isContextDraftSummarySave(toolName, input) {
382
387
  return false;
383
388
  return input.artifact_type === "CONTEXT-DRAFT";
384
389
  }
390
+ function withDepthGateDisplayReason(result, displayReason = "Depth confirmation is waiting for your answer.") {
391
+ if (!result.block)
392
+ return result;
393
+ return { ...result, displayReason };
394
+ }
385
395
  function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
386
396
  if (deferredApprovalGate?.basePath !== basePath)
387
397
  return { block: false };
@@ -389,14 +399,14 @@ function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
389
399
  return { block: false };
390
400
  if (isContextDraftSummarySave(toolName, input))
391
401
  return { block: false };
392
- return {
402
+ return withDepthGateDisplayReason({
393
403
  block: true,
394
404
  reason: [
395
405
  `HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
396
406
  `Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
397
407
  `Wait for the user's answer before calling additional tools.`,
398
408
  ].join(" "),
399
- };
409
+ });
400
410
  }
401
411
  export function resolveNotificationStoreBasePath(basePath) {
402
412
  return resolveWorktreeProjectRoot(basePath);
@@ -754,7 +764,7 @@ export function registerHooks(pi, ecosystemHandlers) {
754
764
  if (ctx) {
755
765
  await maybePauseAutoForApprovalGate(ctx, pi, true, "Depth confirmation is waiting for your answer — pausing auto-mode.");
756
766
  }
757
- return bashGuard;
767
+ return withDepthGateDisplayReason(bashGuard);
758
768
  }
759
769
  }
760
770
  else {
@@ -763,7 +773,7 @@ export function registerHooks(pi, ecosystemHandlers) {
763
773
  if (ctx) {
764
774
  await maybePauseAutoForApprovalGate(ctx, pi, true, "Depth confirmation is waiting for your answer — pausing auto-mode.");
765
775
  }
766
- return gateGuard;
776
+ return withDepthGateDisplayReason(gateGuard);
767
777
  }
768
778
  }
769
779
  }
@@ -847,8 +857,9 @@ export function registerHooks(pi, ecosystemHandlers) {
847
857
  if (!isToolCallEventType("write", event))
848
858
  return;
849
859
  const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive(discussionBasePath), discussionBasePath);
850
- if (result.block)
851
- return result;
860
+ if (result.block) {
861
+ return withDepthGateDisplayReason(result, "Depth check required before writing milestone context.");
862
+ }
852
863
  });
853
864
  // ── Safety harness: evidence collection + destructive command blocking ──
854
865
  pi.on("tool_call", async (event, ctx) => {
@@ -2,7 +2,7 @@
2
2
  import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { minimatch } from "minimatch";
5
- import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
5
+ import { GSD_PHASE_SCOPE_DISPLAY_REASON, shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
6
6
  import { getIsolationMode } from "../preferences.js";
7
7
  import { compileSubagentPermissionContract } from "../unit-context-manifest.js";
8
8
  import { logWarning } from "../workflow-logger.js";
@@ -643,6 +643,13 @@ function blockReason(unitType, mode, what) {
643
643
  `the work belongs in execute-task, not in a planning unit.`,
644
644
  ].join(" ");
645
645
  }
646
+ function planningBlock(unitType, mode, what) {
647
+ return {
648
+ block: true,
649
+ reason: blockReason(unitType, mode, what),
650
+ displayReason: GSD_PHASE_SCOPE_DISPLAY_REASON,
651
+ };
652
+ }
646
653
  /**
647
654
  * Planning-unit tool-policy enforcement. Returns { block } per the policy
648
655
  * resolved from the active unit's manifest:
@@ -690,10 +697,10 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
690
697
  if (tool.startsWith("gsd_"))
691
698
  return { block: false };
692
699
  if (PLANNING_WRITE_TOOLS.has(tool) || tool === "bash" || PLANNING_SUBAGENT_TOOLS.has(tool)) {
693
- return { block: true, reason: blockReason(unitType, policy.mode, `${tool} is not permitted (read-only)`) };
700
+ return planningBlock(unitType, policy.mode, `${tool} is not permitted (read-only)`);
694
701
  }
695
702
  // Unknown tool in read-only mode — block by default.
696
- return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
703
+ return planningBlock(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`);
697
704
  }
698
705
  // planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
699
706
  if (PLANNING_SAFE_TOOLS.has(tool))
@@ -711,10 +718,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
711
718
  // instead of silently bypassing the gate.
712
719
  if (agentClasses === undefined) {
713
720
  warnMissingControlledDispatchAgentClasses(unitType, policy.mode, tool);
714
- return {
715
- block: true,
716
- reason: blockReason(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`),
717
- };
721
+ return planningBlock(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`);
718
722
  }
719
723
  // agentClasses was explicitly provided but resolved to an empty list (for
720
724
  // example, a bare tool call with no agent field). Pass through; no agents
@@ -724,41 +728,29 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
724
728
  }
725
729
  const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
726
730
  if (globallyDisallowed) {
727
- return {
728
- block: true,
729
- reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`),
730
- };
731
+ return planningBlock(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`);
731
732
  }
732
733
  const disallowedByPolicy = requested.find(a => !allowed.has(a));
733
734
  if (disallowedByPolicy) {
734
- return {
735
- block: true,
736
- reason: blockReason(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`),
737
- };
735
+ return planningBlock(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`);
738
736
  }
739
737
  return { block: false };
740
738
  }
741
- return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
739
+ return planningBlock(unitType, policy.mode, "subagent dispatch is not permitted in planning units");
742
740
  }
743
741
  if (tool === "bash") {
744
742
  if (policy.mode === "verification") {
745
743
  if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
746
744
  return { block: false };
747
- return {
748
- block: true,
749
- reason: blockReason(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
750
- };
745
+ return planningBlock(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`);
751
746
  }
752
747
  if (BASH_READ_ONLY_RE.test(pathOrCommand))
753
748
  return { block: false };
754
- return {
755
- block: true,
756
- reason: blockReason(unitType, policy.mode, `bash is restricted to read-only commands (cat/grep/git log/etc); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
757
- };
749
+ return planningBlock(unitType, policy.mode, `bash is restricted to read-only commands (cat/grep/git log/etc); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`);
758
750
  }
759
751
  if (PLANNING_WRITE_TOOLS.has(tool)) {
760
752
  if (!pathOrCommand) {
761
- return { block: true, reason: blockReason(unitType, policy.mode, `${tool} called with empty path`) };
753
+ return planningBlock(unitType, policy.mode, `${tool} called with empty path`);
762
754
  }
763
755
  const absPath = isAbsolute(pathOrCommand) ? pathOrCommand : resolve(basePath, pathOrCommand);
764
756
  // Always allow .gsd/ writes — that's where planning artifacts live.
@@ -768,10 +760,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
768
760
  if (policy.mode === "docs" && matchesAllowedGlob(absPath, basePath, policy.allowedPathGlobs)) {
769
761
  return { block: false };
770
762
  }
771
- return {
772
- block: true,
773
- reason: blockReason(unitType, policy.mode, `cannot ${tool} "${pathOrCommand}" — writes are restricted to .gsd/${policy.mode === "docs" ? " and " + policy.allowedPathGlobs.join(", ") : ""}`),
774
- };
763
+ return planningBlock(unitType, policy.mode, `cannot ${tool} "${pathOrCommand}" — writes are restricted to .gsd/${policy.mode === "docs" ? " and " + policy.allowedPathGlobs.join(", ") : ""}`);
775
764
  }
776
765
  // Unknown tool name — pass through. Other layers (queue, pending-gate,
777
766
  // CONTEXT.md write) catch known mutating shapes; defaulting to allow here
@@ -0,0 +1,61 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Shared DB-backed guard for milestone closeout finalization.
3
+ import { getDbPath, getLatestAssessmentByScope, getMilestone, getMilestoneSlices, getPendingGates, getSliceTasks, isDbAvailable, refreshOpenDatabaseFromDisk, } from "./gsd-db.js";
4
+ import { isClosedStatus } from "./status-guards.js";
5
+ export const CLOSEOUT_CONSISTENCY_BLOCKED_REASON = "closeout-consistency-blocked";
6
+ function blocked(reason, message) {
7
+ return {
8
+ ok: false,
9
+ reason,
10
+ recoveryReason: CLOSEOUT_CONSISTENCY_BLOCKED_REASON,
11
+ message,
12
+ };
13
+ }
14
+ function isFileBackedDbPath(path) {
15
+ return Boolean(path && path !== ":memory:");
16
+ }
17
+ export function checkCloseoutConsistencyGate(milestoneId, options = {}) {
18
+ if (!isDbAvailable()) {
19
+ return blocked("db-unavailable", `Closeout consistency blocked for ${milestoneId}: canonical DB is unavailable.`);
20
+ }
21
+ if (options.refreshFromDisk && isFileBackedDbPath(getDbPath()) && !refreshOpenDatabaseFromDisk()) {
22
+ return blocked("db-refresh-failed", `Closeout consistency blocked for ${milestoneId}: canonical DB refresh failed.`);
23
+ }
24
+ const milestone = getMilestone(milestoneId);
25
+ if (!milestone) {
26
+ return blocked("milestone-missing", `Closeout consistency blocked for ${milestoneId}: milestone is missing from canonical DB.`);
27
+ }
28
+ if (!isClosedStatus(milestone.status)) {
29
+ return blocked("milestone-open", `Closeout consistency blocked for ${milestoneId}: canonical DB milestone status is "${milestone.status}".`);
30
+ }
31
+ if (milestone.status !== "skipped") {
32
+ const validation = getLatestAssessmentByScope(milestoneId, "milestone-validation");
33
+ if (validation?.status !== "pass") {
34
+ return blocked("validation-not-pass", `Closeout consistency blocked for ${milestoneId}: latest milestone validation is "${validation?.status ?? "absent"}".`);
35
+ }
36
+ }
37
+ const slices = getMilestoneSlices(milestoneId);
38
+ if (slices.length === 0 && milestone.status !== "skipped") {
39
+ return blocked("slice-missing", `Closeout consistency blocked for ${milestoneId}: no slices exist in canonical DB.`);
40
+ }
41
+ for (const slice of slices) {
42
+ if (!isClosedStatus(slice.status)) {
43
+ return blocked("slice-open", `Closeout consistency blocked for ${milestoneId}: slice ${slice.id} status is "${slice.status}".`);
44
+ }
45
+ for (const task of getSliceTasks(milestoneId, slice.id)) {
46
+ if (!isClosedStatus(task.status)) {
47
+ return blocked("task-open", `Closeout consistency blocked for ${milestoneId}: task ${slice.id}/${task.id} status is "${task.status}".`);
48
+ }
49
+ }
50
+ const pendingGate = getPendingGates(milestoneId, slice.id)[0];
51
+ if (pendingGate) {
52
+ return blocked("quality-gate-pending", `Closeout consistency blocked for ${milestoneId}: quality gate ${pendingGate.gate_id} is still pending for ${slice.id}.`);
53
+ }
54
+ }
55
+ return { ok: true };
56
+ }
57
+ export function formatCloseoutConsistencyBlock(result) {
58
+ if (result.ok)
59
+ return "";
60
+ return `${result.message} Recovery reason: ${result.recoveryReason}. Resolve the canonical DB state and run /gsd auto to retry.`;
61
+ }