@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.9f86580
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/browser-tools/engine/managed-gsd-browser.js +18 -2
- package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
- package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
- package/dist/resources/extensions/browser-tools/index.js +29 -2
- package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
- package/dist/resources/extensions/gsd/auto/phases.js +45 -3
- package/dist/resources/extensions/gsd/auto/session.js +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +26 -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-timers.js +24 -10
- 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/auto.js +26 -4
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
- 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/commands/handlers/auto.js +10 -0
- package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
- package/dist/resources/extensions/gsd/config-overlay.js +1 -0
- package/dist/resources/extensions/gsd/context-masker.js +129 -5
- package/dist/resources/extensions/gsd/guided-flow.js +93 -108
- 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/planner-handoff.js +98 -0
- package/dist/resources/extensions/gsd/preferences-models.js +1 -0
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
- package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
- package/dist/resources/extensions/gsd/tool-contract.js +6 -1
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
- package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
- package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
- package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
- 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 +5 -5
- 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 +157 -18
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +159 -36
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
- package/packages/pi-ai/dist/providers/transform-messages.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/scripts/install/handoff.js +16 -3
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
- package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
- package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
- package/src/resources/extensions/browser-tools/index.ts +36 -5
- package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
- package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
- package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
- package/src/resources/extensions/gsd/auto/phases.ts +48 -6
- package/src/resources/extensions/gsd/auto/session.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +26 -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-timers.ts +25 -9
- 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/auto.ts +28 -4
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
- 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/commands/handlers/auto.ts +9 -0
- package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
- package/src/resources/extensions/gsd/config-overlay.ts +1 -0
- package/src/resources/extensions/gsd/context-masker.ts +152 -5
- package/src/resources/extensions/gsd/guided-flow.ts +128 -135
- 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/planner-handoff.ts +149 -0
- package/src/resources/extensions/gsd/preferences-models.ts +1 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
- package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
- 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-supervisor.test.mjs +4 -0
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
- 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/complete-slice-verification-gate.test.ts +118 -0
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
- 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 +7 -1
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
- 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/planner-handoff.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
- package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
- 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 +56 -0
- package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
- package/src/resources/extensions/gsd/tool-contract.ts +7 -1
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
- package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
- package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
- package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
- 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 → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Regression tests for DB-backed closeout consistency before merge.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
import { mergeMilestoneToMain } from "../auto-worktree.ts";
|
|
12
|
+
import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
|
|
13
|
+
|
|
14
|
+
function git(args: string[], cwd: string): string {
|
|
15
|
+
return execFileSync("git", args, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createRepo(): string {
|
|
23
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "merge-closeout-gate-")));
|
|
24
|
+
git(["init"], dir);
|
|
25
|
+
git(["config", "user.email", "test@test.com"], dir);
|
|
26
|
+
git(["config", "user.name", "Test"], dir);
|
|
27
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
28
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
29
|
+
git(["add", "."], dir);
|
|
30
|
+
git(["commit", "-m", "init"], dir);
|
|
31
|
+
git(["branch", "-M", "main"], dir);
|
|
32
|
+
|
|
33
|
+
git(["checkout", "-b", "milestone/M001"], dir);
|
|
34
|
+
writeFileSync(join(dir, "feature.ts"), "export const feature = true;\n");
|
|
35
|
+
git(["add", "feature.ts"], dir);
|
|
36
|
+
git(["commit", "-m", "feat: milestone work"], dir);
|
|
37
|
+
git(["checkout", "main"], dir);
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("mergeMilestoneToMain blocks when project DB closeout is still open", () => {
|
|
42
|
+
const savedCwd = process.cwd();
|
|
43
|
+
const repo = createRepo();
|
|
44
|
+
try {
|
|
45
|
+
assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
|
|
46
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
47
|
+
|
|
48
|
+
const mainHeadBefore = git(["rev-parse", "main"], repo);
|
|
49
|
+
process.chdir(repo);
|
|
50
|
+
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => mergeMilestoneToMain(repo, "M001", "# M001\n- [x] **S01: Done**\n"),
|
|
53
|
+
/closeout-consistency-blocked/,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
assert.equal(git(["rev-parse", "main"], repo), mainHeadBefore);
|
|
57
|
+
assert.equal(git(["branch", "--show-current"], repo), "main");
|
|
58
|
+
} finally {
|
|
59
|
+
closeDatabase();
|
|
60
|
+
process.chdir(savedCwd);
|
|
61
|
+
if (existsSync(repo)) rmSync(repo, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -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");
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { GSD_COMMAND_DESCRIPTION, getGsdArgumentCompletions, TOP_LEVEL_SUBCOMMANDS } from "../commands/catalog.ts";
|
|
8
|
+
import { handleCoreCommand } from "../commands/handlers/core.ts";
|
|
9
|
+
import { DISPATCH_RULES } from "../auto-dispatch.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildGsdPlannerSpawnPlan,
|
|
12
|
+
formatGsdPlannerCommand,
|
|
13
|
+
hasPlannerHandoffBeenOffered,
|
|
14
|
+
markPlannerHandoffOffered,
|
|
15
|
+
PLANNER_HANDOFF_RULE_NAME,
|
|
16
|
+
} from "../planner-handoff.ts";
|
|
17
|
+
|
|
18
|
+
describe("planner handoff command catalog", () => {
|
|
19
|
+
test("/gsd planner is hidden from description and completions", () => {
|
|
20
|
+
assert.doesNotMatch(GSD_COMMAND_DESCRIPTION, /\|planner(?:\||$)/);
|
|
21
|
+
assert.equal(
|
|
22
|
+
TOP_LEVEL_SUBCOMMANDS.some((command) => command.cmd === "planner"),
|
|
23
|
+
false,
|
|
24
|
+
"planner should not appear in top-level commands",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const completions = getGsdArgumentCompletions("pla");
|
|
28
|
+
|
|
29
|
+
assert.equal(
|
|
30
|
+
completions.some((completion) => completion.value === "planner"),
|
|
31
|
+
false,
|
|
32
|
+
"planner should not appear in top-level completions",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
getGsdArgumentCompletions("planner --"),
|
|
37
|
+
[],
|
|
38
|
+
"planner should not expose nested completions",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("planner handoff command handler", () => {
|
|
44
|
+
test("/gsd planner falls through to the unknown-command path", async () => {
|
|
45
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
46
|
+
const ctx = {
|
|
47
|
+
ui: {
|
|
48
|
+
notify(message: string, level?: string) {
|
|
49
|
+
notifications.push({ message, level });
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handled = await handleCoreCommand("planner M001 --dry-run --inspect", ctx as any);
|
|
55
|
+
|
|
56
|
+
assert.equal(handled, false);
|
|
57
|
+
assert.deepEqual(notifications, []);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("planner handoff launcher", () => {
|
|
62
|
+
test("builds gsd-planner command with project and milestone context", () => {
|
|
63
|
+
const plan = buildGsdPlannerSpawnPlan({
|
|
64
|
+
basePath: "/tmp/project with spaces",
|
|
65
|
+
milestoneId: "M001",
|
|
66
|
+
extraArgs: ["--inspect"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(plan, {
|
|
70
|
+
command: "gsd-planner",
|
|
71
|
+
args: ["--project", "/tmp/project with spaces", "--milestone", "M001", "--inspect"],
|
|
72
|
+
cwd: "/tmp/project with spaces",
|
|
73
|
+
});
|
|
74
|
+
assert.equal(
|
|
75
|
+
formatGsdPlannerCommand(plan),
|
|
76
|
+
'gsd-planner --project "/tmp/project with spaces" --milestone M001 --inspect',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("records one-shot handoff markers per milestone", () => {
|
|
81
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-planner-marker-"));
|
|
82
|
+
try {
|
|
83
|
+
assert.equal(hasPlannerHandoffBeenOffered(basePath, "M001"), false);
|
|
84
|
+
markPlannerHandoffOffered(basePath, "M001");
|
|
85
|
+
assert.equal(hasPlannerHandoffBeenOffered(basePath, "M001"), true);
|
|
86
|
+
assert.equal(hasPlannerHandoffBeenOffered(basePath, "M002"), false);
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("planner handoff dispatch rule", () => {
|
|
94
|
+
test("rule is not registered while /gsd planner is disabled", () => {
|
|
95
|
+
assert.equal(
|
|
96
|
+
DISPATCH_RULES.some((rule) => rule.name === PLANNER_HANDOFF_RULE_NAME),
|
|
97
|
+
false,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -2,7 +2,21 @@ 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
|
+
RUN_UAT_BROWSER_TOOL_NAMES,
|
|
7
|
+
buildRunUatResultPresentation,
|
|
8
|
+
buildRunUatPresentationForType,
|
|
9
|
+
RUN_UAT_READ_ONLY_TOOL_NAMES,
|
|
10
|
+
RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
11
|
+
RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
12
|
+
} from "../tool-presentation-plan.ts";
|
|
13
|
+
import {
|
|
14
|
+
buildMinimalAutoGsdToolSet,
|
|
15
|
+
MINIMAL_AUTO_BASE_TOOL_NAMES,
|
|
16
|
+
MINIMAL_GSD_TOOL_NAMES,
|
|
17
|
+
} from "../bootstrap/register-hooks.ts";
|
|
18
|
+
import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.ts";
|
|
19
|
+
import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.ts";
|
|
6
20
|
|
|
7
21
|
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
8
22
|
const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
|
|
@@ -15,6 +29,84 @@ function readTemplate(name: string): string {
|
|
|
15
29
|
return readFileSync(join(templatesDir, `${name}.md`), "utf-8");
|
|
16
30
|
}
|
|
17
31
|
|
|
32
|
+
function escapeRegExp(value: string): string {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const registeredPhaseToolNames = [
|
|
37
|
+
...new Set([
|
|
38
|
+
...MINIMAL_AUTO_BASE_TOOL_NAMES,
|
|
39
|
+
...MINIMAL_GSD_TOOL_NAMES,
|
|
40
|
+
...Object.values(UNIT_TOOL_CONTRACTS).flatMap((contract) => contract.allowedGsdTools),
|
|
41
|
+
]),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const PHASE_PROMPT_TOOL_CALLS: Record<string, readonly string[]> = {
|
|
45
|
+
"research-milestone": ["gsd_summary_save"],
|
|
46
|
+
"plan-milestone": [
|
|
47
|
+
"gsd_milestone_status",
|
|
48
|
+
"gsd_plan_milestone",
|
|
49
|
+
"gsd_plan_slice",
|
|
50
|
+
"gsd_decision_save",
|
|
51
|
+
],
|
|
52
|
+
"research-slice": ["gsd_summary_save"],
|
|
53
|
+
"plan-slice": ["gsd_reassess_roadmap", "gsd_plan_slice", "gsd_decision_save"],
|
|
54
|
+
"refine-slice": ["gsd_plan_slice", "gsd_decision_save"],
|
|
55
|
+
"replan-slice": ["gsd_replan_slice"],
|
|
56
|
+
"execute-task": ["gsd_task_complete"],
|
|
57
|
+
"reactive-execute": ["gsd_summary_save"],
|
|
58
|
+
"complete-slice": [
|
|
59
|
+
"gsd_exec",
|
|
60
|
+
"gsd_task_reopen",
|
|
61
|
+
"gsd_replan_slice",
|
|
62
|
+
"gsd_requirement_update",
|
|
63
|
+
"capture_thought",
|
|
64
|
+
"gsd_slice_complete",
|
|
65
|
+
"gsd_summary_save",
|
|
66
|
+
],
|
|
67
|
+
"reassess-roadmap": ["gsd_milestone_status", "gsd_reassess_roadmap"],
|
|
68
|
+
"validate-milestone": ["gsd_milestone_status", "gsd_validate_milestone", "gsd_reassess_roadmap"],
|
|
69
|
+
"run-uat": ["gsd_uat_exec", "gsd_uat_result_save"],
|
|
70
|
+
"gate-evaluate": ["gsd_save_gate_result"],
|
|
71
|
+
"complete-milestone": [
|
|
72
|
+
"gsd_milestone_status",
|
|
73
|
+
"gsd_requirement_update",
|
|
74
|
+
"gsd_summary_save",
|
|
75
|
+
"capture_thought",
|
|
76
|
+
"gsd_complete_milestone",
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
test("auto phase prompt tool calls are available in scoped tool surfaces", () => {
|
|
81
|
+
for (const [unitType, promptTools] of Object.entries(PHASE_PROMPT_TOOL_CALLS)) {
|
|
82
|
+
const prompt = readPrompt(unitType);
|
|
83
|
+
const activeTools = buildMinimalAutoGsdToolSet(
|
|
84
|
+
registeredPhaseToolNames,
|
|
85
|
+
unitType,
|
|
86
|
+
registeredPhaseToolNames,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
for (const toolName of promptTools) {
|
|
90
|
+
assert.match(
|
|
91
|
+
prompt,
|
|
92
|
+
new RegExp(`\\b${escapeRegExp(toolName)}\\b`),
|
|
93
|
+
`${unitType} prompt should mention ${toolName}`,
|
|
94
|
+
);
|
|
95
|
+
assert.ok(
|
|
96
|
+
activeTools.includes(toolName),
|
|
97
|
+
`${unitType} prompt mentions ${toolName}, but scoped tools are ${activeTools.join(", ")}`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const scopeResult = shouldBlockAutoUnitToolCall(unitType, toolName);
|
|
101
|
+
assert.equal(
|
|
102
|
+
scopeResult.block,
|
|
103
|
+
false,
|
|
104
|
+
`${unitType} phase gate blocked ${toolName}: ${scopeResult.reason ?? "unknown reason"}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
18
110
|
test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
|
|
19
111
|
const prompt = readPrompt("reactive-execute");
|
|
20
112
|
assert.match(prompt, /subagent-written summary as authoritative/i);
|
|
@@ -29,7 +121,7 @@ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence"
|
|
|
29
121
|
assert.match(prompt, /uatType:\s*"\{\{uatType\}\}"/);
|
|
30
122
|
assert.match(prompt, /gsd_uat_result_save/);
|
|
31
123
|
assert.match(prompt, /presentedTools/);
|
|
32
|
-
assert.match(prompt, /
|
|
124
|
+
assert.match(prompt, /\{\{canonicalPresentation\}\}/);
|
|
33
125
|
assert.match(prompt, /live-runtime/);
|
|
34
126
|
assert.match(prompt, /browser\/runtime\/network/i);
|
|
35
127
|
assert.match(prompt, /NEEDS-HUMAN/);
|
|
@@ -51,16 +143,66 @@ test("run-uat prompt gives the complete UAT result-save presentation contract",
|
|
|
51
143
|
const prompt = readPrompt("run-uat");
|
|
52
144
|
assert.match(prompt, /Call `gsd_uat_result_save` once after all checks are complete/);
|
|
53
145
|
assert.doesNotMatch(prompt, /Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`/);
|
|
146
|
+
assert.match(prompt, /\{\{canonicalPresentation\}\}/);
|
|
147
|
+
assert.match(prompt, /\{\{toolPresentationPlanId\}\}/);
|
|
54
148
|
|
|
149
|
+
const presentation = buildRunUatResultPresentation();
|
|
150
|
+
assert.equal(presentation.toolPresentationPlanId, RUN_UAT_TOOL_PRESENTATION_PLAN_ID);
|
|
55
151
|
for (const toolName of RUN_UAT_WORKFLOW_TOOL_NAMES) {
|
|
56
|
-
assert.ok(
|
|
152
|
+
assert.ok(presentation.presentedTools.includes(toolName), `presentation should include required tool ${toolName}`);
|
|
153
|
+
}
|
|
154
|
+
for (const toolName of RUN_UAT_READ_ONLY_TOOL_NAMES) {
|
|
155
|
+
assert.ok(presentation.presentedTools.includes(toolName), `presentation should include read-only tool ${toolName}`);
|
|
57
156
|
}
|
|
58
157
|
|
|
59
158
|
for (const toolName of ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"] as const) {
|
|
60
|
-
assert.ok(
|
|
159
|
+
assert.ok(
|
|
160
|
+
presentation.blockedTools.some((entry) => entry.name === toolName),
|
|
161
|
+
`presentation should include blocked tool ${toolName}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
assert.ok(
|
|
166
|
+
presentation.blockedTools.every((entry) => entry.reason === "forbidden during run-uat"),
|
|
167
|
+
"presentation should explain blocked run-uat tools",
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("browser-executable UAT presentation uses direct browser tools", () => {
|
|
172
|
+
const presentation = buildRunUatPresentationForType("browser-executable");
|
|
173
|
+
|
|
174
|
+
assert.equal(presentation.surface, "hybrid");
|
|
175
|
+
for (const toolName of RUN_UAT_BROWSER_TOOL_NAMES) {
|
|
176
|
+
assert.ok(presentation.presentedTools.includes(toolName), `presentation should include browser tool ${toolName}`);
|
|
177
|
+
}
|
|
178
|
+
assert.ok(!presentation.presentedTools.some((toolName) => toolName.startsWith("mcp__gsd-browser__")));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("live-runtime and mixed UAT presentations also surface browser tools", () => {
|
|
182
|
+
// Regression (M001/S03): the run-uat prompt tells live-runtime and mixed to
|
|
183
|
+
// drive a browser, so the runner must actually receive the browser tools and
|
|
184
|
+
// a hybrid surface — otherwise live checks silently downgrade to NEEDS-HUMAN.
|
|
185
|
+
for (const uatType of ["live-runtime", "mixed", "human-experience"] as const) {
|
|
186
|
+
const presentation = buildRunUatPresentationForType(uatType);
|
|
187
|
+
assert.equal(presentation.surface, "hybrid", `${uatType} should use the hybrid surface`);
|
|
188
|
+
for (const toolName of RUN_UAT_BROWSER_TOOL_NAMES) {
|
|
189
|
+
assert.ok(
|
|
190
|
+
presentation.presentedTools.includes(toolName),
|
|
191
|
+
`${uatType} presentation should include browser tool ${toolName}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
61
194
|
}
|
|
195
|
+
});
|
|
62
196
|
|
|
63
|
-
|
|
197
|
+
test("artifact-driven and runtime-executable UAT presentations stay browser-free", () => {
|
|
198
|
+
for (const uatType of ["artifact-driven", "runtime-executable"] as const) {
|
|
199
|
+
const presentation = buildRunUatPresentationForType(uatType);
|
|
200
|
+
assert.equal(presentation.surface, "mcp", `${uatType} should use the mcp surface`);
|
|
201
|
+
assert.ok(
|
|
202
|
+
!RUN_UAT_BROWSER_TOOL_NAMES.some((toolName) => presentation.presentedTools.includes(toolName)),
|
|
203
|
+
`${uatType} presentation should not include browser tools`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
64
206
|
});
|
|
65
207
|
|
|
66
208
|
test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
|
|
@@ -210,6 +210,61 @@ test("end-to-end: audit event is emitted when an auto trace is active", async ()
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
test("same-API transform with changes does not fire the observer (no real provider switch)", async () => {
|
|
214
|
+
const { basePath, cleanup } = withTempBasePath();
|
|
215
|
+
try {
|
|
216
|
+
initNotificationStore(basePath);
|
|
217
|
+
installProviderSwitchObserver();
|
|
218
|
+
|
|
219
|
+
// Target api === source api. The conversation ends on an unresolved tool
|
|
220
|
+
// call, so a synthetic tool result IS backfilled (a non-empty report) — but
|
|
221
|
+
// this is a within-provider normalization, not a cross-provider switch.
|
|
222
|
+
// `sourceApi` is omitted (the common case), so fromApi defaults to the
|
|
223
|
+
// target api and equals toApi. The observer must stay silent.
|
|
224
|
+
const sameApiModel = {
|
|
225
|
+
id: "gpt-5",
|
|
226
|
+
name: "GPT-5",
|
|
227
|
+
api: "openai-responses",
|
|
228
|
+
provider: "openai",
|
|
229
|
+
baseUrl: "",
|
|
230
|
+
reasoning: false,
|
|
231
|
+
input: ["text"],
|
|
232
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
233
|
+
contextWindow: 128000,
|
|
234
|
+
maxTokens: 8192,
|
|
235
|
+
} as Parameters<typeof transformMessagesWithReport>[1];
|
|
236
|
+
|
|
237
|
+
const messages = [
|
|
238
|
+
{
|
|
239
|
+
role: "assistant" as const,
|
|
240
|
+
content: [
|
|
241
|
+
{ type: "toolCall" as const, id: "call_orphan_1", name: "bash", arguments: {} },
|
|
242
|
+
],
|
|
243
|
+
api: "openai-responses",
|
|
244
|
+
provider: "openai",
|
|
245
|
+
model: "gpt-5",
|
|
246
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
247
|
+
stopReason: "stop" as const,
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
transformMessagesWithReport(
|
|
253
|
+
messages as Parameters<typeof transformMessagesWithReport>[0],
|
|
254
|
+
sameApiModel,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
assert.equal(getProviderSwitchStats().totalSwitches, 0, "same→same transform must not count as a provider switch");
|
|
258
|
+
assert.equal(
|
|
259
|
+
readNotifications(basePath).filter((n) => n.message.includes("Provider switch")).length,
|
|
260
|
+
0,
|
|
261
|
+
"same→same transform must not emit a provider-switch notification",
|
|
262
|
+
);
|
|
263
|
+
} finally {
|
|
264
|
+
cleanup();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
213
268
|
test("empty report does not bump counter or emit a notification", async () => {
|
|
214
269
|
const { basePath, cleanup } = withTempBasePath();
|
|
215
270
|
try {
|
|
@@ -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,40 @@ 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
|
+
|
|
111
|
+
test("auto Unit tool scope allows plan-slice to reassess invalid roadmap assumptions", () => {
|
|
112
|
+
const result = shouldBlockAutoUnitToolCall("plan-slice", "gsd_reassess_roadmap");
|
|
113
|
+
|
|
114
|
+
assert.equal(result.block, false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("auto Unit tool scope allows status/read helpers named by closeout prompts", () => {
|
|
118
|
+
for (const unitType of ["plan-milestone", "validate-milestone", "complete-milestone", "reassess-roadmap"]) {
|
|
119
|
+
const result = shouldBlockAutoUnitToolCall(unitType, "gsd_milestone_status");
|
|
120
|
+
assert.equal(result.block, false, `${unitType} should be able to call gsd_milestone_status`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("auto Unit tool scope blocks stale per-task planner in slice planning phases", () => {
|
|
125
|
+
for (const unitType of ["plan-slice", "refine-slice", "replan-slice"]) {
|
|
126
|
+
const result = shouldBlockAutoUnitToolCall(unitType, "gsd_plan_task");
|
|
127
|
+
assert.equal(result.block, true, `${unitType} should not call stale gsd_plan_task`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
77
131
|
test("Recovery Classification covers ADR-015 failure families", () => {
|
|
78
132
|
const cases = [
|
|
79
133
|
["invalid tool schema enum", "tool-schema", "stop"],
|
|
134
|
+
["Tool Contract failure: complete-slice cannot use gsd_uat_result_save", "tool-contract", "stop"],
|
|
80
135
|
["deterministic policy rejection", "deterministic-policy", "stop"],
|
|
136
|
+
["cannot legally advance because required UAT Assessment artifact is missing", "lifecycle-progression", "stop"],
|
|
81
137
|
["stale worker lease", "stale-worker", "stop"],
|
|
82
138
|
["worktree root missing .git", "worktree-invalid", "stop"],
|
|
83
139
|
["verification drift in state snapshot", "verification-drift", "escalate"],
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Focused tests for `resolveSkillManifest` and `filterSkillsByManifest`.
|
|
4
4
|
// Covers the wildcard semantics, the newly seeded unit-type entries
|
|
5
5
|
// (complete-milestone, validate-milestone, reassess-roadmap, research-slice,
|
|
6
|
-
// plan-slice, refine-slice, replan-slice, run-uat), and the
|
|
7
|
-
// wildcard fallback for the execute-task hot path (RFC #4779).
|
|
6
|
+
// plan-slice, refine-slice, replan-slice, run-uat, complete-slice), and the
|
|
7
|
+
// deliberate wildcard fallback for the execute-task hot path (RFC #4779).
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
@@ -23,6 +23,7 @@ const NEWLY_WIRED_UNIT_TYPES = [
|
|
|
23
23
|
"refine-slice",
|
|
24
24
|
"replan-slice",
|
|
25
25
|
"run-uat",
|
|
26
|
+
"complete-slice",
|
|
26
27
|
] as const;
|
|
27
28
|
|
|
28
29
|
test("resolveSkillManifest returns null for undefined unit type (wildcard)", () => {
|
|
@@ -65,7 +66,7 @@ test("resolveSkillManifest: slice-level manifests include decompose-into-slices"
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
test("resolveSkillManifest: validation / completion flows include verify-before-complete", () => {
|
|
68
|
-
for (const unitType of ["complete-milestone", "validate-milestone", "run-uat"] as const) {
|
|
69
|
+
for (const unitType of ["complete-milestone", "validate-milestone", "run-uat", "complete-slice"] as const) {
|
|
69
70
|
const allowlist = resolveSkillManifest(unitType);
|
|
70
71
|
assert.ok(
|
|
71
72
|
allowlist?.includes("verify-before-complete"),
|