@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
|
@@ -15,7 +15,7 @@ import { delimiter, join } from "node:path";
|
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
16
16
|
|
|
17
17
|
import { mergeMilestoneToMain } from "../auto-worktree.ts";
|
|
18
|
-
import { closeDatabase, openDatabase } from "../gsd-db.ts";
|
|
18
|
+
import { closeDatabase, insertAssessment, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
19
19
|
import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
|
|
20
20
|
import { _clearGsdRootCache } from "../paths.ts";
|
|
21
21
|
import { _resetServiceCache } from "../worktree.ts";
|
|
@@ -145,6 +145,15 @@ test("mergeMilestoneToMain keeps the Windows DB cycle closed through squash merg
|
|
|
145
145
|
|
|
146
146
|
withPlatform("win32", () => {
|
|
147
147
|
assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
|
|
148
|
+
insertMilestone({ id: "M001", title: "Windows DB cycle", status: "complete" });
|
|
149
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Done Slice", status: "complete" });
|
|
150
|
+
insertAssessment({
|
|
151
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
152
|
+
milestoneId: "M001",
|
|
153
|
+
status: "pass",
|
|
154
|
+
scope: "milestone-validation",
|
|
155
|
+
fullContent: "verdict: pass",
|
|
156
|
+
});
|
|
148
157
|
assert.equal(existsSync(join(repo, ".gsd", "gsd.db-shm")), true);
|
|
149
158
|
|
|
150
159
|
process.env.PATH = `${bin}${delimiter}${originalPath}`;
|
|
@@ -6,7 +6,7 @@ import assert from "node:assert/strict";
|
|
|
6
6
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { openDatabase, insertMilestone, closeDatabase } from "../gsd-db.js";
|
|
9
|
+
import { openDatabase, insertAssessment, insertMilestone, insertSlice, closeDatabase } from "../gsd-db.js";
|
|
10
10
|
import {
|
|
11
11
|
isMilestoneCloseoutSettled,
|
|
12
12
|
evaluateCompleteMilestoneDispatch,
|
|
@@ -39,6 +39,14 @@ test("isMilestoneCloseoutSettled requires DB closed and summary artifact", async
|
|
|
39
39
|
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
40
40
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
41
41
|
insertMilestone({ id: "M001", title: "Done", status: "complete" });
|
|
42
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Done Slice", status: "complete" });
|
|
43
|
+
insertAssessment({
|
|
44
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
45
|
+
milestoneId: "M001",
|
|
46
|
+
status: "pass",
|
|
47
|
+
scope: "milestone-validation",
|
|
48
|
+
fullContent: "verdict: pass",
|
|
49
|
+
});
|
|
42
50
|
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
43
51
|
mkdirSync(milestoneDir, { recursive: true });
|
|
44
52
|
writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\n");
|
|
@@ -2,7 +2,12 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildRunUatResultPresentation,
|
|
7
|
+
RUN_UAT_READ_ONLY_TOOL_NAMES,
|
|
8
|
+
RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
9
|
+
RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
10
|
+
} from "../tool-presentation-plan.ts";
|
|
6
11
|
|
|
7
12
|
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
8
13
|
const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
|
|
@@ -29,7 +34,7 @@ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence"
|
|
|
29
34
|
assert.match(prompt, /uatType:\s*"\{\{uatType\}\}"/);
|
|
30
35
|
assert.match(prompt, /gsd_uat_result_save/);
|
|
31
36
|
assert.match(prompt, /presentedTools/);
|
|
32
|
-
assert.match(prompt, /
|
|
37
|
+
assert.match(prompt, /\{\{canonicalPresentation\}\}/);
|
|
33
38
|
assert.match(prompt, /live-runtime/);
|
|
34
39
|
assert.match(prompt, /browser\/runtime\/network/i);
|
|
35
40
|
assert.match(prompt, /NEEDS-HUMAN/);
|
|
@@ -51,16 +56,29 @@ test("run-uat prompt gives the complete UAT result-save presentation contract",
|
|
|
51
56
|
const prompt = readPrompt("run-uat");
|
|
52
57
|
assert.match(prompt, /Call `gsd_uat_result_save` once after all checks are complete/);
|
|
53
58
|
assert.doesNotMatch(prompt, /Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`/);
|
|
59
|
+
assert.match(prompt, /\{\{canonicalPresentation\}\}/);
|
|
60
|
+
assert.match(prompt, /\{\{toolPresentationPlanId\}\}/);
|
|
54
61
|
|
|
62
|
+
const presentation = buildRunUatResultPresentation();
|
|
63
|
+
assert.equal(presentation.toolPresentationPlanId, RUN_UAT_TOOL_PRESENTATION_PLAN_ID);
|
|
55
64
|
for (const toolName of RUN_UAT_WORKFLOW_TOOL_NAMES) {
|
|
56
|
-
assert.ok(
|
|
65
|
+
assert.ok(presentation.presentedTools.includes(toolName), `presentation should include required tool ${toolName}`);
|
|
66
|
+
}
|
|
67
|
+
for (const toolName of RUN_UAT_READ_ONLY_TOOL_NAMES) {
|
|
68
|
+
assert.ok(presentation.presentedTools.includes(toolName), `presentation should include read-only tool ${toolName}`);
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
for (const toolName of ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"] as const) {
|
|
60
|
-
assert.ok(
|
|
72
|
+
assert.ok(
|
|
73
|
+
presentation.blockedTools.some((entry) => entry.name === toolName),
|
|
74
|
+
`presentation should include blocked tool ${toolName}`,
|
|
75
|
+
);
|
|
61
76
|
}
|
|
62
77
|
|
|
63
|
-
assert.ok(
|
|
78
|
+
assert.ok(
|
|
79
|
+
presentation.blockedTools.every((entry) => entry.reason === "forbidden during run-uat"),
|
|
80
|
+
"presentation should explain blocked run-uat tools",
|
|
81
|
+
);
|
|
64
82
|
});
|
|
65
83
|
|
|
66
84
|
test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
|
|
@@ -70,6 +70,50 @@ test("register-hooks hard-blocks destructive bash commands outside auto-mode", a
|
|
|
70
70
|
assert.match(block?.reason ?? "", /IaC apply\/destroy/);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
test("register-hooks keeps depth-gate reason model-facing and adds displayReason", async (t) => {
|
|
74
|
+
const dir = makeTempDir("display-reason");
|
|
75
|
+
const originalCwd = process.cwd();
|
|
76
|
+
process.chdir(dir);
|
|
77
|
+
resetWriteGateState(dir);
|
|
78
|
+
|
|
79
|
+
t.after(() => {
|
|
80
|
+
try {
|
|
81
|
+
resetWriteGateState(dir);
|
|
82
|
+
} finally {
|
|
83
|
+
process.chdir(originalCwd);
|
|
84
|
+
rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
89
|
+
const pi = {
|
|
90
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
91
|
+
const existing = handlers.get(event) ?? [];
|
|
92
|
+
existing.push(handler);
|
|
93
|
+
handlers.set(event, existing);
|
|
94
|
+
},
|
|
95
|
+
} as any;
|
|
96
|
+
|
|
97
|
+
registerHooks(pi, []);
|
|
98
|
+
|
|
99
|
+
let block: any;
|
|
100
|
+
for (const handler of handlers.get("tool_call") ?? []) {
|
|
101
|
+
const result = await handler({
|
|
102
|
+
toolName: "write",
|
|
103
|
+
input: {
|
|
104
|
+
path: join(dir, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
|
|
105
|
+
content: "# M001 Context\n",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (result?.block) block = result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
assert.equal(block?.block, true);
|
|
112
|
+
assert.match(block?.reason ?? "", /HARD BLOCK: Cannot write to milestone CONTEXT\.md/);
|
|
113
|
+
assert.match(block?.reason ?? "", /ask_user_questions/);
|
|
114
|
+
assert.equal(block?.displayReason, "Depth check required before writing milestone context.");
|
|
115
|
+
});
|
|
116
|
+
|
|
73
117
|
test("register-hooks unlocks milestone depth verification from question id without guided-flow state (#4047)", async (t) => {
|
|
74
118
|
const dir = makeTempDir("manual");
|
|
75
119
|
const originalCwd = process.cwd();
|
|
@@ -123,6 +123,10 @@ test("#4782 phase 3: buildRunUatPrompt inlines UAT and keeps summary/project con
|
|
|
123
123
|
// Project path is advertised on-demand; full project body is not inlined.
|
|
124
124
|
assert.match(prompt, /\.gsd\/PROJECT\.md/);
|
|
125
125
|
assert.ok(!prompt.includes("Run-UAT composer fixture project"), "run-uat should not inline full project context");
|
|
126
|
+
|
|
127
|
+
assert.match(prompt, /"toolPresentationPlanId": "run-uat\/default-v1"/);
|
|
128
|
+
assert.match(prompt, /"gsd_uat_result_save"/);
|
|
129
|
+
assert.match(prompt, /"read"/);
|
|
126
130
|
});
|
|
127
131
|
|
|
128
132
|
test("#4782 phase 3: buildRunUatPrompt omits optional slice summary when file is missing", async (t) => {
|
|
@@ -7,6 +7,7 @@ import assert from "node:assert/strict";
|
|
|
7
7
|
import { classifyFailure } from "../recovery-classification.js";
|
|
8
8
|
import { reconcileBeforeDispatch } from "../state-reconciliation.js";
|
|
9
9
|
import { compileUnitToolContract } from "../tool-contract.js";
|
|
10
|
+
import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.js";
|
|
10
11
|
import type { GSDState } from "../types.js";
|
|
11
12
|
|
|
12
13
|
function makeState(overrides: Partial<GSDState> = {}): GSDState {
|
|
@@ -63,10 +64,35 @@ test("Tool Contract compiles known Unit prompt and tool policy", () => {
|
|
|
63
64
|
assert.equal(result.ok, true);
|
|
64
65
|
assert.equal(result.ok && result.contract.unitType, "execute-task");
|
|
65
66
|
assert.deepEqual(result.ok && result.contract.requiredWorkflowTools, ["gsd_task_complete"]);
|
|
67
|
+
assert.deepEqual(result.ok && result.contract.forbiddenWorkflowTools, []);
|
|
66
68
|
assert.equal(result.ok && result.contract.toolsPolicy.mode, "all");
|
|
67
69
|
assert.ok(result.ok && result.contract.validationRules.includes("closeout-tool-present"));
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
test("Tool Contract records high-risk cross-phase tool boundaries without single-owning every tool", () => {
|
|
73
|
+
const completeSlice = compileUnitToolContract("complete-slice");
|
|
74
|
+
const runUat = compileUnitToolContract("run-uat");
|
|
75
|
+
|
|
76
|
+
assert.equal(completeSlice.ok, true);
|
|
77
|
+
assert.ok(
|
|
78
|
+
completeSlice.ok &&
|
|
79
|
+
completeSlice.contract.forbiddenWorkflowTools.some((tool) => tool.name === "gsd_uat_result_save"),
|
|
80
|
+
"complete-slice should explicitly forbid saving UAT Assessments",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
assert.equal(runUat.ok, true);
|
|
84
|
+
assert.ok(
|
|
85
|
+
runUat.ok &&
|
|
86
|
+
runUat.contract.requiredWorkflowTools.includes("gsd_uat_result_save"),
|
|
87
|
+
"run-uat should own the UAT result-save tool",
|
|
88
|
+
);
|
|
89
|
+
assert.ok(
|
|
90
|
+
runUat.ok &&
|
|
91
|
+
runUat.contract.forbiddenWorkflowTools.some((tool) => tool.name === "gsd_exec"),
|
|
92
|
+
"run-uat should prefer typed UAT execution over generic gsd_exec",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
70
96
|
test("Tool Contract fails closed for unknown Units", () => {
|
|
71
97
|
const result = compileUnitToolContract("custom-step");
|
|
72
98
|
|
|
@@ -74,10 +100,20 @@ test("Tool Contract fails closed for unknown Units", () => {
|
|
|
74
100
|
assert.equal(!result.ok && result.reason, "unknown-unit-type");
|
|
75
101
|
});
|
|
76
102
|
|
|
103
|
+
test("auto Unit tool scope blocks complete-slice from saving UAT Assessment", () => {
|
|
104
|
+
const result = shouldBlockAutoUnitToolCall("complete-slice", "gsd_uat_result_save");
|
|
105
|
+
|
|
106
|
+
assert.equal(result.block, true);
|
|
107
|
+
assert.match(result.reason ?? "", /Tool Contract failure/);
|
|
108
|
+
assert.match(result.reason ?? "", /Run UAT owns persisted UAT Assessment/);
|
|
109
|
+
});
|
|
110
|
+
|
|
77
111
|
test("Recovery Classification covers ADR-015 failure families", () => {
|
|
78
112
|
const cases = [
|
|
79
113
|
["invalid tool schema enum", "tool-schema", "stop"],
|
|
114
|
+
["Tool Contract failure: complete-slice cannot use gsd_uat_result_save", "tool-contract", "stop"],
|
|
80
115
|
["deterministic policy rejection", "deterministic-policy", "stop"],
|
|
116
|
+
["cannot legally advance because required UAT Assessment artifact is missing", "lifecycle-progression", "stop"],
|
|
81
117
|
["stale worker lease", "stale-worker", "stop"],
|
|
82
118
|
["worktree root missing .git", "worktree-invalid", "stop"],
|
|
83
119
|
["verification drift in state snapshot", "verification-drift", "escalate"],
|
|
@@ -101,7 +101,7 @@ test("buildMinimalAutoGsdToolSet keeps unit-specific completion tools without al
|
|
|
101
101
|
assert.ok(!result.includes("gsd_complete_slice"));
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific tools", () => {
|
|
104
|
+
test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific and read-only tools", () => {
|
|
105
105
|
const active = ["ask_user_questions", "bash", "read", "edit", "write", "gsd_summary_save"];
|
|
106
106
|
const registered = [
|
|
107
107
|
...active,
|
|
@@ -123,11 +123,11 @@ test("buildMinimalAutoGsdToolSet scopes run-uat to UAT-specific tools", () => {
|
|
|
123
123
|
assert.ok(result.includes("gsd_resume"));
|
|
124
124
|
assert.ok(result.includes("gsd_milestone_status"));
|
|
125
125
|
assert.ok(result.includes("gsd_journal_query"));
|
|
126
|
+
assert.ok(result.includes("read"));
|
|
126
127
|
assert.ok(result.includes("browser_navigate"), "run-uat needs browser_navigate");
|
|
127
128
|
assert.ok(result.includes("browser_click"), "run-uat needs browser_click");
|
|
128
129
|
assert.ok(!result.includes("ToolSearch"));
|
|
129
130
|
assert.ok(!result.includes("bash"));
|
|
130
|
-
assert.ok(!result.includes("read"));
|
|
131
131
|
assert.ok(!result.includes("edit"));
|
|
132
132
|
assert.ok(!result.includes("write"));
|
|
133
133
|
assert.ok(!result.includes("gsd_exec"));
|
|
@@ -230,9 +230,9 @@ test("buildMinimalAutoGsdToolSet preserves compatible browser add-ons for run-ua
|
|
|
230
230
|
assert.ok(result.includes("gsd_uat_exec"));
|
|
231
231
|
assert.ok(result.includes("gsd_uat_result_save"));
|
|
232
232
|
assert.ok(result.includes("subagent"));
|
|
233
|
+
assert.ok(result.includes("read"));
|
|
233
234
|
assert.ok(!result.includes("ToolSearch"));
|
|
234
235
|
assert.ok(!result.includes("bash"));
|
|
235
|
-
assert.ok(!result.includes("read"));
|
|
236
236
|
assert.ok(!result.includes("edit"));
|
|
237
237
|
assert.ok(!result.includes("write"));
|
|
238
238
|
assert.ok(!result.includes("gsd_exec"));
|
|
@@ -281,12 +281,12 @@ test("buildMinimalAutoGsdToolSet honors provider-compatible registered tools for
|
|
|
281
281
|
|
|
282
282
|
assert.ok(result.includes("gsd_uat_exec"));
|
|
283
283
|
assert.ok(result.includes("gsd_uat_result_save"));
|
|
284
|
+
assert.ok(result.includes("read"));
|
|
284
285
|
assert.ok(result.includes("browser_navigate"));
|
|
285
286
|
assert.ok(result.includes("browser_click"));
|
|
286
287
|
assert.ok(!result.includes("browser_screenshot"), "provider-filtered screenshot tool must stay filtered");
|
|
287
288
|
assert.ok(!result.includes("ToolSearch"));
|
|
288
289
|
assert.ok(!result.includes("bash"));
|
|
289
|
-
assert.ok(!result.includes("read"));
|
|
290
290
|
assert.ok(!result.includes("gsd_exec"));
|
|
291
291
|
assert.ok(!result.includes("gsd_summary_save"));
|
|
292
292
|
assert.ok(!result.includes("gsd_save_gate_result"));
|
|
@@ -75,6 +75,11 @@ test("closeout executors reject phase escalation from the wrong active auto unit
|
|
|
75
75
|
const milestone = await executeCompleteMilestone({} as Parameters<typeof executeCompleteMilestone>[0], "/tmp/project");
|
|
76
76
|
assert.equal(milestone.isError, true);
|
|
77
77
|
assert.match(String(milestone.details.error), /complete_milestone may only run from complete-milestone/);
|
|
78
|
+
|
|
79
|
+
const uat = await executeUatResultSave({} as Parameters<typeof executeUatResultSave>[0], "/tmp/project");
|
|
80
|
+
assert.equal(uat.isError, true);
|
|
81
|
+
assert.match(String(uat.details.error), /save_uat_result may only run from run-uat/);
|
|
82
|
+
assert.match(String(uat.details.error), /Tool Contract failure/);
|
|
78
83
|
} finally {
|
|
79
84
|
autoSession.reset();
|
|
80
85
|
}
|
|
@@ -643,6 +648,210 @@ test("executeUatResultSave accepts gsd_uat_exec evidence written in a milestone
|
|
|
643
648
|
}
|
|
644
649
|
});
|
|
645
650
|
|
|
651
|
+
test("executeUatResultSave supplies canonical presentation and normalizes verdict casing", async () => {
|
|
652
|
+
const base = makeTmpBase();
|
|
653
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
654
|
+
const worktreeExecDir = join(worktree, ".gsd", "exec");
|
|
655
|
+
const evidenceId = "uat-lowercase-verdict";
|
|
656
|
+
try {
|
|
657
|
+
openTestDb(base);
|
|
658
|
+
seedMilestone("M001", "Milestone One");
|
|
659
|
+
seedSlice("M001", "S03", "complete");
|
|
660
|
+
mkdirSync(worktreeExecDir, { recursive: true });
|
|
661
|
+
writeFileSync(
|
|
662
|
+
join(worktreeExecDir, `${evidenceId}.meta.json`),
|
|
663
|
+
JSON.stringify({
|
|
664
|
+
id: evidenceId,
|
|
665
|
+
metadata: {
|
|
666
|
+
kind: "uat_exec",
|
|
667
|
+
milestoneId: "M001",
|
|
668
|
+
sliceId: "S03",
|
|
669
|
+
checkId: "UAT-01",
|
|
670
|
+
intent: "uat-artifact-check",
|
|
671
|
+
},
|
|
672
|
+
}),
|
|
673
|
+
"utf-8",
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
677
|
+
milestoneId: "M001",
|
|
678
|
+
sliceId: "S03",
|
|
679
|
+
uatType: "artifact-driven",
|
|
680
|
+
verdict: "pass",
|
|
681
|
+
checks: [{
|
|
682
|
+
id: "UAT-01",
|
|
683
|
+
description: "Static artifact contract passes",
|
|
684
|
+
mode: "artifact",
|
|
685
|
+
result: "PASS",
|
|
686
|
+
evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
|
|
687
|
+
notes: "Artifact check passed.",
|
|
688
|
+
}],
|
|
689
|
+
notes: "UAT passed with canonical presentation supplied by the executor.",
|
|
690
|
+
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
691
|
+
|
|
692
|
+
assert.equal(result.isError, undefined);
|
|
693
|
+
assert.equal(result.details.verdict, "PASS");
|
|
694
|
+
|
|
695
|
+
const attempt = JSON.parse(readFileSync(
|
|
696
|
+
join(base, ".gsd", "uat", "M001", "S03", "attempt-1.json"),
|
|
697
|
+
"utf-8",
|
|
698
|
+
)) as { presentation?: { toolPresentationPlanId?: string; presentedTools?: string[] } };
|
|
699
|
+
assert.equal(attempt.presentation?.toolPresentationPlanId, "run-uat/default-v1");
|
|
700
|
+
assert.ok(attempt.presentation?.presentedTools?.includes("gsd_uat_result_save"));
|
|
701
|
+
assert.ok(attempt.presentation?.presentedTools?.includes("read"));
|
|
702
|
+
} finally {
|
|
703
|
+
closeDatabase();
|
|
704
|
+
cleanup(base);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("executeUatResultSave merges canonical plan ID and read-only tools when presentation lacks plan ID", async () => {
|
|
709
|
+
const base = makeTmpBase();
|
|
710
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
711
|
+
const worktreeExecDir = join(worktree, ".gsd", "exec");
|
|
712
|
+
const evidenceId = "uat-no-plan-id-evidence";
|
|
713
|
+
try {
|
|
714
|
+
openTestDb(base);
|
|
715
|
+
seedMilestone("M001", "Milestone One");
|
|
716
|
+
seedSlice("M001", "S05", "complete");
|
|
717
|
+
mkdirSync(worktreeExecDir, { recursive: true });
|
|
718
|
+
writeFileSync(
|
|
719
|
+
join(worktreeExecDir, `${evidenceId}.meta.json`),
|
|
720
|
+
JSON.stringify({
|
|
721
|
+
id: evidenceId,
|
|
722
|
+
metadata: {
|
|
723
|
+
kind: "uat_exec",
|
|
724
|
+
milestoneId: "M001",
|
|
725
|
+
sliceId: "S05",
|
|
726
|
+
checkId: "UAT-01",
|
|
727
|
+
intent: "uat-artifact-check",
|
|
728
|
+
},
|
|
729
|
+
}),
|
|
730
|
+
"utf-8",
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
734
|
+
milestoneId: "M001",
|
|
735
|
+
sliceId: "S05",
|
|
736
|
+
uatType: "artifact-driven",
|
|
737
|
+
verdict: "PASS",
|
|
738
|
+
checks: [{
|
|
739
|
+
id: "UAT-01",
|
|
740
|
+
description: "Presentation plan ID absent from provider call",
|
|
741
|
+
mode: "artifact",
|
|
742
|
+
result: "PASS",
|
|
743
|
+
evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
|
|
744
|
+
notes: "Canonical merge should apply even when toolPresentationPlanId is absent.",
|
|
745
|
+
}],
|
|
746
|
+
presentation: {
|
|
747
|
+
surface: "mcp",
|
|
748
|
+
presentedTools: [
|
|
749
|
+
"gsd_uat_exec",
|
|
750
|
+
"gsd_uat_result_save",
|
|
751
|
+
"gsd_resume",
|
|
752
|
+
"gsd_milestone_status",
|
|
753
|
+
"gsd_journal_query",
|
|
754
|
+
],
|
|
755
|
+
blockedTools: [
|
|
756
|
+
{ name: "gsd_exec", reason: "forbidden during run-uat" },
|
|
757
|
+
{ name: "gsd_summary_save", reason: "forbidden during run-uat" },
|
|
758
|
+
{ name: "gsd_save_gate_result", reason: "forbidden during run-uat" },
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
notes: "Provider omitted toolPresentationPlanId; executor must canonicalize.",
|
|
762
|
+
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
763
|
+
|
|
764
|
+
assert.equal(result.isError, undefined);
|
|
765
|
+
assert.equal(result.details.verdict, "PASS");
|
|
766
|
+
|
|
767
|
+
const attempt = JSON.parse(readFileSync(
|
|
768
|
+
join(base, ".gsd", "uat", "M001", "S05", "attempt-1.json"),
|
|
769
|
+
"utf-8",
|
|
770
|
+
)) as { presentation?: { toolPresentationPlanId?: string; presentedTools?: string[] } };
|
|
771
|
+
assert.equal(attempt.presentation?.toolPresentationPlanId, "run-uat/default-v1");
|
|
772
|
+
assert.ok(attempt.presentation?.presentedTools?.includes("read"), "read-only tool must be merged in");
|
|
773
|
+
assert.ok(attempt.presentation?.presentedTools?.includes("gsd_uat_result_save"));
|
|
774
|
+
} finally {
|
|
775
|
+
closeDatabase();
|
|
776
|
+
cleanup(base);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("executeUatResultSave rejects saved UAT without fresh UAT-owned evidence", async () => {
|
|
781
|
+
const base = makeTmpBase();
|
|
782
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
783
|
+
const worktreeExecDir = join(worktree, ".gsd", "exec");
|
|
784
|
+
const evidenceId = "generic-exec-evidence";
|
|
785
|
+
try {
|
|
786
|
+
openTestDb(base);
|
|
787
|
+
seedMilestone("M001", "Milestone One");
|
|
788
|
+
seedSlice("M001", "S04", "complete");
|
|
789
|
+
mkdirSync(worktreeExecDir, { recursive: true });
|
|
790
|
+
writeFileSync(
|
|
791
|
+
join(worktreeExecDir, `${evidenceId}.meta.json`),
|
|
792
|
+
JSON.stringify({
|
|
793
|
+
id: evidenceId,
|
|
794
|
+
metadata: { kind: "exec" },
|
|
795
|
+
}),
|
|
796
|
+
"utf-8",
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
800
|
+
milestoneId: "M001",
|
|
801
|
+
sliceId: "S04",
|
|
802
|
+
uatType: "artifact-driven",
|
|
803
|
+
verdict: "PASS",
|
|
804
|
+
checks: [{
|
|
805
|
+
id: "UAT-01",
|
|
806
|
+
description: "Static artifact contract passes",
|
|
807
|
+
mode: "artifact",
|
|
808
|
+
result: "PASS",
|
|
809
|
+
evidence: [{ kind: "gsd_exec", ref: evidenceId }],
|
|
810
|
+
notes: "Generic evidence should not satisfy fresh UAT evidence.",
|
|
811
|
+
}],
|
|
812
|
+
notes: "UAT should not pass without fresh UAT-owned evidence.",
|
|
813
|
+
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
814
|
+
|
|
815
|
+
assert.equal(result.isError, true);
|
|
816
|
+
assert.match(String(result.content[0]?.text), /fresh gsd_uat_exec evidence/);
|
|
817
|
+
} finally {
|
|
818
|
+
closeDatabase();
|
|
819
|
+
cleanup(base);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test("executeUatResultSave rejects an unrecognized uatType", async () => {
|
|
824
|
+
const base = makeTmpBase();
|
|
825
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
826
|
+
try {
|
|
827
|
+
openTestDb(base);
|
|
828
|
+
mkdirSync(worktree, { recursive: true });
|
|
829
|
+
seedMilestone("M001", "Milestone One");
|
|
830
|
+
seedSlice("M001", "S06", "complete");
|
|
831
|
+
|
|
832
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
833
|
+
milestoneId: "M001",
|
|
834
|
+
sliceId: "S06",
|
|
835
|
+
uatType: "hallucinated-mode",
|
|
836
|
+
verdict: "PASS",
|
|
837
|
+
checks: [{
|
|
838
|
+
id: "UAT-01",
|
|
839
|
+
description: "Static artifact contract passes",
|
|
840
|
+
mode: "artifact",
|
|
841
|
+
result: "PASS",
|
|
842
|
+
evidence: [{ kind: "gsd_uat_exec", ref: "some-ref" }],
|
|
843
|
+
}],
|
|
844
|
+
notes: "Should fail before evidence validation.",
|
|
845
|
+
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
846
|
+
|
|
847
|
+
assert.equal(result.isError, true);
|
|
848
|
+
assert.match(String(result.content[0]?.text), /uatType must be one of/);
|
|
849
|
+
} finally {
|
|
850
|
+
closeDatabase();
|
|
851
|
+
cleanup(base);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
646
855
|
test("executeUatResultSave rejects artifact-driven PASS with human follow-up checks", async () => {
|
|
647
856
|
const base = makeTmpBase();
|
|
648
857
|
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
@@ -1409,6 +1618,10 @@ test("executeSummarySave blocks final root artifacts while approval gate is pend
|
|
|
1409
1618
|
|
|
1410
1619
|
assert.equal(result.isError, true);
|
|
1411
1620
|
assert.equal(result.details.error, "root_artifact_write_blocked");
|
|
1621
|
+
assert.equal(
|
|
1622
|
+
result.details.displayReason,
|
|
1623
|
+
"Approval confirmation required before saving final project setup artifacts.",
|
|
1624
|
+
);
|
|
1412
1625
|
assert.match(result.content[0].text, /has not been confirmed/);
|
|
1413
1626
|
assert.equal(existsSync(join(base, ".gsd", "REQUIREMENTS.md")), false);
|
|
1414
1627
|
|
|
@@ -1451,6 +1664,10 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
|
|
|
1451
1664
|
|
|
1452
1665
|
assert.equal(blocked.isError, true);
|
|
1453
1666
|
assert.equal(blocked.details.error, "root_artifact_write_blocked");
|
|
1667
|
+
assert.equal(
|
|
1668
|
+
blocked.details.displayReason,
|
|
1669
|
+
"Approval confirmation required before saving final project setup artifacts.",
|
|
1670
|
+
);
|
|
1454
1671
|
assert.match(blocked.content[0].text, /fail-closed/);
|
|
1455
1672
|
assert.equal(existsSync(join(base, ".gsd", "PROJECT.md")), false);
|
|
1456
1673
|
|
|
@@ -1667,6 +1884,10 @@ test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file i
|
|
|
1667
1884
|
content: "# Context\n\ncontent",
|
|
1668
1885
|
}, base));
|
|
1669
1886
|
assert.equal(blocked.isError, true, "should be blocked without depth verification");
|
|
1887
|
+
assert.equal(
|
|
1888
|
+
blocked.details.displayReason,
|
|
1889
|
+
"Depth check required before writing milestone context.",
|
|
1890
|
+
);
|
|
1670
1891
|
assert.match(
|
|
1671
1892
|
blocked.content[0].text,
|
|
1672
1893
|
/HARD BLOCK/,
|
|
@@ -9,6 +9,7 @@ import test from 'node:test';
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { join, sep } from 'node:path';
|
|
11
11
|
|
|
12
|
+
import { GSD_PHASE_SCOPE_DISPLAY_REASON } from '../auto-unit-tool-scope.ts';
|
|
12
13
|
import { ALLOWED_PLANNING_DISPATCH_AGENTS, shouldBlockPlanningUnit } from '../bootstrap/write-gate.ts';
|
|
13
14
|
import { extractSubagentAgentClasses } from '../bootstrap/subagent-input.ts';
|
|
14
15
|
import { isDeterministicPolicyError } from '../auto-tool-tracking.ts';
|
|
@@ -65,6 +66,19 @@ test('planning-unit: deterministic block reason is suitable for retry short-circ
|
|
|
65
66
|
assert.strictEqual(isDeterministicPolicyError(r.reason!), true);
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
test('planning-unit: blocked tool-policy calls include UI-safe display reason', () => {
|
|
70
|
+
const r = shouldBlockPlanningUnit(
|
|
71
|
+
'edit',
|
|
72
|
+
'src/main.ts',
|
|
73
|
+
BASE,
|
|
74
|
+
'discuss-milestone',
|
|
75
|
+
PLANNING,
|
|
76
|
+
);
|
|
77
|
+
assert.strictEqual(r.block, true);
|
|
78
|
+
assert.match(r.reason!, /HARD BLOCK/);
|
|
79
|
+
assert.strictEqual(r.displayReason, GSD_PHASE_SCOPE_DISPLAY_REASON);
|
|
80
|
+
});
|
|
81
|
+
|
|
68
82
|
test('planning-unit: blocks write to user source via relative path', () => {
|
|
69
83
|
const r = shouldBlockPlanningUnit('write', 'src/main.ts', BASE, 'plan-milestone', PLANNING);
|
|
70
84
|
assert.strictEqual(r.block, true);
|
|
@@ -367,6 +381,7 @@ test('auto-unit scope: execute-task allows only its task completion lifecycle to
|
|
|
367
381
|
assert.strictEqual(blocked.block, true);
|
|
368
382
|
assert.match(blocked.reason!, /HARD BLOCK/);
|
|
369
383
|
assert.match(blocked.reason!, /gsd_save_gate_result/);
|
|
384
|
+
assert.strictEqual(blocked.displayReason, GSD_PHASE_SCOPE_DISPLAY_REASON);
|
|
370
385
|
assert.strictEqual(isDeterministicPolicyError(blocked.reason!), true);
|
|
371
386
|
});
|
|
372
387
|
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
type ToolsPolicy,
|
|
9
9
|
} from "./unit-context-manifest.js";
|
|
10
10
|
import { getRequiredWorkflowToolsForAutoUnit } from "./workflow-mcp.js";
|
|
11
|
+
import { getUnitToolSurfaceContract } from "./unit-tool-contracts.js";
|
|
11
12
|
|
|
12
13
|
export interface UnitToolContract {
|
|
13
14
|
unitType: string;
|
|
14
15
|
contextMode: ContextModePolicy;
|
|
15
16
|
toolsPolicy: ToolsPolicy;
|
|
16
17
|
requiredWorkflowTools: readonly string[];
|
|
18
|
+
forbiddenWorkflowTools: readonly { name: string; reason: string }[];
|
|
17
19
|
promptObligations: readonly string[];
|
|
18
20
|
validationRules: readonly string[];
|
|
19
21
|
closeoutTools: readonly string[];
|
|
@@ -30,6 +32,7 @@ export type ToolContractResult =
|
|
|
30
32
|
|
|
31
33
|
export function compileUnitToolContract(unitType: string): ToolContractResult {
|
|
32
34
|
const manifest = resolveManifest(unitType);
|
|
35
|
+
const surfaceContract = getUnitToolSurfaceContract(unitType);
|
|
33
36
|
if (!manifest) {
|
|
34
37
|
return {
|
|
35
38
|
ok: false,
|
|
@@ -39,6 +42,8 @@ export function compileUnitToolContract(unitType: string): ToolContractResult {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
const requiredWorkflowTools = getRequiredWorkflowToolsForAutoUnit(unitType);
|
|
45
|
+
const forbiddenWorkflowTools = Object.entries(surfaceContract?.forbiddenGsdTools ?? {})
|
|
46
|
+
.map(([name, reason]) => ({ name, reason }));
|
|
42
47
|
const closeoutTools = requiredWorkflowTools.filter((tool) =>
|
|
43
48
|
/^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool),
|
|
44
49
|
);
|
|
@@ -58,6 +63,7 @@ export function compileUnitToolContract(unitType: string): ToolContractResult {
|
|
|
58
63
|
contextMode: manifest.contextMode,
|
|
59
64
|
toolsPolicy: manifest.tools,
|
|
60
65
|
requiredWorkflowTools,
|
|
66
|
+
forbiddenWorkflowTools,
|
|
61
67
|
promptObligations: [
|
|
62
68
|
`context-mode:${manifest.contextMode}`,
|
|
63
69
|
`tools-policy:${manifest.tools.mode}`,
|