@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.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
- package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
- package/dist/resources/extensions/gsd/guided-flow.js +89 -107
- package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
- package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -17
- package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
- package/dist/resources/extensions/gsd/tool-contract.js +5 -0
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +17 -7
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -4
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +169 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -75
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -3
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
- package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +3 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +6 -6
- package/packages/pi-ai/dist/models.generated.js +6 -6
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
- package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
- package/src/resources/extensions/gsd/guided-flow.ts +124 -134
- package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
- package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
- package/src/resources/extensions/gsd/prompts/run-uat.md +3 -17
- package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
- package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
- package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
- package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +23 -5
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
- package/src/resources/extensions/gsd/tool-contract.ts +6 -0
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +38 -8
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +100 -5
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +186 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -75
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
|
575
|
-
if (!
|
|
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 {
|
|
3
|
-
export
|
|
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}"
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
1526
|
-
logError("worktree",
|
|
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:
|
|
454
|
-
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], [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|