@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
@@ -418,6 +418,17 @@ function initSessionNotifications(ctx) {
418
418
  installNotifyInterceptor(ctx);
419
419
  initNotificationWidget(ctx);
420
420
  }
421
+ async function prepareWorkflowMcpForHookContext(ctx, basePath) {
422
+ // Skip MCP auto-prep when running inside an auto-worktree. The worktree
423
+ // already has .mcp.json from createAutoWorktree, and re-running the writer
424
+ // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
425
+ // CLI path resolution), dirtying the tree and breaking the milestone merge.
426
+ const { isInAutoWorktree } = await import("../auto-worktree.js");
427
+ if (isInAutoWorktree(basePath))
428
+ return;
429
+ const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
430
+ prepareWorkflowMcpForProject(ctx, basePath);
431
+ }
421
432
  export function registerHooks(pi, ecosystemHandlers) {
422
433
  // ADR-005 Phase 3b: surface pi-ai ProviderSwitchReport via audit, notification, and counter.
423
434
  // Idempotent — only the first registerHooks call installs.
@@ -438,12 +449,7 @@ export function registerHooks(pi, ecosystemHandlers) {
438
449
  await syncServiceTierStatus(ctx);
439
450
  await applyDisabledModelProviderPolicy(ctx);
440
451
  await applyCompactionThresholdOverride(ctx);
441
- // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
442
- const { isInAutoWorktree } = await import("../auto-worktree.js");
443
- if (!isInAutoWorktree(basePath)) {
444
- const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
445
- prepareWorkflowMcpForProject(ctx, basePath);
446
- }
452
+ await prepareWorkflowMcpForHookContext(ctx, basePath);
447
453
  // Apply show_token_cost preference (#1515)
448
454
  try {
449
455
  const { loadEffectiveGSDPreferences } = await import("../preferences.js");
@@ -468,15 +474,7 @@ export function registerHooks(pi, ecosystemHandlers) {
468
474
  await syncServiceTierStatus(ctx);
469
475
  await applyDisabledModelProviderPolicy(ctx);
470
476
  await applyCompactionThresholdOverride(ctx);
471
- // Skip MCP auto-prep when running inside an auto-worktree. The worktree
472
- // already has .mcp.json from createAutoWorktree, and re-running the writer
473
- // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
474
- // CLI path resolution), dirtying the tree and breaking the milestone merge.
475
- const { isInAutoWorktree } = await import("../auto-worktree.js");
476
- if (!isInAutoWorktree(basePath)) {
477
- const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
478
- prepareWorkflowMcpForProject(ctx, basePath);
479
- }
477
+ await prepareWorkflowMcpForHookContext(ctx, basePath);
480
478
  await loadToolApiKeysForSession();
481
479
  if (!isAutoActive()) {
482
480
  ctx.ui.setWidget("gsd-progress", undefined);
@@ -511,6 +509,10 @@ export function registerHooks(pi, ecosystemHandlers) {
511
509
  }
512
510
  }
513
511
  clearDeferredApprovalGate(beforeAgentBasePath);
512
+ // session_start can fire before the active provider has settled. By
513
+ // before_agent_start, Claude Code CLI sessions should get the same
514
+ // project MCP config that /gsd mcp init would write.
515
+ await prepareWorkflowMcpForHookContext(ctx, beforeAgentBasePath);
514
516
  // GSD's own context injection (existing behavior — unchanged).
515
517
  const { buildBeforeAgentStartResult } = await import("./system-context.js");
516
518
  const gsdResult = await buildBeforeAgentStartResult(event, ctx);
@@ -167,7 +167,13 @@ export function getCloseoutManualResolveBlocker(basePath) {
167
167
  if (conflictProbe.status === "dirty" && conflictProbe.unmerged.length > 0) {
168
168
  return `Unmerged paths remain in ${basePath}: ${conflictProbe.unmerged.slice(0, 5).join(", ")}`;
169
169
  }
170
- const status = runGit(basePath, ["status", "--porcelain"]);
170
+ let status;
171
+ try {
172
+ status = runGit(basePath, ["status", "--porcelain"]);
173
+ }
174
+ catch {
175
+ return `Could not inspect git status in ${basePath}.`;
176
+ }
171
177
  if (status) {
172
178
  return `Working tree still has uncommitted changes in ${basePath}. Commit, stash, or run /gsd closeout retry first.`;
173
179
  }
@@ -197,7 +197,15 @@ export async function handleAutoCommand(trimmed, ctx, pi) {
197
197
  if (trimmed === "") {
198
198
  if (!(await guardRemoteSession(ctx, pi)))
199
199
  return true;
200
- if (await hasUnresolvedCloseoutBlocker(ctx, projectRoot()))
200
+ const basePath = projectRoot();
201
+ const { hasGsdBootstrapArtifacts } = await import("../../detection.js");
202
+ const { gsdRoot } = await import("../../paths.js");
203
+ if (!hasGsdBootstrapArtifacts(gsdRoot(basePath))) {
204
+ const { showSmartEntry } = await import("../../guided-flow.js");
205
+ await showSmartEntry(ctx, pi, basePath, { step: true });
206
+ return true;
207
+ }
208
+ if (await hasUnresolvedCloseoutBlocker(ctx, basePath))
201
209
  return true;
202
210
  const { showGsdHome } = await import("../../gsd-command-home.js");
203
211
  await showGsdHome(ctx, pi, projectRoot());
@@ -17,6 +17,7 @@ import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
17
17
  import { getAutoWorktreePath } from "./auto-worktree.js";
18
18
  import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
19
19
  import { loadPrompt } from "./prompt-loader.js";
20
+ import { isPnpmInstall } from "../../shared/package-manager-detection.js";
20
21
  import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
21
22
  import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
22
23
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-pi/latest";
@@ -42,6 +43,8 @@ function isBunInstall(argv1 = process.argv[1]) {
42
43
  function resolveInstallCommand(pkg) {
43
44
  if (isBunInstall())
44
45
  return `bun add -g ${pkg}`;
46
+ if (isPnpmInstall())
47
+ return `pnpm add -g ${pkg}`;
45
48
  return `npm install -g ${pkg}`;
46
49
  }
47
50
  async function fetchLatestVersionForCommand() {
@@ -15,16 +15,15 @@ import { delimiter, join } from "node:path";
15
15
  import { AuthStorage } from "@gsd/pi-coding-agent";
16
16
  import { getEnvApiKey } from "@gsd/pi-ai";
17
17
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
- import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
18
+ import { getAuthPath, PROVIDER_REGISTRY, supportsBrowserOAuth } from "./key-manager.js";
19
19
  import { homedir } from "node:os";
20
20
  // ── Provider routing constants ────────────────────────────────────────────────
21
21
  /**
22
22
  * Providers that use external CLI authentication (not API keys).
23
- * These are always considered "found" the host CLI handles auth.
23
+ * When explicitly selected, the provider's own CLI/session owns auth.
24
24
  */
25
25
  const CLI_AUTH_PROVIDERS = new Set([
26
26
  "claude-code",
27
- "openai-codex",
28
27
  "google-gemini-cli",
29
28
  "google-antigravity",
30
29
  ]);
@@ -126,23 +125,26 @@ function collectConfiguredModelProviders() {
126
125
  * Used for lightweight binary-presence checks (PATH scan, no subprocess).
127
126
  */
128
127
  const CLI_BINARY_MAP = {
129
- "claude-code": "claude",
130
- "openai-codex": "codex",
131
- "google-gemini-cli": "gemini",
132
- "google-antigravity": "antigravity",
128
+ "claude-code": ["claude", "claude-code"],
129
+ "google-gemini-cli": ["gemini"],
130
+ "google-antigravity": ["agy"],
133
131
  };
132
+ const CLI_AUTH_PATH_CHECK_PROVIDERS = new Set([
133
+ "google-gemini-cli",
134
+ "google-antigravity",
135
+ ]);
134
136
  /**
135
137
  * Check if a CLI provider's binary exists anywhere in PATH.
136
138
  * Fast filesystem scan — no subprocess, no network, sub-1ms.
137
139
  */
138
140
  function isCliBinaryInPath(providerId) {
139
- const binary = CLI_BINARY_MAP[providerId];
140
- if (!binary)
141
+ const binaries = CLI_BINARY_MAP[providerId];
142
+ if (!binaries)
141
143
  return false;
142
144
  const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
143
145
  // On Windows, command shims are commonly installed as .cmd/.exe/.bat/.com.
144
146
  // Scan PATHEXT candidates in addition to the bare binary name.
145
- const executableNames = [binary];
147
+ const executableNames = [...binaries];
146
148
  if (process.platform === "win32") {
147
149
  const rawPathExt = process.env.PATHEXT
148
150
  ?.split(";")
@@ -150,10 +152,12 @@ function isCliBinaryInPath(providerId) {
150
152
  .filter(Boolean) ?? [];
151
153
  const normalizedPathExt = rawPathExt.map(ext => ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`);
152
154
  const defaultExt = [".exe", ".cmd", ".bat", ".com"];
153
- for (const ext of [...normalizedPathExt, ...defaultExt]) {
154
- const candidate = `${binary}${ext}`;
155
- if (!executableNames.includes(candidate))
156
- executableNames.push(candidate);
155
+ for (const binary of binaries) {
156
+ for (const ext of [...normalizedPathExt, ...defaultExt]) {
157
+ const candidate = `${binary}${ext}`;
158
+ if (!executableNames.includes(candidate))
159
+ executableNames.push(candidate);
160
+ }
157
161
  }
158
162
  }
159
163
  return pathDirs.some(dir => executableNames.some(name => existsSync(join(dir, name))));
@@ -185,11 +189,6 @@ function hasModelsJsonApiKey(providerId) {
185
189
  }
186
190
  function resolveKey(providerId) {
187
191
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
188
- // claude-code never stores credentials in auth.json — GSD delegates entirely to
189
- // the local CLI binary. Presence of the binary in PATH is the only signal.
190
- if (providerId === "claude-code") {
191
- return { found: isCliBinaryInPath("claude-code"), source: "env", backedOff: false };
192
- }
193
192
  if (providerId === "anthropic-vertex" && process.env.ANTHROPIC_VERTEX_PROJECT_ID) {
194
193
  return { found: true, source: "env", backedOff: false };
195
194
  }
@@ -201,7 +200,16 @@ function resolveKey(providerId) {
201
200
  const creds = auth.getCredentialsForProvider(providerId);
202
201
  if (creds.length > 0) {
203
202
  // Filter out empty placeholder keys (from skipped onboarding)
204
- const hasRealKey = creds.some(c => c.type === "oauth" || (c.type === "api_key" && c.key));
203
+ const hasRealKey = creds.some(c => {
204
+ if (c.type === "oauth")
205
+ return true;
206
+ if (c.type !== "api_key")
207
+ return false;
208
+ const key = c.key?.trim();
209
+ if (!key)
210
+ return false;
211
+ return !(CLI_AUTH_PROVIDERS.has(providerId) && key === "cli");
212
+ });
205
213
  if (hasRealKey) {
206
214
  return {
207
215
  found: true,
@@ -229,6 +237,11 @@ function resolveKey(providerId) {
229
237
  if (hasModelsJsonApiKey(providerId)) {
230
238
  return { found: true, source: "models.json", backedOff: false };
231
239
  }
240
+ // Cross-provider routes can use a local CLI when it is installed. Explicit
241
+ // external CLI provider selections are handled in checkLlmProviders() below.
242
+ if (CLI_AUTH_PROVIDERS.has(providerId) && isCliBinaryInPath(providerId)) {
243
+ return { found: true, source: "env", backedOff: false };
244
+ }
232
245
  return { found: false, source: "none", backedOff: false };
233
246
  }
234
247
  // ── Individual check groups ────────────────────────────────────────────────────
@@ -236,15 +249,32 @@ function checkLlmProviders() {
236
249
  const required = collectConfiguredModelProviders();
237
250
  const results = [];
238
251
  for (const providerId of required) {
239
- // CLI-authenticated providers don't need API keys skip key check
252
+ // CLI-authenticated providers don't need API keys. The provider's own
253
+ // request path validates CLI sessions when it is used.
240
254
  if (CLI_AUTH_PROVIDERS.has(providerId)) {
241
255
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
256
+ const label = info?.label ?? providerId;
257
+ if (CLI_AUTH_PATH_CHECK_PROVIDERS.has(providerId) && !isCliBinaryInPath(providerId)) {
258
+ const binaries = CLI_BINARY_MAP[providerId]?.map(binary => `\`${binary}\``).join(" or ");
259
+ results.push({
260
+ name: providerId,
261
+ label,
262
+ category: "llm",
263
+ status: "error",
264
+ message: `${label} — CLI not found`,
265
+ detail: binaries
266
+ ? `Install ${label} and ensure ${binaries} is on PATH`
267
+ : `Install ${label} and ensure its CLI is on PATH`,
268
+ required: true,
269
+ });
270
+ continue;
271
+ }
242
272
  results.push({
243
273
  name: providerId,
244
- label: info?.label ?? providerId,
274
+ label,
245
275
  category: "llm",
246
276
  status: "ok",
247
- message: `${info?.label ?? providerId} — CLI auth (no key needed)`,
277
+ message: `${label} — CLI auth (no key needed)`,
248
278
  required: true,
249
279
  });
250
280
  continue;
@@ -282,7 +312,7 @@ function checkLlmProviders() {
282
312
  message: `${label} — not configured`,
283
313
  detail: providerId === "anthropic-vertex"
284
314
  ? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
285
- : info?.hasOAuth
315
+ : info && supportsBrowserOAuth(info)
286
316
  ? `Run /gsd keys to authenticate`
287
317
  : `Set ${envVar} or run /gsd keys`,
288
318
  required: true,
@@ -2,7 +2,7 @@
2
2
  // File Purpose: Detect and reconcile unresolved Git conflict state before automation runs.
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
- import { join } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
6
6
  import { autoResolveSafeConflictPaths } from "./git-conflict-resolve.js";
7
7
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
8
8
  import { abortAndReset } from "./git-self-heal.js";
@@ -10,6 +10,23 @@ import { logWarning } from "./workflow-logger.js";
10
10
  function splitZeroDelimited(output) {
11
11
  return output.split("\0").filter(Boolean);
12
12
  }
13
+ function hasGitMarker(basePath) {
14
+ try {
15
+ let dir = resolve(basePath);
16
+ for (let i = 0; i < 30; i++) {
17
+ if (existsSync(join(dir, ".git")))
18
+ return true;
19
+ const parent = dirname(dir);
20
+ if (parent === dir)
21
+ break;
22
+ dir = parent;
23
+ }
24
+ }
25
+ catch {
26
+ // Fall through to the git probes, which will report unknown on failure.
27
+ }
28
+ return false;
29
+ }
13
30
  export function listUnmergedGitPaths(basePath) {
14
31
  try {
15
32
  const output = spawnSync("git", ["diff", "--name-only", "--diff-filter=U", "-z"], {
@@ -57,6 +74,14 @@ export function listMergeStateBlockers(basePath) {
57
74
  return MERGE_STATE_MARKERS.filter((marker) => existsSync(join(gitDir, marker)));
58
75
  }
59
76
  export function probeGitConflictState(basePath) {
77
+ if (!hasGitMarker(basePath)) {
78
+ return {
79
+ status: "clean",
80
+ unmerged: [],
81
+ checkFailures: [],
82
+ mergeStateBlockers: [],
83
+ };
84
+ }
60
85
  const unmerged = listUnmergedGitPaths(basePath);
61
86
  if (unmerged === null) {
62
87
  return {
@@ -12,18 +12,18 @@ import { getErrorMessage } from "./error-utils.js";
12
12
  import { gsdHome } from "./gsd-home.js";
13
13
  export const PROVIDER_REGISTRY = [
14
14
  // LLM Providers
15
- { id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
15
+ { id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], authMode: "apiKey", dashboardUrl: "console.anthropic.com" },
16
16
  // Claude Code CLI: routes through the local `claude` binary — no API key,
17
17
  // authentication is handled by the CLI's own OAuth flow.
18
18
  // Referenced by doctor-providers.ts, auto-model-selection.ts, and others;
19
19
  // must be in the canonical registry so all consumers see the same catalog.
20
20
  // See: https://github.com/open-gsd/gsd-pi/issues/4541
21
- { id: "claude-code", label: "Claude Code CLI", category: "llm", hasOAuth: true },
21
+ { id: "claude-code", label: "Claude Code CLI", category: "llm", authMode: "externalCli" },
22
22
  { id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
23
- { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", hasOAuth: true },
24
- { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)", category: "llm", hasOAuth: true },
25
- { id: "google-gemini-cli", label: "Google Gemini CLI", category: "llm", hasOAuth: true },
26
- { id: "google-antigravity", label: "Antigravity", category: "llm", hasOAuth: true },
23
+ { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", authMode: "browserOAuth" },
24
+ { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)", category: "llm", authMode: "browserOAuth" },
25
+ { id: "google-gemini-cli", label: "Google Gemini CLI", category: "llm", authMode: "externalCli" },
26
+ { id: "google-antigravity", label: "Antigravity", category: "llm", authMode: "externalCli" },
27
27
  { id: "google", label: "Google (Gemini)", category: "llm", envVar: "GEMINI_API_KEY", dashboardUrl: "aistudio.google.com/apikey" },
28
28
  { id: "groq", label: "Groq", category: "llm", envVar: "GROQ_API_KEY", dashboardUrl: "console.groq.com" },
29
29
  { id: "xai", label: "xAI (Grok)", category: "llm", envVar: "XAI_API_KEY", dashboardUrl: "console.x.ai" },
@@ -49,6 +49,20 @@ export const PROVIDER_REGISTRY = [
49
49
  { id: "telegram_bot", label: "Telegram Bot", category: "remote", envVar: "TELEGRAM_BOT_TOKEN" },
50
50
  ];
51
51
  // ─── Utilities ──────────────────────────────────────────────────────────────────
52
+ export function getProviderAuthMode(provider) {
53
+ return provider.authMode ?? "apiKey";
54
+ }
55
+ export function supportsBrowserOAuth(provider) {
56
+ return getProviderAuthMode(provider) === "browserOAuth";
57
+ }
58
+ export function supportsStoredApiKey(provider) {
59
+ const mode = getProviderAuthMode(provider);
60
+ if (mode === "externalCli" || mode === "cloudIdentity" || mode === "none")
61
+ return false;
62
+ if (mode === "browserOAuth")
63
+ return Boolean(provider.envVar || provider.prefixes?.length);
64
+ return true;
65
+ }
52
66
  /**
53
67
  * Mask an API key for display: show first 4 + last 4 chars.
54
68
  * Keys shorter than 12 chars show only first 2 + last 2.
@@ -79,11 +93,14 @@ export function formatDuration(ms) {
79
93
  /**
80
94
  * Describe a credential's type and status.
81
95
  */
82
- export function describeCredential(cred) {
96
+ export function describeCredential(cred, provider) {
83
97
  if (cred.type === "api_key") {
84
98
  const apiCred = cred;
85
99
  if (!apiCred.key)
86
100
  return "empty key";
101
+ if (apiCred.key === "cli" && provider && getProviderAuthMode(provider) === "externalCli") {
102
+ return "external CLI";
103
+ }
87
104
  return `API key (${maskKey(apiCred.key)})`;
88
105
  }
89
106
  if (cred.type === "oauth") {
@@ -129,7 +146,7 @@ export function getAllKeyStatuses(auth) {
129
146
  const firstCred = creds[0];
130
147
  const desc = creds.length > 1
131
148
  ? `${creds.length} keys (round-robin)`
132
- : describeCredential(firstCred);
149
+ : describeCredential(firstCred, provider);
133
150
  return {
134
151
  provider,
135
152
  configured: true,
@@ -236,9 +253,20 @@ export async function handleAddKey(providerArg, ctx, auth) {
236
253
  return false;
237
254
  provider = PROVIDER_REGISTRY[idx];
238
255
  }
239
- // If OAuth is available, offer choice
240
- if (provider.hasOAuth) {
241
- const methods = ["API key", "Browser login (OAuth)"];
256
+ const authMode = getProviderAuthMode(provider);
257
+ if (authMode === "externalCli") {
258
+ ctx.ui.notify(`${provider.label} is authenticated by its own local CLI. ` +
259
+ `Sign in with that CLI first, then run /login to activate it in GSD.`, "info");
260
+ return false;
261
+ }
262
+ if (authMode === "cloudIdentity") {
263
+ ctx.ui.notify(`${provider.label} uses cloud identity credentials. Configure the provider's cloud CLI or environment, then restart GSD.`, "info");
264
+ return false;
265
+ }
266
+ if (supportsBrowserOAuth(provider)) {
267
+ const methods = supportsStoredApiKey(provider)
268
+ ? ["API token/key", "Browser login (OAuth)"]
269
+ : ["Browser login (OAuth)"];
242
270
  const method = await ctx.ui.select(`${provider.label} — how do you want to authenticate?`, methods);
243
271
  if (!method || typeof method !== "string")
244
272
  return false;
@@ -248,6 +276,10 @@ export async function handleAddKey(providerArg, ctx, auth) {
248
276
  return false;
249
277
  }
250
278
  }
279
+ if (!supportsStoredApiKey(provider)) {
280
+ ctx.ui.notify(`${provider.label} does not accept a GSD-stored API key. Use /login.`, "info");
281
+ return false;
282
+ }
251
283
  // API key input
252
284
  const input = await ctx.ui.input(`API key for ${provider.label}:`, provider.envVar ? `or set ${provider.envVar} env var` : "paste your key here");
253
285
  if (input === null || input === undefined)
@@ -309,7 +341,7 @@ export async function handleRemoveKey(providerArg, ctx, auth) {
309
341
  }
310
342
  // Multi-key handling
311
343
  if (creds.length > 1) {
312
- const options = creds.map((c, i) => `[${i + 1}] ${describeCredential(c)}`);
344
+ const options = creds.map((c, i) => `[${i + 1}] ${describeCredential(c, provider)}`);
313
345
  options.push("Remove all");
314
346
  const choice = await ctx.ui.select(`${provider.label} has ${creds.length} keys. Remove which?`, options);
315
347
  if (!choice || typeof choice !== "string")
@@ -330,7 +362,7 @@ export async function handleRemoveKey(providerArg, ctx, auth) {
330
362
  }
331
363
  }
332
364
  else {
333
- const confirmed = await ctx.ui.confirm("Remove key?", `Remove ${describeCredential(creds[0])} for ${provider.label}?`);
365
+ const confirmed = await ctx.ui.confirm("Remove key?", `Remove ${describeCredential(creds[0], provider)} for ${provider.label}?`);
334
366
  if (!confirmed)
335
367
  return false;
336
368
  auth.remove(provider.id);
@@ -116,6 +116,15 @@ export async function handleCompleteTask(params, basePath) {
116
116
  if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
117
117
  return { error: "milestoneId is required and must be a non-empty string" };
118
118
  }
119
+ if (!params.oneLiner || typeof params.oneLiner !== "string" || params.oneLiner.trim() === "") {
120
+ return { error: "oneLiner is required and must be a non-empty string" };
121
+ }
122
+ if (!params.narrative || typeof params.narrative !== "string" || params.narrative.trim() === "") {
123
+ return { error: "narrative is required and must be a non-empty string" };
124
+ }
125
+ if (!params.verification || typeof params.verification !== "string" || params.verification.trim() === "") {
126
+ return { error: "verification is required and must be a non-empty string" };
127
+ }
119
128
  const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, params.milestoneId);
120
129
  // ── Ownership check (opt-in: only enforced when claim file exists) ──────
121
130
  const ownershipErr = checkOwnership(artifactBasePath, taskUnitKey(params.milestoneId, params.sliceId, params.taskId), params.actorName);
@@ -238,6 +238,24 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
238
238
  };
239
239
  }
240
240
  }
241
+ function normalizeVerificationEvidence(evidence) {
242
+ return (evidence ?? []).map((entry) => typeof entry === "string"
243
+ ? { command: entry, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 }
244
+ : entry);
245
+ }
246
+ function deriveVerificationSummary(evidence) {
247
+ if (evidence.length === 0)
248
+ return null;
249
+ const rendered = evidence.slice(0, 3).map((entry) => {
250
+ const command = entry.command.trim() || "(unspecified command)";
251
+ const verdict = entry.verdict.trim() || "recorded";
252
+ return `\`${command}\` exited ${entry.exitCode} (${verdict})`;
253
+ });
254
+ const suffix = evidence.length > rendered.length
255
+ ? `; ${evidence.length - rendered.length} more check(s) recorded`
256
+ : "";
257
+ return `Verification evidence recorded: ${rendered.join("; ")}${suffix}.`;
258
+ }
241
259
  export async function executeTaskComplete(params, basePath = process.cwd()) {
242
260
  const dbAvailable = await ensureDbOpen(basePath);
243
261
  if (!dbAvailable) {
@@ -249,7 +267,28 @@ export async function executeTaskComplete(params, basePath = process.cwd()) {
249
267
  }
250
268
  try {
251
269
  const coerced = { ...params };
252
- coerced.verificationEvidence = (params.verificationEvidence ?? []).map((v) => typeof v === "string" ? { command: v, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 } : v);
270
+ const verificationEvidence = normalizeVerificationEvidence(params.verificationEvidence);
271
+ coerced.verificationEvidence = verificationEvidence;
272
+ const verification = typeof params.verification === "string" ? params.verification.trim() : "";
273
+ if (verification.length === 0) {
274
+ const derived = deriveVerificationSummary(verificationEvidence);
275
+ if (derived) {
276
+ coerced.verification = derived;
277
+ }
278
+ else if (params.blockerDiscovered === true) {
279
+ coerced.verification = "Not run: blocker discovered before verification.";
280
+ }
281
+ else {
282
+ return {
283
+ content: [{
284
+ type: "text",
285
+ text: "Error completing task: verification is required unless verificationEvidence is provided or blockerDiscovered is true.",
286
+ }],
287
+ details: { operation: "complete_task", error: "verification_required" },
288
+ isError: true,
289
+ };
290
+ }
291
+ }
253
292
  const result = await handleCompleteTask(coerced, basePath);
254
293
  if ("error" in result) {
255
294
  return {
@@ -203,7 +203,7 @@ function validateMilestoneId(milestoneId) {
203
203
  * - emits worktree-created telemetry on successful entry
204
204
  * - notifies the caller via `ctx.notify` for every user-visible outcome
205
205
  */
206
- export function _enterMilestoneCore(s, deps, milestoneId, ctx) {
206
+ export function _enterMilestoneCore(s, deps, milestoneId, ctx, opts = {}) {
207
207
  if (!isValidMilestoneId(milestoneId)) {
208
208
  debugLog("WorktreeLifecycle", {
209
209
  action: "enterMilestone",
@@ -305,7 +305,7 @@ export function _enterMilestoneCore(s, deps, milestoneId, ctx) {
305
305
  // Handles the case where originalBasePath is falsy and basePath is itself
306
306
  // a worktree path — prevents double-nested worktree paths (#3729).
307
307
  const basePath = resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
308
- const mode = getIsolationMode(basePath);
308
+ const mode = opts.modeOverride ?? getIsolationMode(basePath);
309
309
  if (s.isolationDegraded) {
310
310
  if (mode === "worktree") {
311
311
  try {
@@ -841,7 +841,8 @@ export function mergeMilestoneStandalone(deps, mctx) {
841
841
  pushed: false,
842
842
  };
843
843
  }
844
- const mode = getIsolationMode(originalBasePath || worktreeBasePath);
844
+ const mode = mctx.isolationModeOverride ??
845
+ getIsolationMode(originalBasePath || worktreeBasePath);
845
846
  debugLog("WorktreeLifecycle", {
846
847
  action: "mergeAndExit",
847
848
  milestoneId,
@@ -1137,6 +1138,7 @@ export class WorktreeLifecycle {
1137
1138
  originalBasePath: this.s.originalBasePath,
1138
1139
  worktreeBasePath: this.s.basePath,
1139
1140
  milestoneId,
1141
+ isolationModeOverride: this.s.strandedRecoveryIsolationMode ?? undefined,
1140
1142
  isolationDegraded: this.s.isolationDegraded,
1141
1143
  notify: ctx.notify,
1142
1144
  });
@@ -1223,6 +1225,7 @@ export class WorktreeLifecycle {
1223
1225
  // Rebuild GitService after merge (branch HEAD changed)
1224
1226
  rebuildGitService(this.s, this.deps);
1225
1227
  }
1228
+ this.s.strandedRecoveryIsolationMode = null;
1226
1229
  return result;
1227
1230
  }
1228
1231
  // ── Removed: _mergeWorktreeMode / _mergeBranchMode bodies ────────────
@@ -1345,6 +1348,24 @@ export class WorktreeLifecycle {
1345
1348
  resumeFromPausedSession(base, persistedWorktreePath) {
1346
1349
  this.s.basePath = resolvePausedResumeBasePath(base, persistedWorktreePath);
1347
1350
  }
1351
+ /**
1352
+ * Adopt in-progress stranded work during bootstrap.
1353
+ *
1354
+ * Unlike completed-orphan recovery, this does not merge, delete, or commit.
1355
+ * It only moves the live session onto the branch/worktree proven by the
1356
+ * audit evidence, while preserving that mode for the eventual merge.
1357
+ */
1358
+ adoptStrandedMilestone(milestoneId, base, ctx, opts) {
1359
+ this.adoptSessionRoot(base);
1360
+ this.s.strandedRecoveryIsolationMode = opts.mode;
1361
+ const result = _enterMilestoneCore(this.s, this.deps, milestoneId, ctx, {
1362
+ modeOverride: opts.mode,
1363
+ });
1364
+ if (!result.ok) {
1365
+ this.s.strandedRecoveryIsolationMode = null;
1366
+ }
1367
+ return result;
1368
+ }
1348
1369
  /**
1349
1370
  * Adopt an orphan worktree for a bootstrap-time merge (ADR-016 phase 2 / B4,
1350
1371
  * issue #5622).