@opengsd/gsd-pi 1.0.2-dev.235ebf3 → 1.0.2-dev.29398d2

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 (184) hide show
  1. package/README.md +63 -12
  2. package/dist/onboarding.js +22 -3
  3. package/dist/resource-loader.d.ts +7 -0
  4. package/dist/resource-loader.js +42 -9
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/context7/index.js +12 -2
  7. package/dist/resources/extensions/google-cli/index.js +30 -0
  8. package/dist/resources/extensions/google-cli/models.js +55 -0
  9. package/dist/resources/extensions/google-cli/package.json +11 -0
  10. package/dist/resources/extensions/google-cli/readiness.js +12 -0
  11. package/dist/resources/extensions/google-cli/stream-adapter.js +191 -0
  12. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  13. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  14. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  15. package/dist/resources/extensions/gsd/auto-start.js +232 -49
  16. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
  19. package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
  20. package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
  21. package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
  22. package/dist/resources/extensions/gsd/doctor-providers.js +54 -24
  23. package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
  24. package/dist/resources/extensions/gsd/key-manager.js +45 -13
  25. package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
  26. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
  27. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
  28. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  29. package/dist/resources/extensions/search-the-web/native-search.js +57 -8
  30. package/dist/resources/shared/package-manager-detection.js +36 -0
  31. package/dist/update-check.d.ts +6 -2
  32. package/dist/update-check.js +7 -3
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/index.html +1 -1
  58. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  65. package/dist/web/standalone/.next/server/chunks/1834.js +2 -2
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  71. package/dist/web/standalone/package.json +0 -1
  72. package/dist/worktree-cli.d.ts +0 -2
  73. package/dist/worktree-cli.js +21 -9
  74. package/package.json +5 -2
  75. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  76. package/packages/cloud-mcp-gateway/package.json +4 -3
  77. package/packages/contracts/package.json +1 -1
  78. package/packages/daemon/package.json +4 -4
  79. package/packages/gsd-agent-core/package.json +5 -5
  80. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +1 -1
  81. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  82. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +2 -2
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts +6 -1
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  86. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js +9 -6
  87. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  89. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  90. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  91. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  92. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  94. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  95. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  96. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  97. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  98. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  100. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts +3 -0
  102. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  103. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +144 -2
  104. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.d.ts.map +1 -1
  106. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js +2 -14
  107. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js.map +1 -1
  108. package/packages/gsd-agent-modes/package.json +7 -7
  109. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  110. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  112. package/packages/mcp-server/package.json +5 -4
  113. package/packages/native/package.json +1 -1
  114. package/packages/pi-agent-core/dist/agent-loop.js +13 -13
  115. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  116. package/packages/pi-agent-core/package.json +1 -1
  117. package/packages/pi-ai/bin/pi-ai.js +14 -0
  118. package/packages/pi-ai/dist/models.generated.d.ts +40 -17
  119. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  120. package/packages/pi-ai/dist/models.generated.js +49 -30
  121. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  122. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/providers/anthropic.js +50 -0
  124. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  125. package/packages/pi-ai/dist/types.d.ts +2 -0
  126. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  127. package/packages/pi-ai/dist/types.js.map +1 -1
  128. package/packages/pi-ai/package.json +3 -2
  129. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  130. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  132. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  133. package/packages/pi-coding-agent/package.json +8 -8
  134. package/packages/pi-tui/package.json +1 -1
  135. package/packages/rpc-client/package.json +2 -2
  136. package/pkg/package.json +1 -1
  137. package/scripts/install/deps.js +10 -0
  138. package/scripts/install/detect-existing.js +17 -3
  139. package/scripts/install/npm-global.js +103 -33
  140. package/scripts/install.js +1 -0
  141. package/src/resources/extensions/context7/index.ts +15 -2
  142. package/src/resources/extensions/google-cli/index.ts +34 -0
  143. package/src/resources/extensions/google-cli/models.ts +57 -0
  144. package/src/resources/extensions/google-cli/package.json +11 -0
  145. package/src/resources/extensions/google-cli/readiness.ts +15 -0
  146. package/src/resources/extensions/google-cli/stream-adapter.ts +245 -0
  147. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  148. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  149. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  150. package/src/resources/extensions/gsd/auto-start.ts +307 -56
  151. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  152. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
  153. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
  154. package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
  155. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
  156. package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
  157. package/src/resources/extensions/gsd/doctor-providers.ts +55 -27
  158. package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
  159. package/src/resources/extensions/gsd/key-manager.ts +57 -14
  160. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
  161. package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
  162. package/src/resources/extensions/gsd/tests/commands-context.test.ts +5 -3
  163. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
  164. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  165. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
  166. package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
  167. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
  168. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
  169. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
  170. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
  171. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
  172. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
  173. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
  174. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  175. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
  176. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
  177. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
  178. package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
  179. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  180. package/src/resources/extensions/search-the-web/native-search.ts +60 -8
  181. package/src/resources/shared/package-manager-detection.ts +39 -0
  182. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  183. /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → bukT6Ux1YchPm2XqjaexX}/_buildManifest.js +0 -0
  184. /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → bukT6Ux1YchPm2XqjaexX}/_ssgManifest.js +0 -0
