@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.
Files changed (149) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  4. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  5. package/dist/resources/extensions/browser-tools/index.js +29 -2
  6. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +45 -3
  8. package/dist/resources/extensions/gsd/auto/session.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +10 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  12. package/dist/resources/extensions/gsd/auto.js +26 -4
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +29 -21
  14. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  15. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  16. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  17. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  18. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  19. package/dist/resources/extensions/gsd/guided-flow.js +4 -1
  20. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  21. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  22. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
  24. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  25. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  26. package/dist/resources/extensions/gsd/tool-contract.js +1 -1
  27. package/dist/resources/extensions/gsd/tool-presentation-plan.js +19 -2
  28. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  29. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +32 -4
  30. package/dist/resources/extensions/gsd/unit-tool-contracts.js +38 -14
  31. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -3
  32. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  33. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  34. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  35. package/dist/web/standalone/.next/BUILD_ID +1 -1
  36. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  37. package/dist/web/standalone/.next/build-manifest.json +2 -2
  38. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  39. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.html +1 -1
  56. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  63. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/package.json +1 -1
  69. package/packages/cloud-mcp-gateway/package.json +2 -2
  70. package/packages/contracts/package.json +1 -1
  71. package/packages/daemon/package.json +4 -4
  72. package/packages/gsd-agent-core/package.json +5 -5
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/mcp-server/package.json +3 -3
  75. package/packages/native/package.json +1 -1
  76. package/packages/pi-agent-core/package.json +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +158 -2
  78. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/models.generated.js +149 -9
  80. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  82. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  83. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  84. package/packages/pi-ai/package.json +1 -1
  85. package/packages/pi-coding-agent/package.json +7 -7
  86. package/packages/pi-tui/package.json +1 -1
  87. package/packages/rpc-client/package.json +2 -2
  88. package/pkg/package.json +1 -1
  89. package/scripts/install/handoff.js +16 -3
  90. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  91. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  92. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  93. package/src/resources/extensions/browser-tools/index.ts +36 -5
  94. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  95. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  96. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  97. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  98. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  99. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  100. package/src/resources/extensions/gsd/auto-dispatch.ts +34 -2
  101. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  102. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  103. package/src/resources/extensions/gsd/auto.ts +28 -4
  104. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +40 -21
  105. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  106. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  107. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  108. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  109. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  110. package/src/resources/extensions/gsd/guided-flow.ts +4 -1
  111. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  112. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  113. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  114. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  115. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
  116. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  117. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  118. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  119. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  121. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  122. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  123. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  124. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  125. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  126. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +1 -1
  127. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  128. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  129. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  130. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  131. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  132. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +113 -1
  133. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  134. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
  135. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  136. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  137. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +131 -2
  138. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  139. package/src/resources/extensions/gsd/tool-contract.ts +1 -1
  140. package/src/resources/extensions/gsd/tool-presentation-plan.ts +21 -2
  141. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  142. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +46 -4
  143. package/src/resources/extensions/gsd/unit-tool-contracts.ts +38 -14
  144. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -3
  145. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  146. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  147. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  148. /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  149. /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 = ["gsd_slice_complete", "gsd_task_reopen", "gsd_replan_slice"];
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
- ["gsd_plan_slice"],
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
- ["gsd_complete_slice"],
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
- ["gsd_milestone_status", "gsd_validate_milestone"],
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
- ["gsd_milestone_status", "gsd_complete_milestone"],
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 accepts workflow MCP tools absent from parent active tool surface", () => {
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.equal(error, null);
853
+ assert.match(error ?? "", /requires gsd_summary_save/);
814
854
  });
815
855
 
816
- test("transport compatibility still checks non-MCP tools against parent active tool surface", () => {
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 secure_env_collect/);
832
- assert.doesNotMatch(error ?? "", /gsd_summary_save/);
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 managed gsd-browser tools",
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 managed browser evidence.",
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 === "browser-executable" ? RUN_UAT_BROWSER_TOOL_NAMES : [];
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 === "browser-executable" ? "hybrid" : "mcp";
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(params: UatResultSaveParams, attempt: number, gateVerdict: "pass" | "flag"): string {
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: [{ type: "text", text: `UAT result saved for ${params.milestoneId}/${params.sliceId}: ${params.verdict}` }],
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) {