@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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-reentry.test.ts — Unit tests for reenterActiveWorktreeIfNeeded.
|
|
3
|
+
*
|
|
4
|
+
* Covers the cold-start (/quit + relaunch) path where cwd lands at the project
|
|
5
|
+
* root instead of the active milestone's worktree. The helper should chdir back
|
|
6
|
+
* into the worktree deterministically, and no-op when it shouldn't act.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, beforeEach } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
import { createAutoWorktree, _resetAutoWorktreeOriginalBaseForTests } from "../auto-worktree.ts";
|
|
17
|
+
import { reenterActiveWorktreeIfNeeded } from "../worktree-reentry.ts";
|
|
18
|
+
|
|
19
|
+
// Safe: all inputs below are hardcoded test strings, not user input.
|
|
20
|
+
function git(subArgs: string[], cwd: string): void {
|
|
21
|
+
execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createTempRepo(
|
|
25
|
+
t: { after: (fn: () => void) => void },
|
|
26
|
+
opts: { isolation?: "worktree" | "none" } = {},
|
|
27
|
+
): string {
|
|
28
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-reentry-")));
|
|
29
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
30
|
+
git(["init"], dir);
|
|
31
|
+
git(["config", "user.email", "test@test.com"], dir);
|
|
32
|
+
git(["config", "user.name", "Test"], dir);
|
|
33
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
34
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
35
|
+
if (opts.isolation === "worktree") {
|
|
36
|
+
writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\ngit:\n isolation: worktree\n---\n", "utf-8");
|
|
37
|
+
}
|
|
38
|
+
const msDir = join(dir, ".gsd", "milestones", "M001");
|
|
39
|
+
mkdirSync(msDir, { recursive: true });
|
|
40
|
+
writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
|
|
41
|
+
git(["add", "."], dir);
|
|
42
|
+
git(["commit", "-m", "init"], dir);
|
|
43
|
+
git(["branch", "-M", "main"], dir);
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("reenterActiveWorktreeIfNeeded", () => {
|
|
48
|
+
const savedCwd = process.cwd();
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
52
|
+
process.chdir(savedCwd);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("re-enters the sole live worktree when sitting at the project root", async (t) => {
|
|
56
|
+
const dir = createTempRepo(t, { isolation: "worktree" });
|
|
57
|
+
t.after(() => process.chdir(savedCwd));
|
|
58
|
+
|
|
59
|
+
// createAutoWorktree chdir's INTO the worktree; simulate a cold start by
|
|
60
|
+
// returning to the project root with a clean workspace registry.
|
|
61
|
+
createAutoWorktree(dir, "M001");
|
|
62
|
+
process.chdir(dir);
|
|
63
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
64
|
+
|
|
65
|
+
const entered = await reenterActiveWorktreeIfNeeded(dir);
|
|
66
|
+
assert.ok(entered, "re-entry returned a worktree path");
|
|
67
|
+
assert.strictEqual(realpathSync(process.cwd()), realpathSync(entered!), "cwd moved into the worktree");
|
|
68
|
+
assert.strictEqual(entered, join(dir, ".gsd", "worktrees", "M001"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no-op when already inside a worktree", async (t) => {
|
|
72
|
+
const dir = createTempRepo(t, { isolation: "worktree" });
|
|
73
|
+
t.after(() => process.chdir(savedCwd));
|
|
74
|
+
|
|
75
|
+
createAutoWorktree(dir, "M001"); // leaves cwd inside the worktree
|
|
76
|
+
const cwdBefore = process.cwd();
|
|
77
|
+
|
|
78
|
+
const entered = await reenterActiveWorktreeIfNeeded(dir);
|
|
79
|
+
assert.strictEqual(entered, null, "no re-entry when already in a worktree");
|
|
80
|
+
assert.strictEqual(process.cwd(), cwdBefore, "cwd unchanged");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("no-op when isolation is not worktree", async (t) => {
|
|
84
|
+
const dir = createTempRepo(t, { isolation: "none" });
|
|
85
|
+
t.after(() => process.chdir(savedCwd));
|
|
86
|
+
process.chdir(dir);
|
|
87
|
+
|
|
88
|
+
const entered = await reenterActiveWorktreeIfNeeded(dir);
|
|
89
|
+
assert.strictEqual(entered, null, "isolation=none never re-enters");
|
|
90
|
+
assert.strictEqual(realpathSync(process.cwd()), realpathSync(dir), "cwd stays at project root");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("no-op when there are no worktrees", async (t) => {
|
|
94
|
+
const dir = createTempRepo(t, { isolation: "worktree" });
|
|
95
|
+
t.after(() => process.chdir(savedCwd));
|
|
96
|
+
process.chdir(dir);
|
|
97
|
+
|
|
98
|
+
const entered = await reenterActiveWorktreeIfNeeded(dir);
|
|
99
|
+
assert.strictEqual(entered, null, "nothing to re-enter");
|
|
100
|
+
assert.strictEqual(realpathSync(process.cwd()), realpathSync(dir), "cwd stays at project root");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -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,8 +42,10 @@ 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
|
-
/^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool),
|
|
48
|
+
/^gsd_(?:task|slice|milestone|complete|validate|save|summary|uat)/.test(tool),
|
|
44
49
|
);
|
|
45
50
|
|
|
46
51
|
if (requiresCloseoutTool(unitType) && closeoutTools.length === 0) {
|
|
@@ -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}`,
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Resolve phase-aware tool surfaces for GSD model presentations.
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
RUN_UAT_BROWSER_TOOL_NAMES,
|
|
6
|
+
RUN_UAT_READ_ONLY_TOOL_NAMES,
|
|
7
|
+
RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
8
|
+
RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
9
|
+
} from "./unit-tool-contracts.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
RUN_UAT_BROWSER_TOOL_NAMES,
|
|
13
|
+
RUN_UAT_READ_ONLY_TOOL_NAMES,
|
|
14
|
+
RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
15
|
+
RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
16
|
+
} from "./unit-tool-contracts.js";
|
|
17
|
+
|
|
4
18
|
export type ToolPresentationSurface = "provider-tools" | "claude-code-sdk" | "mcp" | "hybrid";
|
|
5
19
|
|
|
6
20
|
export interface ToolPresentationModel {
|
|
@@ -20,13 +34,12 @@ export interface ToolPresentationPlan {
|
|
|
20
34
|
diagnostics: string[];
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
] as const;
|
|
37
|
+
export interface RunUatResultPresentation {
|
|
38
|
+
surface: ToolPresentationSurface;
|
|
39
|
+
presentedTools: string[];
|
|
40
|
+
blockedTools: Array<{ name: string; reason: string }>;
|
|
41
|
+
toolPresentationPlanId: string;
|
|
42
|
+
}
|
|
30
43
|
|
|
31
44
|
export const RUN_UAT_FORBIDDEN_TOOL_NAMES = [
|
|
32
45
|
"edit",
|
|
@@ -105,10 +118,72 @@ function addBlockedTool(
|
|
|
105
118
|
export function buildRunUatCanonicalToolNames(options: { includeBrowserTools?: readonly string[] } = {}): string[] {
|
|
106
119
|
return dedupe([
|
|
107
120
|
...RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
121
|
+
...RUN_UAT_READ_ONLY_TOOL_NAMES,
|
|
108
122
|
...(options.includeBrowserTools ?? []),
|
|
109
123
|
]);
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
// UAT modes whose run-uat instructions direct the runner to exercise the live
|
|
127
|
+
// app in a browser. These modes receive the browser tool surface so the runner
|
|
128
|
+
// can actually drive the page instead of silently deferring browser checks to a
|
|
129
|
+
// human. See run-uat.md automation rules: `browser-executable`, `live-runtime`,
|
|
130
|
+
// and `mixed` are all told to drive a browser/runtime path, and
|
|
131
|
+
// `human-experience` is told to capture screenshots. Without this, a webpage
|
|
132
|
+
// UAT classified as anything but `browser-executable` had no browser tools and
|
|
133
|
+
// downgraded its live checks to NEEDS-HUMAN (M001/S03 regression).
|
|
134
|
+
export const BROWSER_INCLUSIVE_UAT_TYPES: readonly string[] = [
|
|
135
|
+
"browser-executable",
|
|
136
|
+
"live-runtime",
|
|
137
|
+
"mixed",
|
|
138
|
+
"human-experience",
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
function uatTypeIncludesBrowser(uatType: string | undefined): boolean {
|
|
142
|
+
return uatType !== undefined && BROWSER_INCLUSIVE_UAT_TYPES.includes(uatType);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function runUatBrowserToolsForType(uatType: string | undefined): readonly string[] {
|
|
146
|
+
return uatTypeIncludesBrowser(uatType) ? RUN_UAT_BROWSER_TOOL_NAMES : [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function runUatPresentationSurfaceForType(uatType: string | undefined): ToolPresentationSurface {
|
|
150
|
+
return uatTypeIncludesBrowser(uatType) ? "hybrid" : "mcp";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildRunUatPresentationForType(
|
|
154
|
+
uatType: string | undefined,
|
|
155
|
+
options: {
|
|
156
|
+
surface?: ToolPresentationSurface;
|
|
157
|
+
presentedTools?: readonly string[];
|
|
158
|
+
} = {},
|
|
159
|
+
): RunUatResultPresentation {
|
|
160
|
+
return buildRunUatResultPresentation({
|
|
161
|
+
...options,
|
|
162
|
+
surface: options.surface ?? runUatPresentationSurfaceForType(uatType),
|
|
163
|
+
includeBrowserTools: runUatBrowserToolsForType(uatType),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function buildRunUatResultPresentation(options: {
|
|
168
|
+
surface?: ToolPresentationSurface;
|
|
169
|
+
includeBrowserTools?: readonly string[];
|
|
170
|
+
presentedTools?: readonly string[];
|
|
171
|
+
} = {}): RunUatResultPresentation {
|
|
172
|
+
const presentedTools = options.presentedTools
|
|
173
|
+
? dedupe(options.presentedTools)
|
|
174
|
+
: buildRunUatCanonicalToolNames({ includeBrowserTools: options.includeBrowserTools });
|
|
175
|
+
const blockedTools = RUN_UAT_FORBIDDEN_TOOL_NAMES
|
|
176
|
+
.filter((toolName) => !toolName.includes("*"))
|
|
177
|
+
.map((name) => ({ name, reason: "forbidden during run-uat" }));
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
surface: options.surface ?? "mcp",
|
|
181
|
+
presentedTools,
|
|
182
|
+
blockedTools,
|
|
183
|
+
toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
112
187
|
export function resolveToolPresentationPlan(options: {
|
|
113
188
|
phase: string;
|
|
114
189
|
surface: ToolPresentationSurface;
|
|
@@ -34,7 +34,8 @@ import { getGatesForTurn } from "../gate-registry.js";
|
|
|
34
34
|
import { gsdProjectionRoot, clearPathCache, resolveMilestoneFile } from "../paths.js";
|
|
35
35
|
import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
36
36
|
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
37
|
-
import { saveFile, clearParseCache } from "../files.js";
|
|
37
|
+
import { saveFile, clearParseCache, extractUatType } from "../files.js";
|
|
38
|
+
import { hasBrowserRequiredText } from "../browser-evidence.js";
|
|
38
39
|
import { invalidateStateCache } from "../state.js";
|
|
39
40
|
import { renderRoadmapFromDb } from "../markdown-renderer.js";
|
|
40
41
|
import { parseRoadmap } from "../parsers-legacy.js";
|
|
@@ -342,6 +343,33 @@ export async function handleCompleteSlice(
|
|
|
342
343
|
return { error: `slice verification indicates blocked/failed state — do not complete a slice that has not passed verification. Address the blockers and re-verify first.` };
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
// ── Browser/web UAT classification gate ────────────────────────────────
|
|
347
|
+
// A UAT that drives a running web UI (opening a page in a browser,
|
|
348
|
+
// navigating to a page/localhost) must declare a browser-capable mode so the
|
|
349
|
+
// run-uat runner surfaces browser tools and actually launches a browser.
|
|
350
|
+
// Otherwise the browser checks get silently deferred to a human and the slice
|
|
351
|
+
// passes on static checks alone (M001/S03 regression). `browser-executable`,
|
|
352
|
+
// `live-runtime`, and `mixed` all receive browser tools (see
|
|
353
|
+
// BROWSER_INCLUSIVE_UAT_TYPES); only the non-browser modes are rejected here.
|
|
354
|
+
//
|
|
355
|
+
// Reuse the canonical hasBrowserRequiredText detector (also used by dispatch
|
|
356
|
+
// and milestone validation): it skips Not-Proven/Out-of-Scope disclaimer
|
|
357
|
+
// sections and only treats verbs like navigate/open as web when they sit next
|
|
358
|
+
// to browser/page/localhost — avoiding false positives on CLI/file/API steps.
|
|
359
|
+
//
|
|
360
|
+
// Only `artifact-driven` is gated. It is the one mode that performs no
|
|
361
|
+
// execution at all (static/file checks), so a browser-requiring UAT under it
|
|
362
|
+
// genuinely defers verification to a human. Every other mode has a real
|
|
363
|
+
// verification path: `runtime-executable` runs browser test commands like
|
|
364
|
+
// `npx playwright test` via gsd_uat_exec, and live-runtime/mixed/
|
|
365
|
+
// browser-executable receive browser tools (BROWSER_INCLUSIVE_UAT_TYPES).
|
|
366
|
+
const declaredUatMode = extractUatType(params.uatContent || "") ?? "artifact-driven";
|
|
367
|
+
if (declaredUatMode === "artifact-driven" && hasBrowserRequiredText(params.uatContent || "")) {
|
|
368
|
+
return {
|
|
369
|
+
error: `UAT requires browser verification (opening a page in a browser, navigating to a page or localhost, screenshots) but declares "UAT mode: artifact-driven", which only runs static/file checks and would defer the browser work to a human. Use a mode that actually verifies the UI: "browser-executable" (interactive browser tools), "runtime-executable" (a browser test command such as playwright), or a browser-inclusive "mixed"/"live-runtime". Re-author the UAT Type section and complete the slice again.`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
345
373
|
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
346
374
|
const completedAt = new Date().toISOString();
|
|
347
375
|
let guardError: string | null = null;
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
} from "../gsd-db.js";
|
|
18
18
|
import { GATE_REGISTRY } from "../gate-registry.js";
|
|
19
19
|
import { generateRequirementsMd, saveArtifactToDb } from "../db-writer.js";
|
|
20
|
-
import { clearPathCache, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
|
|
20
|
+
import { clearPathCache, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
|
|
21
21
|
import { saveFile, clearParseCache } from "../files.js";
|
|
22
|
+
import { buildManualValidationGuidance, resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
22
23
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
23
24
|
import { isAbsolute, join, resolve } from "node:path";
|
|
24
25
|
import type { CompleteMilestoneParams } from "./complete-milestone.js";
|
|
@@ -48,9 +49,11 @@ import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
|
48
49
|
import { parseProject } from "../schemas/parsers.js";
|
|
49
50
|
import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
|
|
50
51
|
import {
|
|
52
|
+
buildRunUatPresentationForType,
|
|
51
53
|
canonicalWorkflowToolName,
|
|
52
54
|
parseMcpToolName,
|
|
53
55
|
RUN_UAT_FORBIDDEN_TOOL_NAMES,
|
|
56
|
+
RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
54
57
|
RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
55
58
|
} from "../tool-presentation-plan.js";
|
|
56
59
|
|
|
@@ -90,7 +93,7 @@ function blockIfWrongAutoUnit(requiredUnitType: string, operation: string): Tool
|
|
|
90
93
|
if (!snapshot.active || !snapshot.currentUnit) return null;
|
|
91
94
|
if (snapshot.currentUnit.type === requiredUnitType) return null;
|
|
92
95
|
|
|
93
|
-
const error = `HARD BLOCK: ${operation} may only run from ${requiredUnitType}; active unit is ${snapshot.currentUnit.type}. The orchestrator owns phase transitions.`;
|
|
96
|
+
const error = `HARD BLOCK: Tool Contract failure: ${operation} may only run from ${requiredUnitType}; active unit is ${snapshot.currentUnit.type}. Fix unit-tool-contracts.ts or the active Unit prompt. The orchestrator owns phase transitions.`;
|
|
94
97
|
return {
|
|
95
98
|
content: [{ type: "text", text: error }],
|
|
96
99
|
details: { operation, error },
|
|
@@ -178,7 +181,11 @@ export async function executeSummarySave(
|
|
|
178
181
|
if (rootArtifactGuard.block) {
|
|
179
182
|
return {
|
|
180
183
|
content: [{ type: "text", text: `Error saving artifact: ${rootArtifactGuard.reason ?? "root artifact write blocked"}` }],
|
|
181
|
-
details: {
|
|
184
|
+
details: {
|
|
185
|
+
operation: "save_summary",
|
|
186
|
+
error: "root_artifact_write_blocked",
|
|
187
|
+
displayReason: "Approval confirmation required before saving final project setup artifacts.",
|
|
188
|
+
},
|
|
182
189
|
isError: true,
|
|
183
190
|
};
|
|
184
191
|
}
|
|
@@ -191,9 +198,13 @@ export async function executeSummarySave(
|
|
|
191
198
|
if (contextGuard.block) {
|
|
192
199
|
return {
|
|
193
200
|
content: [{ type: "text", text: `Error saving artifact: ${contextGuard.reason ?? "context write blocked"}` }],
|
|
194
|
-
details: {
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
details: {
|
|
202
|
+
operation: "save_summary",
|
|
203
|
+
error: "context_write_blocked",
|
|
204
|
+
displayReason: "Depth check required before writing milestone context.",
|
|
205
|
+
},
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
197
208
|
}
|
|
198
209
|
try {
|
|
199
210
|
let relativePath: string;
|
|
@@ -441,6 +452,9 @@ export interface UatEvidenceRef {
|
|
|
441
452
|
kind: "gsd_uat_exec" | "gsd_exec" | "screenshot" | "log" | "url" | "browser";
|
|
442
453
|
ref: string;
|
|
443
454
|
note?: string;
|
|
455
|
+
unitType?: string;
|
|
456
|
+
tool?: string;
|
|
457
|
+
executionId?: string;
|
|
444
458
|
}
|
|
445
459
|
|
|
446
460
|
export interface UatCheckResultInput {
|
|
@@ -1008,10 +1022,68 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
1008
1022
|
return typeof value === "string" && value.trim().length > 0;
|
|
1009
1023
|
}
|
|
1010
1024
|
|
|
1025
|
+
function mergeBlockedTools(
|
|
1026
|
+
current: UatPresentationInput["blockedTools"] | undefined,
|
|
1027
|
+
canonical: UatPresentationInput["blockedTools"],
|
|
1028
|
+
): UatPresentationInput["blockedTools"] {
|
|
1029
|
+
const merged = new Map<string, { name: string; reason: string }>();
|
|
1030
|
+
for (const entry of [...(current ?? []), ...canonical]) {
|
|
1031
|
+
merged.set(canonicalWorkflowToolName(parseMcpToolName(entry.name)?.tool ?? entry.name), entry);
|
|
1032
|
+
}
|
|
1033
|
+
return [...merged.values()];
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function mergePresentedTools(current: readonly string[] | undefined, canonical: readonly string[]): string[] {
|
|
1037
|
+
return [...new Set([...(current ?? []), ...canonical])];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function normalizeUatVerdict(params: UatResultSaveParams): UatResultSaveParams {
|
|
1041
|
+
const raw = params as Partial<UatResultSaveParams> & Record<string, unknown>;
|
|
1042
|
+
if (typeof raw.verdict === "string") {
|
|
1043
|
+
return { ...params, verdict: raw.verdict.toUpperCase() as UatVerdict };
|
|
1044
|
+
}
|
|
1045
|
+
return params;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function supplyDefaultPresentation(params: UatResultSaveParams): UatResultSaveParams {
|
|
1049
|
+
const raw = params as Partial<UatResultSaveParams> & Record<string, unknown>;
|
|
1050
|
+
if (!raw.presentation) {
|
|
1051
|
+
return { ...params, presentation: buildRunUatPresentationForType(params.uatType) };
|
|
1052
|
+
}
|
|
1053
|
+
return params;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function mergeCanonicalPresentation(params: UatResultSaveParams): UatResultSaveParams {
|
|
1057
|
+
const canonicalPresentation = buildRunUatPresentationForType(params.uatType);
|
|
1058
|
+
const providedPresentation = params.presentation as Partial<UatPresentationInput>;
|
|
1059
|
+
return {
|
|
1060
|
+
...params,
|
|
1061
|
+
presentation: {
|
|
1062
|
+
...providedPresentation,
|
|
1063
|
+
surface: providedPresentation.surface ?? canonicalPresentation.surface,
|
|
1064
|
+
presentedTools: mergePresentedTools(providedPresentation.presentedTools, canonicalPresentation.presentedTools),
|
|
1065
|
+
blockedTools: mergeBlockedTools(providedPresentation.blockedTools, canonicalPresentation.blockedTools),
|
|
1066
|
+
toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
1067
|
+
} as UatPresentationInput,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const VALID_UAT_TYPES: readonly UatType[] = [
|
|
1072
|
+
"artifact-driven",
|
|
1073
|
+
"browser-executable",
|
|
1074
|
+
"runtime-executable",
|
|
1075
|
+
"live-runtime",
|
|
1076
|
+
"mixed",
|
|
1077
|
+
"human-experience",
|
|
1078
|
+
];
|
|
1079
|
+
|
|
1011
1080
|
function ensureUatRequiredFields(params: UatResultSaveParams): string | null {
|
|
1012
1081
|
if (!isNonEmptyString(params.milestoneId)) return "milestoneId is required";
|
|
1013
1082
|
if (!isNonEmptyString(params.sliceId)) return "sliceId is required";
|
|
1014
1083
|
if (!isNonEmptyString(params.uatType)) return "uatType is required";
|
|
1084
|
+
if (!(VALID_UAT_TYPES as readonly string[]).includes(params.uatType)) {
|
|
1085
|
+
return `uatType must be one of: ${VALID_UAT_TYPES.join(", ")}`;
|
|
1086
|
+
}
|
|
1015
1087
|
if (!["PASS", "FAIL", "PARTIAL"].includes(params.verdict)) return "verdict must be PASS, FAIL, or PARTIAL";
|
|
1016
1088
|
if (!Array.isArray(params.checks) || params.checks.length === 0) return "checks must contain at least one UAT check";
|
|
1017
1089
|
if (!params.presentation || !Array.isArray(params.presentation.presentedTools)) return "presentation.presentedTools is required";
|
|
@@ -1147,6 +1219,15 @@ function validateUatChecks(basePath: string, params: UatResultSaveParams): strin
|
|
|
1147
1219
|
return null;
|
|
1148
1220
|
}
|
|
1149
1221
|
|
|
1222
|
+
function validateFreshUatOwnedEvidence(params: UatResultSaveParams): string | null {
|
|
1223
|
+
const hasFreshUatEvidence = params.checks.some((check) =>
|
|
1224
|
+
(check.evidence ?? []).some((evidence) => evidence.kind === "gsd_uat_exec")
|
|
1225
|
+
);
|
|
1226
|
+
return hasFreshUatEvidence
|
|
1227
|
+
? null
|
|
1228
|
+
: "UAT Assessment requires at least one fresh gsd_uat_exec evidence reference from run-uat";
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1150
1231
|
function validateUatMode(params: UatResultSaveParams): string | null {
|
|
1151
1232
|
const modes = new Set(params.checks.map((check) => check.mode));
|
|
1152
1233
|
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
@@ -1257,7 +1338,12 @@ function escapeMarkdownTableCell(value: unknown): string {
|
|
|
1257
1338
|
.replace(/\r?\n/g, "<br>");
|
|
1258
1339
|
}
|
|
1259
1340
|
|
|
1260
|
-
function renderUatAssessment(
|
|
1341
|
+
function renderUatAssessment(
|
|
1342
|
+
params: UatResultSaveParams,
|
|
1343
|
+
attempt: number,
|
|
1344
|
+
gateVerdict: "pass" | "flag",
|
|
1345
|
+
basePath: string,
|
|
1346
|
+
): string {
|
|
1261
1347
|
const lines = [
|
|
1262
1348
|
"---",
|
|
1263
1349
|
`sliceId: ${params.sliceId}`,
|
|
@@ -1292,6 +1378,27 @@ function renderUatAssessment(params: UatResultSaveParams, attempt: number, gateV
|
|
|
1292
1378
|
"",
|
|
1293
1379
|
`Aggregate UAT gate saved as ${gateVerdict}.`,
|
|
1294
1380
|
];
|
|
1381
|
+
|
|
1382
|
+
// When any check still needs a human, point them at the exact checkout to
|
|
1383
|
+
// validate — critical for worktree milestones whose code sits under a hidden
|
|
1384
|
+
// `.gsd/worktrees/` path the reviewer would otherwise have to hunt for.
|
|
1385
|
+
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
1386
|
+
if (hasHuman) {
|
|
1387
|
+
const guidance = buildManualValidationGuidance(basePath, params.milestoneId, {
|
|
1388
|
+
uatPath: relSliceFile(basePath, params.milestoneId, params.sliceId, "UAT"),
|
|
1389
|
+
});
|
|
1390
|
+
if (guidance) {
|
|
1391
|
+
lines.push(
|
|
1392
|
+
"",
|
|
1393
|
+
"## Manual Validation",
|
|
1394
|
+
"",
|
|
1395
|
+
"One or more checks are marked `NEEDS-HUMAN` and require a person to validate:",
|
|
1396
|
+
"",
|
|
1397
|
+
...guidance.split("\n").map((line) => `- ${line}`),
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1295
1402
|
return `${lines.join("\n")}\n`;
|
|
1296
1403
|
}
|
|
1297
1404
|
|
|
@@ -1306,15 +1413,30 @@ export async function executeUatResultSave(
|
|
|
1306
1413
|
params: UatResultSaveParams,
|
|
1307
1414
|
basePath: string = process.cwd(),
|
|
1308
1415
|
): Promise<ToolExecutionResult> {
|
|
1416
|
+
const unitGuard = blockIfWrongAutoUnit("run-uat", "save_uat_result");
|
|
1417
|
+
if (unitGuard) return unitGuard;
|
|
1418
|
+
|
|
1419
|
+
// Phase 1: normalize verdict and supply the canonical presentation when none was provided.
|
|
1420
|
+
params = normalizeUatVerdict(params);
|
|
1421
|
+
params = supplyDefaultPresentation(params);
|
|
1422
|
+
|
|
1309
1423
|
const dbAvailable = await ensureDbOpen(basePath);
|
|
1310
1424
|
if (!dbAvailable) return errorResult("save_uat_result", "GSD database is not available.", "db_unavailable");
|
|
1311
1425
|
|
|
1426
|
+
// Phase 2: validate the submitted presentation before the canonical merge so that
|
|
1427
|
+
// presentations missing required workflow tools are rejected rather than silently patched.
|
|
1312
1428
|
const requiredError = ensureUatRequiredFields(params);
|
|
1313
1429
|
if (requiredError) return errorResult("save_uat_result", requiredError, "invalid_params");
|
|
1314
1430
|
const presentationError = validateCanonicalPresentation(params);
|
|
1315
1431
|
if (presentationError) return errorResult("save_uat_result", presentationError, "alias_tool_name");
|
|
1432
|
+
|
|
1433
|
+
// Phase 3: merge in the canonical plan ID and read-only audit tools so the persisted
|
|
1434
|
+
// artifact always carries the full audit surface even when the provider omitted them.
|
|
1435
|
+
params = mergeCanonicalPresentation(params);
|
|
1316
1436
|
const checkError = validateUatChecks(basePath, params);
|
|
1317
1437
|
if (checkError) return errorResult("save_uat_result", checkError, "invalid_evidence");
|
|
1438
|
+
const freshEvidenceError = validateFreshUatOwnedEvidence(params);
|
|
1439
|
+
if (freshEvidenceError) return errorResult("save_uat_result", freshEvidenceError, "missing_fresh_uat_evidence");
|
|
1318
1440
|
const modeError = validateUatMode(params);
|
|
1319
1441
|
if (modeError) return errorResult("save_uat_result", modeError, "uat_mode_mismatch");
|
|
1320
1442
|
|
|
@@ -1329,7 +1451,7 @@ export async function executeUatResultSave(
|
|
|
1329
1451
|
}
|
|
1330
1452
|
const gateVerdict = params.verdict === "PASS" ? "pass" : "flag";
|
|
1331
1453
|
const rationale = params.notes ?? `UAT ${params.verdict} for ${params.sliceId}.`;
|
|
1332
|
-
const assessment = renderUatAssessment(params, attempt, gateVerdict);
|
|
1454
|
+
const assessment = renderUatAssessment(params, attempt, gateVerdict, basePath);
|
|
1333
1455
|
const summary = await executeSummarySave(
|
|
1334
1456
|
{
|
|
1335
1457
|
milestone_id: params.milestoneId,
|
|
@@ -1373,8 +1495,20 @@ export async function executeUatResultSave(
|
|
|
1373
1495
|
evaluatedAt,
|
|
1374
1496
|
});
|
|
1375
1497
|
invalidateStateCache();
|
|
1498
|
+
// Surface where to validate when checks are left for a human, so the path
|
|
1499
|
+
// (often a buried worktree checkout) reaches the reviewer, not just the file.
|
|
1500
|
+
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
1501
|
+
const manualGuidance = hasHuman
|
|
1502
|
+
? buildManualValidationGuidance(basePath, params.milestoneId, {
|
|
1503
|
+
uatPath: relSliceFile(basePath, params.milestoneId, params.sliceId, "UAT"),
|
|
1504
|
+
})
|
|
1505
|
+
: null;
|
|
1506
|
+
const savedText = `UAT result saved for ${params.milestoneId}/${params.sliceId}: ${params.verdict}`;
|
|
1376
1507
|
return {
|
|
1377
|
-
content: [{
|
|
1508
|
+
content: [{
|
|
1509
|
+
type: "text",
|
|
1510
|
+
text: manualGuidance ? `${savedText}\n\nManual validation needed:\n${manualGuidance}` : savedText,
|
|
1511
|
+
}],
|
|
1378
1512
|
details: {
|
|
1379
1513
|
operation: "save_uat_result",
|
|
1380
1514
|
milestoneId: params.milestoneId,
|
|
@@ -1384,6 +1518,9 @@ export async function executeUatResultSave(
|
|
|
1384
1518
|
attempt,
|
|
1385
1519
|
attemptPath,
|
|
1386
1520
|
recommendedNextUnit: params.verdict === "PASS" ? null : "reactive-execute",
|
|
1521
|
+
...(hasHuman
|
|
1522
|
+
? { manualValidationPath: resolveCanonicalMilestoneRoot(basePath, params.milestoneId) }
|
|
1523
|
+
: {}),
|
|
1387
1524
|
},
|
|
1388
1525
|
};
|
|
1389
1526
|
} catch (err) {
|