@@ -702,6 +702,48 @@ test("runProviderChecks reports ok for claude-code without any API key", () => {
702
702
  rmSync(tmpHome, { recursive: true, force: true });
703
703
  });
704
704
 
705
+ test("runProviderChecks reports errors for required Google CLI providers missing from PATH", () => {
706
+ const scenarios = [
707
+ { provider: "google-gemini-cli", label: "Google Gemini CLI", model: "gemini-2.5-pro" },
708
+ { provider: "google-antigravity", label: "Antigravity", model: "default" },
709
+ ];
710
+
711
+ for (const { provider, label, model } of scenarios) {
712
+ const repo = realpathSync(mkdtempSync(join(tmpdir(), `gsd-providers-${provider}-repo-`)));
713
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
714
+ writeFileSync(
715
+ join(repo, ".gsd", "PREFERENCES.md"),
716
+ [
717
+ "---",
718
+ "models:",
719
+ " execution:",
720
+ ` model: ${model}`,
721
+ ` provider: ${provider}`,
722
+ "---",
723
+ "",
724
+ ].join("\n"),
725
+ );
726
+
727
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), `gsd-providers-${provider}-home-`)));
728
+
729
+ withEnv({
730
+ HOME: tmpHome,
731
+ PATH: tmpHome,
732
+ }, () => {
733
+ withCwd(repo, () => {
734
+ const results = runProviderChecks();
735
+ const cli = results.find(r => r.name === provider);
736
+ assert.ok(cli, `${provider} result should exist`);
737
+ assert.equal(cli!.status, "error", `${provider} should error when the CLI binary is missing`);
738
+ assert.ok(cli!.detail?.includes(label), "should explain which CLI must be installed");
739
+ });
740
+ });
741
+
742
+ rmSync(repo, { recursive: true, force: true });
743
+ rmSync(tmpHome, { recursive: true, force: true });
744
+ }
745
+ });
746
+
705
747
  test("runProviderChecks reports ok for Anthropic via claude-code binary in PATH", () => {
706
748
  // Simulate a user who has no Anthropic API key but has the claude CLI installed.
707
749
  // Their PREFERENCES use a claude model without an explicit provider, so the doctor
@@ -736,6 +778,69 @@ test("runProviderChecks reports ok for Anthropic via claude-code binary in PATH"
736
778
  });
737
779
  });
738
780
 
