@opengsd/gsd-pi 1.1.1-dev.75048e7 → 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 +10 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
- package/dist/resources/extensions/gsd/auto-timers.js +24 -10
- package/dist/resources/extensions/gsd/auto.js +26 -4
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +29 -21
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
- 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 +4 -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 +2 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
- package/dist/resources/extensions/gsd/tool-contract.js +1 -1
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +19 -2
- package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +32 -4
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +38 -14
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -3
- 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 +8 -8
- 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 +8 -8
- 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/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +158 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +149 -9
- 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/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 +34 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
- package/src/resources/extensions/gsd/auto-timers.ts +25 -9
- package/src/resources/extensions/gsd/auto.ts +28 -4
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +40 -21
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
- 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 +4 -1
- 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 +2 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- 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-supervisor.test.mjs +4 -0
- package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -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-rule-coverage.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +1 -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/planner-handoff.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +113 -1
- package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +131 -2
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
- package/src/resources/extensions/gsd/tool-contract.ts +1 -1
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +21 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +46 -4
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +38 -14
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -3
- 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/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
|
@@ -48,12 +48,52 @@ test("auto execute-task requires canonical task completion tool", () => {
|
|
|
48
48
|
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("execute-task"), ["gsd_task_complete"]);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test("plan-slice requires planning and roadmap reassessment tools", () => {
|
|
52
|
+
const expected = ["gsd_plan_slice", "gsd_reassess_roadmap"];
|
|
53
|
+
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("plan-slice"), expected);
|
|
54
|
+
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("plan-slice"), expected);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("plan-milestone requires status, roadmap, and single-slice planning tools", () => {
|
|
58
|
+
const expected = ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice"];
|
|
59
|
+
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("plan-milestone"), expected);
|
|
60
|
+
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("plan-milestone"), expected);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("refine-slice requires canonical slice planning tool", () => {
|
|
64
|
+
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("refine-slice"), ["gsd_plan_slice"]);
|
|
65
|
+
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("refine-slice"), ["gsd_plan_slice"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
51
68
|
test("complete-slice requires closeout and execution handoff tools", () => {
|
|
52
|
-
const expected = [
|
|
69
|
+
const expected = [
|
|
70
|
+
"gsd_slice_complete",
|
|
71
|
+
"gsd_task_reopen",
|
|
72
|
+
"gsd_replan_slice",
|
|
73
|
+
"gsd_requirement_update",
|
|
74
|
+
"gsd_summary_save",
|
|
75
|
+
];
|
|
53
76
|
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("complete-slice"), expected);
|
|
54
77
|
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("complete-slice"), expected);
|
|
55
78
|
});
|
|
56
79
|
|
|
80
|
+
test("complete-milestone requires status, requirement, project refresh, and closeout tools", () => {
|
|
81
|
+
const expected = [
|
|
82
|
+
"gsd_milestone_status",
|
|
83
|
+
"gsd_requirement_update",
|
|
84
|
+
"gsd_summary_save",
|
|
85
|
+
"gsd_complete_milestone",
|
|
86
|
+
];
|
|
87
|
+
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("complete-milestone"), expected);
|
|
88
|
+
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("complete-milestone"), expected);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("reactive-execute requires task completion and failed-task summary tools", () => {
|
|
92
|
+
const expected = ["gsd_task_complete", "gsd_summary_save"];
|
|
93
|
+
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("reactive-execute"), expected);
|
|
94
|
+
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("reactive-execute"), expected);
|
|
95
|
+
});
|
|
96
|
+
|
|
57
97
|
test("workflow MCP capability surface includes native legacy gsd aliases", () => {
|
|
58
98
|
const err = getWorkflowTransportSupportError(
|
|
59
99
|
"claude-code",
|
|
@@ -679,7 +719,7 @@ test("transport compatibility ignores API-backed providers", () => {
|
|
|
679
719
|
test("transport compatibility now allows plan-slice over workflow MCP surface", () => {
|
|
680
720
|
const error = getWorkflowTransportSupportError(
|
|
681
721
|
"claude-code",
|
|
682
|
-
|
|
722
|
+
getRequiredWorkflowToolsForAutoUnit("plan-slice"),
|
|
683
723
|
{
|
|
684
724
|
projectRoot: "/tmp/project",
|
|
685
725
|
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
|
@@ -696,7 +736,7 @@ test("transport compatibility now allows plan-slice over workflow MCP surface",
|
|
|
696
736
|
test("transport compatibility now allows complete-slice over workflow MCP surface", () => {
|
|
697
737
|
const error = getWorkflowTransportSupportError(
|
|
698
738
|
"claude-code",
|
|
699
|
-
|
|
739
|
+
getRequiredWorkflowToolsForAutoUnit("complete-slice"),
|
|
700
740
|
{
|
|
701
741
|
projectRoot: "/tmp/project",
|
|
702
742
|
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
|
@@ -747,7 +787,7 @@ test("transport compatibility now allows gate-evaluate over workflow MCP surface
|
|
|
747
787
|
test("transport compatibility now allows validate-milestone over workflow MCP surface", () => {
|
|
748
788
|
const error = getWorkflowTransportSupportError(
|
|
749
789
|
"claude-code",
|
|
750
|
-
|
|
790
|
+
getRequiredWorkflowToolsForAutoUnit("validate-milestone"),
|
|
751
791
|
{
|
|
752
792
|
projectRoot: "/tmp/project",
|
|
753
793
|
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
|
@@ -764,7 +804,7 @@ test("transport compatibility now allows validate-milestone over workflow MCP su
|
|
|
764
804
|
test("transport compatibility now allows complete-milestone over workflow MCP surface", () => {
|
|
765
805
|
const error = getWorkflowTransportSupportError(
|
|
766
806
|
"claude-code",
|
|
767
|
-
|
|
807
|
+
getRequiredWorkflowToolsForAutoUnit("complete-milestone"),
|
|
768
808
|
{
|
|
769
809
|
projectRoot: "/tmp/project",
|
|
770
810
|
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
|
@@ -795,7 +835,7 @@ test("transport compatibility now allows replan-slice over workflow MCP surface"
|
|
|
795
835
|
assert.equal(error, null);
|
|
796
836
|
});
|
|
797
837
|
|
|
798
|
-
test("transport compatibility
|
|
838
|
+
test("transport compatibility rejects MCP tools not connected in active tool surface", () => {
|
|
799
839
|
const error = getWorkflowTransportSupportError(
|
|
800
840
|
"claude-code",
|
|
801
841
|
["gsd_summary_save"],
|
|
@@ -810,10 +850,10 @@ test("transport compatibility accepts workflow MCP tools absent from parent acti
|
|
|
810
850
|
},
|
|
811
851
|
);
|
|
812
852
|
|
|
813
|
-
assert.
|
|
853
|
+
assert.match(error ?? "", /requires gsd_summary_save/);
|
|
814
854
|
});
|
|
815
855
|
|
|
816
|
-
test("transport compatibility
|
|
856
|
+
test("transport compatibility checks all required tools against active tool surface", () => {
|
|
817
857
|
const error = getWorkflowTransportSupportError(
|
|
818
858
|
"claude-code",
|
|
819
859
|
["gsd_summary_save", "secure_env_collect"],
|
|
@@ -828,8 +868,9 @@ test("transport compatibility still checks non-MCP tools against parent active t
|
|
|
828
868
|
},
|
|
829
869
|
);
|
|
830
870
|
|
|
831
|
-
assert.match(error ?? "", /requires
|
|
832
|
-
assert.
|
|
871
|
+
assert.match(error ?? "", /requires.*(?:gsd_summary_save|secure_env_collect)/);
|
|
872
|
+
assert.match(error ?? "", /gsd_summary_save/);
|
|
873
|
+
assert.match(error ?? "", /secure_env_collect/);
|
|
833
874
|
});
|
|
834
875
|
|
|
835
876
|
test("transport compatibility still blocks units whose MCP tools are not exposed", () => {
|
|
@@ -850,6 +891,32 @@ test("transport compatibility still blocks units whose MCP tools are not exposed
|
|
|
850
891
|
assert.match(error ?? "", /currently exposes only/);
|
|
851
892
|
});
|
|
852
893
|
|
|
894
|
+
test("discuss-milestone guided flow does not abort when all required tools are on MCP surface (regression #469)", () => {
|
|
895
|
+
// Guided flow starts the workflow MCP server as part of dispatch, so the
|
|
896
|
+
// parent session active-tool list is not authoritative for MCP tools.
|
|
897
|
+
const discussMilestoneTools = [
|
|
898
|
+
"gsd_summary_save",
|
|
899
|
+
"gsd_requirement_save",
|
|
900
|
+
"gsd_requirement_update",
|
|
901
|
+
"gsd_plan_milestone",
|
|
902
|
+
"gsd_milestone_generate_id",
|
|
903
|
+
];
|
|
904
|
+
const error = getWorkflowTransportSupportError(
|
|
905
|
+
"claude-code",
|
|
906
|
+
discussMilestoneTools,
|
|
907
|
+
{
|
|
908
|
+
projectRoot: "/tmp/project",
|
|
909
|
+
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
|
910
|
+
surface: "guided flow",
|
|
911
|
+
unitType: "discuss-milestone",
|
|
912
|
+
authMode: "externalCli",
|
|
913
|
+
baseUrl: "local://claude-code",
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
assert.equal(error, null);
|
|
918
|
+
});
|
|
919
|
+
|
|
853
920
|
test("transport compatibility accepts MCP-namespaced runtime tools", () => {
|
|
854
921
|
const error = getWorkflowTransportSupportError(
|
|
855
922
|
"claude-code",
|
|
@@ -737,13 +737,13 @@ test("executeUatResultSave supplies direct browser tools for browser-executable
|
|
|
737
737
|
verdict: "PASS",
|
|
738
738
|
checks: [{
|
|
739
739
|
id: "UAT-01",
|
|
740
|
-
description: "Browser flow used
|
|
740
|
+
description: "Browser flow used browser tools",
|
|
741
741
|
mode: "browser",
|
|
742
742
|
result: "PASS",
|
|
743
743
|
evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
|
|
744
744
|
notes: "Browser check passed.",
|
|
745
745
|
}],
|
|
746
|
-
notes: "UAT passed with
|
|
746
|
+
notes: "UAT passed with browser evidence.",
|
|
747
747
|
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
748
748
|
|
|
749
749
|
assert.equal(result.isError, undefined);
|
|
@@ -836,6 +836,135 @@ test("executeUatResultSave merges canonical plan ID and read-only tools when pre
|
|
|
836
836
|
}
|
|
837
837
|
});
|
|
838
838
|
|
|
839
|
+
test("executeUatResultSave surfaces the worktree validation path for NEEDS-HUMAN checks", async () => {
|
|
840
|
+
const base = makeTmpBase();
|
|
841
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
842
|
+
const worktreeExecDir = join(worktree, ".gsd", "exec");
|
|
843
|
+
const evidenceId = "uat-human-validation-evidence";
|
|
844
|
+
try {
|
|
845
|
+
openTestDb(base);
|
|
846
|
+
seedMilestone("M001", "Milestone One");
|
|
847
|
+
seedSlice("M001", "S07", "complete");
|
|
848
|
+
mkdirSync(worktreeExecDir, { recursive: true });
|
|
849
|
+
writeFileSync(
|
|
850
|
+
join(worktreeExecDir, `${evidenceId}.meta.json`),
|
|
851
|
+
JSON.stringify({
|
|
852
|
+
id: evidenceId,
|
|
853
|
+
metadata: {
|
|
854
|
+
kind: "uat_exec",
|
|
855
|
+
milestoneId: "M001",
|
|
856
|
+
sliceId: "S07",
|
|
857
|
+
checkId: "UAT-01",
|
|
858
|
+
intent: "uat-runtime-check",
|
|
859
|
+
},
|
|
860
|
+
}),
|
|
861
|
+
"utf-8",
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
865
|
+
milestoneId: "M001",
|
|
866
|
+
sliceId: "S07",
|
|
867
|
+
uatType: "human-experience",
|
|
868
|
+
verdict: "PASS",
|
|
869
|
+
checks: [
|
|
870
|
+
{
|
|
871
|
+
id: "UAT-01",
|
|
872
|
+
description: "Service boots and renders the dashboard",
|
|
873
|
+
mode: "runtime",
|
|
874
|
+
result: "PASS",
|
|
875
|
+
evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
|
|
876
|
+
notes: "Boot check passed.",
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
id: "UAT-02",
|
|
880
|
+
description: "Dashboard layout feels balanced",
|
|
881
|
+
mode: "human-follow-up",
|
|
882
|
+
result: "NEEDS-HUMAN",
|
|
883
|
+
nonAutomatable: true,
|
|
884
|
+
notes: "Open the app and eyeball the spacing.",
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
notes: "Automatable checks passed; layout taste needs a human.",
|
|
888
|
+
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
889
|
+
|
|
890
|
+
assert.equal(result.isError, undefined);
|
|
891
|
+
assert.equal(result.details.verdict, "PASS");
|
|
892
|
+
// The reviewer needs the buried worktree checkout path, not just the file.
|
|
893
|
+
assert.equal(result.details.manualValidationPath, worktree);
|
|
894
|
+
const returnedText = (result.content[0] as { text: string }).text;
|
|
895
|
+
assert.match(returnedText, /Manual validation needed/);
|
|
896
|
+
assert.ok(returnedText.includes(worktree), "tool return should include the worktree path");
|
|
897
|
+
|
|
898
|
+
const assessment = readFileSync(
|
|
899
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S07", "S07-ASSESSMENT.md"),
|
|
900
|
+
"utf-8",
|
|
901
|
+
);
|
|
902
|
+
assert.match(assessment, /## Manual Validation/);
|
|
903
|
+
assert.ok(assessment.includes(worktree), "assessment should include the worktree checkout path");
|
|
904
|
+
assert.match(assessment, /git worktree/);
|
|
905
|
+
} finally {
|
|
906
|
+
closeDatabase();
|
|
907
|
+
cleanup(base);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test("executeUatResultSave omits manual-validation guidance when no human checks remain", async () => {
|
|
912
|
+
const base = makeTmpBase();
|
|
913
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
914
|
+
const worktreeExecDir = join(worktree, ".gsd", "exec");
|
|
915
|
+
const evidenceId = "uat-no-human-evidence";
|
|
916
|
+
try {
|
|
917
|
+
openTestDb(base);
|
|
918
|
+
seedMilestone("M001", "Milestone One");
|
|
919
|
+
seedSlice("M001", "S08", "complete");
|
|
920
|
+
mkdirSync(worktreeExecDir, { recursive: true });
|
|
921
|
+
writeFileSync(
|
|
922
|
+
join(worktreeExecDir, `${evidenceId}.meta.json`),
|
|
923
|
+
JSON.stringify({
|
|
924
|
+
id: evidenceId,
|
|
925
|
+
metadata: {
|
|
926
|
+
kind: "uat_exec",
|
|
927
|
+
milestoneId: "M001",
|
|
928
|
+
sliceId: "S08",
|
|
929
|
+
checkId: "UAT-01",
|
|
930
|
+
intent: "uat-artifact-check",
|
|
931
|
+
},
|
|
932
|
+
}),
|
|
933
|
+
"utf-8",
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
937
|
+
milestoneId: "M001",
|
|
938
|
+
sliceId: "S08",
|
|
939
|
+
uatType: "artifact-driven",
|
|
940
|
+
verdict: "PASS",
|
|
941
|
+
checks: [{
|
|
942
|
+
id: "UAT-01",
|
|
943
|
+
description: "Config file exists",
|
|
944
|
+
mode: "artifact",
|
|
945
|
+
result: "PASS",
|
|
946
|
+
evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
|
|
947
|
+
notes: "Artifact present.",
|
|
948
|
+
}],
|
|
949
|
+
notes: "Fully automated pass.",
|
|
950
|
+
} as unknown as Parameters<typeof executeUatResultSave>[0], worktree));
|
|
951
|
+
|
|
952
|
+
assert.equal(result.isError, undefined);
|
|
953
|
+
assert.equal(result.details.manualValidationPath, undefined);
|
|
954
|
+
const returnedText = (result.content[0] as { text: string }).text;
|
|
955
|
+
assert.equal(returnedText.includes("Manual validation needed"), false);
|
|
956
|
+
|
|
957
|
+
const assessment = readFileSync(
|
|
958
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S08", "S08-ASSESSMENT.md"),
|
|
959
|
+
"utf-8",
|
|
960
|
+
);
|
|
961
|
+
assert.equal(assessment.includes("## Manual Validation"), false);
|
|
962
|
+
} finally {
|
|
963
|
+
closeDatabase();
|
|
964
|
+
cleanup(base);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
839
968
|
test("executeUatResultSave rejects saved UAT without fresh UAT-owned evidence", async () => {
|
|
840
969
|
const base = makeTmpBase();
|
|
841
970
|
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
@@ -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
|
+
});
|
|
@@ -45,7 +45,7 @@ export function compileUnitToolContract(unitType: string): ToolContractResult {
|
|
|
45
45
|
const forbiddenWorkflowTools = Object.entries(surfaceContract?.forbiddenGsdTools ?? {})
|
|
46
46
|
.map(([name, reason]) => ({ name, reason }));
|
|
47
47
|
const closeoutTools = requiredWorkflowTools.filter((tool) =>
|
|
48
|
-
/^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool),
|
|
48
|
+
/^gsd_(?:task|slice|milestone|complete|validate|save|summary|uat)/.test(tool),
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
if (requiresCloseoutTool(unitType) && closeoutTools.length === 0) {
|
|
@@ -123,12 +123,31 @@ export function buildRunUatCanonicalToolNames(options: { includeBrowserTools?: r
|
|
|
123
123
|
]);
|
|
124
124
|
}
|
|
125
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
|
+
|
|
126
145
|
export function runUatBrowserToolsForType(uatType: string | undefined): readonly string[] {
|
|
127
|
-
return uatType
|
|
146
|
+
return uatTypeIncludesBrowser(uatType) ? RUN_UAT_BROWSER_TOOL_NAMES : [];
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
export function runUatPresentationSurfaceForType(uatType: string | undefined): ToolPresentationSurface {
|
|
131
|
-
return uatType
|
|
150
|
+
return uatTypeIncludesBrowser(uatType) ? "hybrid" : "mcp";
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
export function buildRunUatPresentationForType(
|
|
@@ -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";
|
|
@@ -1337,7 +1338,12 @@ function escapeMarkdownTableCell(value: unknown): string {
|
|
|
1337
1338
|
.replace(/\r?\n/g, "<br>");
|
|
1338
1339
|
}
|
|
1339
1340
|
|
|
1340
|
-
function renderUatAssessment(
|
|
1341
|
+
function renderUatAssessment(
|
|
1342
|
+
params: UatResultSaveParams,
|
|
1343
|
+
attempt: number,
|
|
1344
|
+
gateVerdict: "pass" | "flag",
|
|
1345
|
+
basePath: string,
|
|
1346
|
+
): string {
|
|
1341
1347
|
const lines = [
|
|
1342
1348
|
"---",
|
|
1343
1349
|
`sliceId: ${params.sliceId}`,
|
|
@@ -1372,6 +1378,27 @@ function renderUatAssessment(params: UatResultSaveParams, attempt: number, gateV
|
|
|
1372
1378
|
"",
|
|
1373
1379
|
`Aggregate UAT gate saved as ${gateVerdict}.`,
|
|
1374
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
|
+
|
|
1375
1402
|
return `${lines.join("\n")}\n`;
|
|
1376
1403
|
}
|
|
1377
1404
|
|
|
@@ -1424,7 +1451,7 @@ export async function executeUatResultSave(
|
|
|
1424
1451
|
}
|
|
1425
1452
|
const gateVerdict = params.verdict === "PASS" ? "pass" : "flag";
|
|
1426
1453
|
const rationale = params.notes ?? `UAT ${params.verdict} for ${params.sliceId}.`;
|
|
1427
|
-
const assessment = renderUatAssessment(params, attempt, gateVerdict);
|
|
1454
|
+
const assessment = renderUatAssessment(params, attempt, gateVerdict, basePath);
|
|
1428
1455
|
const summary = await executeSummarySave(
|
|
1429
1456
|
{
|
|
1430
1457
|
milestone_id: params.milestoneId,
|
|
@@ -1468,8 +1495,20 @@ export async function executeUatResultSave(
|
|
|
1468
1495
|
evaluatedAt,
|
|
1469
1496
|
});
|
|
1470
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}`;
|
|
1471
1507
|
return {
|
|
1472
|
-
content: [{
|
|
1508
|
+
content: [{
|
|
1509
|
+
type: "text",
|
|
1510
|
+
text: manualGuidance ? `${savedText}\n\nManual validation needed:\n${manualGuidance}` : savedText,
|
|
1511
|
+
}],
|
|
1473
1512
|
details: {
|
|
1474
1513
|
operation: "save_uat_result",
|
|
1475
1514
|
milestoneId: params.milestoneId,
|
|
@@ -1479,6 +1518,9 @@ export async function executeUatResultSave(
|
|
|
1479
1518
|
attempt,
|
|
1480
1519
|
attemptPath,
|
|
1481
1520
|
recommendedNextUnit: params.verdict === "PASS" ? null : "reactive-execute",
|
|
1521
|
+
...(hasHuman
|
|
1522
|
+
? { manualValidationPath: resolveCanonicalMilestoneRoot(basePath, params.milestoneId) }
|
|
1523
|
+
: {}),
|
|
1482
1524
|
},
|
|
1483
1525
|
};
|
|
1484
1526
|
} catch (err) {
|