781
+ test("runProviderChecks ignores external CLI auth sentinels when the CLI is missing", () => {
782
+ const scenarios = [
783
+ {
784
+ requiredProvider: "anthropic",
785
+ routeProvider: "claude-code",
786
+ model: "claude-sonnet-4-6",
787
+ env: {
788
+ ANTHROPIC_API_KEY: undefined,
789
+ ANTHROPIC_OAUTH_TOKEN: undefined,
790
+ COPILOT_GITHUB_TOKEN: undefined,
791
+ GH_TOKEN: undefined,
792
+ GITHUB_TOKEN: undefined,
793
+ },
794
+ },
795
+ {
796
+ requiredProvider: "google",
797
+ routeProvider: "google-gemini-cli",
798
+ model: "gemini-2.5-pro",
799
+ env: {
800
+ GEMINI_API_KEY: undefined,
801
+ GOOGLE_API_KEY: undefined,
802
+ },
803
+ },
804
+ ];
805
+
806
+ for (const { requiredProvider, routeProvider, model, env } of scenarios) {
807
+ const repo = realpathSync(mkdtempSync(join(tmpdir(), `gsd-providers-${routeProvider}-sentinel-repo-`)));
808
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), `gsd-providers-${routeProvider}-sentinel-home-`)));
809
+ const agentDir = join(tmpHome, ".gsd", "agent");
810
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
811
+ mkdirSync(agentDir, { recursive: true });
812
+ writeFileSync(
813
+ join(repo, ".gsd", "PREFERENCES.md"),
814
+ [
815
+ "---",
816
+ "models:",
817
+ ` execution: ${model}`,
818
+ "---",
819
+ "",
820
+ ].join("\n"),
821
+ );
822
+ writeFileSync(join(agentDir, "auth.json"), JSON.stringify({
823
+ [routeProvider]: { type: "api_key", key: "cli" },
824
+ }));
825
+
826
+ withEnv({
827
+ ...env,
828
+ HOME: tmpHome,
829
+ PATH: tmpHome,
830
+ }, () => {
831
+ withCwd(repo, () => {
832
+ const results = runProviderChecks();
833
+ const provider = results.find(r => r.name === requiredProvider);
834
+ assert.ok(provider, `${requiredProvider} result should exist`);
835
+ assert.equal(provider!.status, "error", `${routeProvider} sentinel should not satisfy ${requiredProvider}`);
836
+ });
837
+ });
838
+
839
+ rmSync(repo, { recursive: true, force: true });
840
+ rmSync(tmpHome, { recursive: true, force: true });
841
+ }
842
+ });
843
+
739
844
  test("runProviderChecks detects claude.cmd in PATH on Windows (#4503)", { skip: process.platform !== "win32" }, () => {
740
845
  const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-win-route-home-")));
741
846
  const binDir = join(tmpHome, "bin");
@@ -10,9 +10,10 @@ import {
10
10
  formatKeyDashboard,
11
11
  formatTestResults,
12
12
  runKeyDoctor,
13
- formatDoctorFindings,
14
- PROVIDER_REGISTRY,
15
- } from "../key-manager.ts";
13
+ formatDoctorFindings,
14
+ PROVIDER_REGISTRY,
15
+ getProviderAuthMode,
16
+ } from "../key-manager.ts";
16
17
 
17
18
  function makeAuth(data: Record<string, any> = {}): AuthStorage {
18
19
  return AuthStorage.inMemory(data);
@@ -76,6 +77,12 @@ test("describeCredential describes an empty API key", () => {
76
77
  assert.equal(describeCredential({ type: "api_key", key: "" }), "empty key");
77
78
  });
78
79
 
80
+ test("describeCredential describes external CLI sentinels without calling them API keys", () => {
81
+ const provider = PROVIDER_REGISTRY.find((p) => p.id === "claude-code");
82
+ assert.ok(provider);
83
+ assert.equal(describeCredential({ type: "api_key", key: "cli" }, provider), "external CLI");
84
+ });
85
+
79
86
  test("describeCredential describes an OAuth token with expiry", () => {
80
87
  const result = describeCredential({
81
88
  type: "oauth",
@@ -149,7 +156,19 @@ test("PROVIDER_REGISTRY includes claude-code as a first-class LLM provider (#454
149
156
  const entry = PROVIDER_REGISTRY.find((p) => p.id === "claude-code");
150
157
  assert.ok(entry, "claude-code must be in PROVIDER_REGISTRY");
151
158
  assert.equal(entry!.category, "llm");
152
- assert.ok(entry!.hasOAuth, "claude-code uses OAuth (CLI auth)");
159
+ assert.equal(getProviderAuthMode(entry!), "externalCli");
160
+ });
161
+
162
+ test("PROVIDER_REGISTRY classifies only Copilot and Codex as browser OAuth LLM providers", () => {
163
+ const modes = Object.fromEntries(
164
+ PROVIDER_REGISTRY.filter((p) => p.category === "llm").map((p) => [p.id, getProviderAuthMode(p)]),
165
+ );
166
+ assert.equal(modes.anthropic, "apiKey");
167
+ assert.equal(modes["github-copilot"], "browserOAuth");
168
+ assert.equal(modes["openai-codex"], "browserOAuth");
169
+ assert.equal(modes["claude-code"], "externalCli");
170
+ assert.equal(modes["google-gemini-cli"], "externalCli");
171
+ assert.equal(modes["google-antigravity"], "externalCli");
153
172
  });
154
173
 
155
174
  test("PROVIDER_REGISTRY includes all tool/search providers", () => {
@@ -48,20 +48,27 @@ describe("auditOrphanedMilestoneBranches", () => {
48
48
  const result = auditOrphanedMilestoneBranches(dir, "worktree");
49
49
  assert.deepStrictEqual(result.recovered, []);
50
50
  assert.deepStrictEqual(result.warnings, []);
51
+ assert.deepStrictEqual(result.actions, []);
52
+ assert.equal(result.blockingStrandedWork, null);
51
53
  });
52
54
 
53
- test("skips in none isolation mode", () => {
54
- // Create a milestone branch that would otherwise be detected
55
+ test("runs in none isolation mode and cleans safe completed residue", () => {
55
56
  run("git branch milestone/M001", dir);
56
57
  insertMilestone({ id: "M001", title: "Test", status: "complete" });
57
58
 
58
59
  const result = auditOrphanedMilestoneBranches(dir, "none");
59
- assert.deepStrictEqual(result.recovered, []);
60
+ assert.ok(
61
+ result.recovered.some((r) => r.includes("Deleted merged branch milestone/M001")),
62
+ `should clean merged completed residue even in none mode; got: ${JSON.stringify(result.recovered)}`,
63
+ );
60
64
  assert.deepStrictEqual(result.warnings, []);
65
+ assert.ok(
66
+ result.actions.some((action) => action.kind === "complete-merged-branch"),
67
+ "should record structured cleanup action",
68
+ );
61
69
 
62
- // Branch should still exist
63
70
  const branches = run("git branch --list milestone/M001", dir);
64
- assert.ok(branches.includes("milestone/M001"), "branch should be preserved in none mode");
71
+ assert.equal(branches, "", "safe completed branch should be cleaned in none mode");
65
72
  });
66
73
 
67
74
  test("deletes merged branch for completed milestone", () => {
@@ -149,9 +156,12 @@ describe("auditOrphanedMilestoneBranches", () => {
149
156
  // Must surface a warning so the user knows the worktree holds uncollapsed work
150
157
  assert.ok(result.warnings.length > 0, "should warn about in-progress orphan");
151
158
  assert.ok(
152
- result.warnings.some(w => w.includes("milestone/M001") && w.includes("in-progress")),
153
- `warning should mention milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
159
+ result.warnings.some(w => w.includes("Stranded work") && w.includes("milestone/M001") && w.includes("in-progress")),
160
+ `warning should mention stranded milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
154
161
  );
162
+ assert.equal(result.blockingStrandedWork?.milestoneId, "M001");
163
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "branch");
164
+ assert.equal(result.blockingStrandedWork?.commitsAhead, 1);
155
165
 
156
166
  // Branch must still exist
157
167
  const branches = run("git branch --list milestone/M001", dir);
@@ -184,6 +194,53 @@ describe("auditOrphanedMilestoneBranches", () => {
184
194
  result.warnings.some(w => w.includes(".gsd/worktrees/M001") || w.includes("worktree")),
185
195
  `warning should reference the worktree location; got: ${JSON.stringify(result.warnings)}`,
186
196
  );
197
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
198
+ });
199
+
200
+ test("detects dirty in-progress worktree even when branch has no commits ahead", () => {
201
+ run("git branch milestone/M001", dir);
202
+
203
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
204
+ mkdirSync(wtDir, { recursive: true });
205
+ writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
206
+ writeFileSync(join(wtDir, "dirty.txt"), "uncommitted work\n");
207
+
208
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
209
+
210
+ const result = auditOrphanedMilestoneBranches(dir, "worktree", {
211
+ hasChanges: (basePath) => basePath === wtDir,
212
+ });
213
+
214
+ assert.deepStrictEqual(result.recovered, []);
215
+ assert.ok(
216
+ result.warnings.some((w) => w.includes("uncommitted changes")),
217
+ `dirty worktree should be treated as stranded work; got: ${JSON.stringify(result.warnings)}`,
218
+ );
219
+ assert.equal(result.blockingStrandedWork?.milestoneId, "M001");
220
+ assert.equal(result.blockingStrandedWork?.dirtyWorktree, true);
221
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
222
+ });
223
+
224
+ test("detects dirty in-progress worktree even when milestone branch is absent", () => {
225
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
226
+ mkdirSync(wtDir, { recursive: true });
227
+ writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
228
+ writeFileSync(join(wtDir, "dirty.txt"), "branchless work\n");
229
+
230
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
231
+
232
+ const result = auditOrphanedMilestoneBranches(dir, "none", {
233
+ hasChanges: (basePath) => basePath === wtDir,
234
+ });
235
+
236
+ assert.deepStrictEqual(result.recovered, []);
237
+ assert.ok(
238
+ result.warnings.some((w) => w.includes("Stranded work") && w.includes("M001")),
239
+ `branchless dirty worktree should block; got: ${JSON.stringify(result.warnings)}`,
240
+ );
241
+ assert.equal(result.blockingStrandedWork?.branch, undefined);
242
+ assert.equal(result.blockingStrandedWork?.dirtyWorktree, true);
243
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
187
244
  });
188
245
 
189
246
  test("cleans up orphaned worktree directory for merged milestone", () => {
@@ -359,7 +416,7 @@ describe("auditOrphanedMilestoneBranches", () => {
359
416
  assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
360
417
  });
361
418
 
362
- test("#5879 — skips branch-less orphan in 'none' isolation mode", () => {
419
+ test("#5879 — cleans branch-less complete orphan in 'none' isolation mode", () => {
363
420
  insertMilestone({ id: "M001", title: "Test", status: "complete" });
364
421
 
365
422
  const wtDir = join(dir, ".gsd", "worktrees", "M001");
@@ -368,7 +425,10 @@ describe("auditOrphanedMilestoneBranches", () => {
368
425
 
369
426
  const result = auditOrphanedMilestoneBranches(dir, "none");
370
427
 
371
- assert.deepStrictEqual(result.recovered, []);
372
- assert.ok(existsSync(wtDir), "'none' mode must not touch worktree dirs");
428
+ assert.ok(
429
+ result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
430
+ `none mode should still clean safe completed residue; got: ${JSON.stringify(result.recovered)}`,
431
+ );
432
+ assert.ok(!existsSync(wtDir), "completed orphan worktree dir should be cleaned in none mode");
373
433
  });
374
434
  });
@@ -15,6 +15,13 @@ function readGsdFile(relativePath: string): string {
15
15
  return readFileSync(resolve(gsdDir, relativePath), "utf-8");
16
16
  }
17
17
 
18
+ function firstIndexOfAny(source: string, needles: string[]): number {
19
+ const indexes = needles
20
+ .map((needle) => source.indexOf(needle))
21
+ .filter((index) => index > -1);
22
+ return indexes.length > 0 ? Math.min(...indexes) : -1;
23
+ }
24
+
18
25
  test("command entrypoints use startAutoDetached instead of awaiting startAuto (#3733)", () => {
19
26
  const autoHandlerSrc = readGsdFile("commands/handlers/auto.ts");
20
27
  const workflowHandlerSrc = readGsdFile("commands/handlers/workflow.ts");
@@ -145,7 +152,11 @@ test("fresh start registers the auto worker before bootstrap enters worktree flo
145
152
  const resumeEnterMilestoneIdx = resumeBody.indexOf("buildLifecycle().enterMilestone");
146
153
  const dbOpenIdx = bootstrapBody.indexOf("await openProjectDbIfPresent(base);");
147
154
  const bootstrapRegisterIdx = bootstrapBody.indexOf("registerAutoWorkerForSession(base);");
148
- const enterMilestoneIdx = bootstrapBody.indexOf("buildLifecycle().enterMilestone");
155
+ const enterMilestoneIdx = firstIndexOfAny(bootstrapBody, [
156
+ "buildLifecycle().enterMilestone",
157
+ "lifecycle.enterMilestone",
158
+ "lifecycle.adoptStrandedMilestone",
159
+ ]);
149
160
 
150
161
  assert.ok(startAutoIdx > -1, "startAuto should exist");
151
162
  assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
@@ -158,7 +169,7 @@ test("fresh start registers the auto worker before bootstrap enters worktree flo
158
169
  assert.ok(bootstrapIdx > -1, "bootstrapAutoSession should exist");
159
170
  assert.ok(dbOpenIdx > -1, "bootstrap should open the project DB");
160
171
  assert.ok(bootstrapRegisterIdx > -1, "bootstrap should register worker after DB open");
161
- assert.ok(enterMilestoneIdx > -1, "bootstrap should enter milestones through lifecycle");
172
+ assert.ok(enterMilestoneIdx > -1, "bootstrap should enter or adopt milestones through lifecycle");
162
173
  assert.ok(
163
174
  preBootstrapRegisterIdx < bootstrapCallIdx,
164
175
  "worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
@@ -235,12 +235,16 @@ test("gsd_task_complete — enrichment arrays are optional", () => {
235
235
  "milestoneId",
236
236
  "oneLiner",
237
237
  "narrative",
238
- "verification",
239
238
  ];
240
239
  for (const field of coreRequired) {
241
240
  assert.ok(required.has(field), `core field "${field}" must be required`);
242
241
  }
243
242
 
243
+ assert.ok(
244
+ !required.has("verification"),
245
+ "verification must be optional at the schema layer so step-mode can recover when verificationEvidence is present",
246
+ );
247
+
244
248
  // Enrichment fields must be optional
245
249
  const enrichmentFields = [
246
250
  "keyFiles",
@@ -272,6 +276,25 @@ test("gsd_task_complete — validates with only core params", () => {
272
276
  assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.join(", ")}`);
273
277
  });
274
278
 
279
+ test("gsd_task_complete — accepts evidence-only verification at schema layer", () => {
280
+ const tool = getTool("gsd_task_complete");
281
+ assert.ok(tool, "gsd_task_complete must be registered");
282
+
283
+ const params = {
284
+ taskId: "T01",
285
+ sliceId: "S01",
286
+ milestoneId: "M001",
287
+ oneLiner: "Implemented the feature",
288
+ narrative: "Created the module and wired it up.",
289
+ verificationEvidence: [
290
+ { command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
291
+ ],
292
+ };
293
+
294
+ const errors = validateSchema(tool, params);
295
+ assert.strictEqual(errors.length, 0, `Evidence-only params should validate but got errors: ${errors.join(", ")}`);
296
+ });
297
+
275
298
  // ─── gsd_complete_milestone: enrichment arrays must be optional ──────────────
276
299
 
277
300
  test("gsd_complete_milestone — enrichment arrays are optional", () => {
@@ -1,6 +1,11 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
3
6
 
7
+ import { registerHooks } from "../bootstrap/register-hooks.ts";
8
+ import { GSD_WORKFLOW_MCP_SERVER_NAME } from "../mcp-project-config.ts";
4
9
  import { prepareWorkflowMcpForProject, shouldAutoPrepareWorkflowMcp } from "../workflow-mcp-auto-prep.ts";
5
10
 
6
11
  test("shouldAutoPrepareWorkflowMcp enables prep for externalCli local transport", () => {
@@ -74,3 +79,58 @@ test("prepareWorkflowMcpForProject warns with /gsd mcp init guidance when prep f
74
79
  assert.equal(notifications[0].level, "warning");
75
80
  assert.match(notifications[0].message, /Please run \/gsd mcp init \./);
76
81
  });
82
+
83
+ test("before_agent_start auto-prepares project workflow MCP for Claude Code CLI", async (t) => {
84
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-before-agent-"));
85
+ const originalCwd = process.cwd();
86
+ const notifications: string[] = [];
87
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
88
+ const pi = {
89
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
90
+ const existing = handlers.get(event) ?? [];
91
+ existing.push(handler);
92
+ handlers.set(event, existing);
93
+ },
94
+ getActiveTools: () => [],
95
+ getAllTools: () => [],
96
+ setActiveTools() {},
97
+ };
98
+
99
+ t.after(() => {
100
+ process.chdir(originalCwd);
101
+ rmSync(projectRoot, { recursive: true, force: true });
102
+ });
103
+
104
+ process.chdir(projectRoot);
105
+ registerHooks(pi as any, []);
106
+
107
+ const beforeAgentStart = handlers.get("before_agent_start")?.[0];
108
+ assert.ok(beforeAgentStart, "before_agent_start hook should be registered");
109
+
110
+ await beforeAgentStart(
111
+ { prompt: "hello", systemPrompt: "base" },
112
+ {
113
+ cwd: projectRoot,
114
+ model: { provider: "claude-code", baseUrl: "local://claude-code" },
115
+ modelRegistry: {
116
+ getProviderAuthMode: () => "externalCli",
117
+ isProviderRequestReady: () => true,
118
+ },
119
+ ui: {
120
+ notify(message: string) {
121
+ notifications.push(message);
122
+ },
123
+ setWidget() {},
124
+ },
125
+ },
126
+ );
127
+
128
+ const configPath = join(projectRoot, ".mcp.json");
129
+ assert.equal(existsSync(configPath), true, "Claude Code CLI turns should create project MCP config");
130
+
131
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
132
+ mcpServers?: Record<string, unknown>;
133
+ };
134
+ assert.ok(parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME]);
135
+ assert.match(notifications.join("\n"), /Claude Code MCP prepared/);
136
+ });
@@ -148,6 +148,60 @@ test("executeTaskComplete coerces string verificationEvidence entries", async ()
148
148
  }
149
149
  });
150
150
 
151
+ test("executeTaskComplete derives missing verification from evidence", async () => {
152
+ const base = makeTmpBase();
153
+ try {
154
+ openTestDb(base);
155
+ const planDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
156
+ mkdirSync(planDir, { recursive: true });
157
+ writeFileSync(join(planDir, "S01-PLAN.md"), "# S01\n\n- [ ] **T01: Demo** `est:5m`\n");
158
+
159
+ const result = await inProjectDir(base, () => executeTaskComplete({
160
+ milestoneId: "M001",
161
+ sliceId: "S01",
162
+ taskId: "T01",
163
+ oneLiner: "Completed task",
164
+ narrative: "Did the work",
165
+ verificationEvidence: [
166
+ { command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
167
+ ],
168
+ }, base));
169
+
170
+ assert.equal(result.details.operation, "complete_task");
171
+ const db = _getAdapter();
172
+ assert.ok(db, "DB should be open");
173
+ const row = db!.prepare(
174
+ "SELECT verification_result FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?",
175
+ ).get("M001", "S01", "T01") as Record<string, unknown> | undefined;
176
+
177
+ assert.match(String(row?.verification_result), /Verification evidence recorded/);
178
+ assert.match(String(row?.verification_result), /`npm test` exited 0 \(pass\)/);
179
+ } finally {
180
+ closeDatabase();
181
+ cleanup(base);
182
+ }
183
+ });
184
+
185
+ test("executeTaskComplete returns a tool error when verification cannot be derived", async () => {
186
+ const base = makeTmpBase();
187
+ try {
188
+ openTestDb(base);
189
+ const result = await inProjectDir(base, () => executeTaskComplete({
190
+ milestoneId: "M001",
191
+ sliceId: "S01",
192
+ taskId: "T01",
193
+ oneLiner: "Completed task",
194
+ narrative: "Did the work",
195
+ }, base));
196
+
197
+ assert.equal(result.isError, true);
198
+ assert.match(String(result.content[0]?.text), /verification is required/);
199
+ } finally {
200
+ closeDatabase();
201
+ cleanup(base);
202
+ }
203
+ });
204
+
151
205
  test("executeSliceComplete preserves omitted optional requirement arrays", async () => {
152
206
  const base = makeTmpBase();
153
207
  try {
@@ -9,7 +9,7 @@ import { join } from "node:path";
9
9
  import { probeGitConflictState } from "../git-conflict-state.js";
10
10
  import { ensureWorkspaceGitReadyForPath } from "../workspace-git-preflight.js";
11
11
  import { isWorkspaceGitAllowedCommand } from "../workspace-git-guard.js";
12
- import { cleanup, git, makeTempRepo } from "./test-utils.ts";
12
+ import { cleanup, git, makeTempDir, makeTempRepo } from "./test-utils.ts";
13
13
 
14
14
  function seedGsdConflict(base: string): void {
15
15
  mkdirSync(join(base, ".gsd"), { recursive: true });
@@ -60,6 +60,21 @@ test("probeGitConflictState reports clean repo", () => {
60
60
  }
61
61
  });
62
62
 
63
+ test("ensureWorkspaceGitReadyForPath allows fresh non-git project setup folders", async () => {
64
+ const base = makeTempDir("gsd-ws-git-non-repo-");
65
+ try {
66
+ mkdirSync(join(base, ".gsd"), { recursive: true });
67
+
68
+ const probe = probeGitConflictState(base);
69
+ assert.equal(probe.status, "clean");
70
+
71
+ const ready = await ensureWorkspaceGitReadyForPath(base);
72
+ assert.equal(ready.ok, true);
73
+ } finally {
74
+ cleanup(base);
75
+ }
76
+ });
77
+
63
78
  test("ensureWorkspaceGitReadyForPath auto-resolves .gsd/ conflicts", async () => {
64
79
  const base = makeTempRepo("gsd-ws-git-heal-");
65
80
  try {
@@ -212,6 +212,34 @@ test("enterMilestone returns ok:true mode:none when isolation disabled", () => {
212
212
  assert.equal(s.basePath, "/project");
213
213
  });
214
214
 
215
+ test("adoptStrandedMilestone forces branch recovery even when normal preferences differ", (t) => {
216
+ const previousCwd = process.cwd();
217
+ const base = makeGitRepoBase({ isolation: "worktree" });
218
+ t.after(() => cleanupRepoBase(base, previousCwd));
219
+
220
+ const s = makeSession({ basePath: base, originalBasePath: base });
221
+ const deps = makeDeps();
222
+ const ctx = makeCtx();
223
+ const lifecycle = new WorktreeLifecycle(s, deps);
224
+
225
+ const result = lifecycle.adoptStrandedMilestone("M001", base, ctx, {
226
+ mode: "branch",
227
+ });
228
+
229
+ assert.equal(result.ok, true, `expected ok:true, got: ${JSON.stringify(result)}`);
230
+ if (result.ok) {
231
+ assert.equal(result.mode, "branch");
232
+ assert.equal(result.path, base);
233
+ }
234
+ assert.equal(s.basePath, base);
235
+ assert.equal(s.strandedRecoveryIsolationMode, "branch");
236
+ const currentBranch = execFileSync("git", ["branch", "--show-current"], {
237
+ cwd: base,
238
+ encoding: "utf-8",
239
+ }).trim();
240
+ assert.equal(currentBranch, "milestone/M001");
241
+ });
242
+
215
243
  test("enterMilestone returns ok:false reason:isolation-degraded when session degraded", () => {
216
244
  const s = makeSession({ isolationDegraded: true });
217
245
  const deps = makeDeps({ getIsolationMode: () => "branch" });