gsd-pi 2.70.1-dev.ec24142 → 2.71.0-dev.06b86c6

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 (161) hide show
  1. package/README.md +24 -17
  2. package/dist/cli.js +12 -3
  3. package/dist/mcp-server.js +6 -6
  4. package/dist/provider-migrations.d.ts +10 -0
  5. package/dist/provider-migrations.js +12 -0
  6. package/dist/resource-loader.js +136 -13
  7. package/dist/resources/GSD-WORKFLOW.md +1 -1
  8. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  9. package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
  10. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
  12. package/dist/resources/extensions/gsd/commands/context.js +15 -6
  13. package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
  14. package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
  15. package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
  16. package/dist/resources/extensions/gsd/error-classifier.js +1 -1
  17. package/dist/resources/extensions/gsd/file-lock.js +60 -0
  18. package/dist/resources/extensions/gsd/notification-store.js +21 -1
  19. package/dist/resources/extensions/gsd/notification-widget.js +1 -1
  20. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
  21. package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  22. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
  23. package/dist/resources/extensions/gsd/prompts/execute-task.md +20 -19
  24. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  25. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
  26. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
  28. package/dist/resources/extensions/gsd/prompts/system.md +1 -0
  29. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
  30. package/dist/resources/extensions/gsd/state.js +234 -332
  31. package/dist/resources/extensions/gsd/workflow-events.js +25 -13
  32. package/dist/resources/skills/create-skill/SKILL.md +2 -0
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  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/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  61. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/package.json +1 -1
  66. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  67. package/packages/mcp-server/dist/workflow-tools.js +21 -11
  68. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  69. package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
  70. package/packages/mcp-server/src/workflow-tools.ts +31 -11
  71. package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
  72. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  73. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
  74. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  75. package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
  76. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
  78. package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
  79. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
  81. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  82. package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
  83. package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
  84. package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
  85. package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
  86. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +202 -1
  87. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +90 -2
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +57 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  102. package/packages/pi-coding-agent/package.json +1 -1
  103. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +249 -1
  104. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
  105. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +96 -2
  106. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  107. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +65 -1
  108. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
  109. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
  110. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
  111. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
  112. package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
  113. package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
  114. package/packages/pi-tui/dist/components/markdown.js +17 -1
  115. package/packages/pi-tui/dist/components/markdown.js.map +1 -1
  116. package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
  117. package/packages/pi-tui/src/components/markdown.ts +22 -1
  118. package/pkg/package.json +1 -1
  119. package/src/resources/GSD-WORKFLOW.md +1 -1
  120. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  121. package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
  122. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -0
  123. package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
  124. package/src/resources/extensions/gsd/commands/context.ts +16 -5
  125. package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
  126. package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
  127. package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
  128. package/src/resources/extensions/gsd/error-classifier.ts +1 -1
  129. package/src/resources/extensions/gsd/file-lock.ts +59 -0
  130. package/src/resources/extensions/gsd/notification-store.ts +19 -1
  131. package/src/resources/extensions/gsd/notification-widget.ts +1 -1
  132. package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
  133. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  134. package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
  135. package/src/resources/extensions/gsd/prompts/execute-task.md +20 -19
  136. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  137. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
  138. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  139. package/src/resources/extensions/gsd/prompts/queue.md +3 -2
  140. package/src/resources/extensions/gsd/prompts/system.md +1 -0
  141. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
  142. package/src/resources/extensions/gsd/state.ts +274 -344
  143. package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
  144. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
  145. package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
  146. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
  147. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
  148. package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
  149. package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
  150. package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
  151. package/src/resources/extensions/gsd/tests/notification-store.test.ts +17 -0
  152. package/src/resources/extensions/gsd/tests/notification-widget.test.ts +25 -0
  153. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
  154. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
  155. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
  156. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
  157. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
  158. package/src/resources/extensions/gsd/workflow-events.ts +34 -25
  159. package/src/resources/skills/create-skill/SKILL.md +2 -0
  160. /package/dist/web/standalone/.next/static/{20e8bFnNjxQJflHNodEve → dYVdRaunb2ZSEA8fjkT-V}/_buildManifest.js +0 -0
  161. /package/dist/web/standalone/.next/static/{20e8bFnNjxQJflHNodEve → dYVdRaunb2ZSEA8fjkT-V}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -27,36 +27,43 @@ One command. Walk away. Come back to a built project with clean git history.
27
27
 
28
28
  ---
29
29
 
30
- ## What's New in v2.68
30
+ ## What's New in v2.71
31
31
 
32
- ### MCP Workflow Tools
32
+ ### MCP Secure Env Collect
33
33
 
34
- - **Full workflow over MCP** — slice replanning, milestone management, slice completion, task completion, and core planning tools are now exposed over MCP for external integrations.
35
- - **Transport-gated MCP** — workflow tool availability adapts to provider transport capabilities automatically.
36
- - **Write gate enforcement** — workflow MCP respects write gates, preventing unauthorized state mutations from external clients.
34
+ - **Secure credential collection over MCP** — the new `secure_env_collect` tool uses MCP form elicitation to collect secrets (API keys, tokens) from external clients without exposing values in tool output. Masks input in interactive mode.
35
+ - **Hardened elicitation schema** — MCP elicitation schema handling is stricter, with proper validation and fallback for providers that don't support forms.
37
36
 
38
- ### Reliability & Recovery
37
+ ### MCP Reliability
39
38
 
40
- - **False degraded-mode fix** — eliminates spurious degraded-mode warnings when the DB hasn't been initialized yet.
41
- - **Stale session resume suppression** — prevents stale interrupted-session resume prompts from hijacking fresh sessions.
42
- - **Merge conflict recovery** — `autoCommitDirtyState` guarded with cwd restore on `MergeConflictError`.
43
- - **Auto-resume hardening** — `autoStartTime` restored on resume, managed resources resynced on auto resume.
39
+ - **Stream ordering preserved** — MCP tool output now renders in the correct order, fixing interleaved output in Claude Code and other MCP clients.
40
+ - **isError flag propagation** — workflow tool execution failures now correctly return `isError: true`, so MCP clients can distinguish success from failure.
41
+ - **Multi-round discuss questions** — new-project discuss phase supports multi-round questioning with structured question gates.
44
42
 
45
- ### TUI & Developer Experience
43
+ ### TUI Fixes
46
44
 
47
- - **Contextual tips system** — TUI and web terminal now surface contextual tips based on workflow state.
48
- - **Claude Code MCP streaming** — real-time streaming and tool output rendering for Claude Code MCP connections.
45
+ - **Pinned output restored** — pinned output bar displays above the editor during tool execution again.
46
+ - **Turn completion cleanup** — pinned latest output is cleared on turn completion, preventing stale output from persisting.
47
+ - **Secure input masking** — extension input values are masked in interactive mode when collecting secrets.
49
48
 
50
- ### Infrastructure
49
+ ### Reliability & Internals
51
50
 
52
- - **Weekly model registry refresh** — CI workflow auto-regenerates the model registry on a weekly schedule.
53
- - **Codebase cache auto-refresh** — stale codebase cache is refreshed automatically without manual intervention.
51
+ - **TOCTOU file locking** — race conditions in event log and custom workflow graph file locking are fixed with proper atomic lock acquisition.
52
+ - **State derive refactor** — `deriveStateFromDb` god function extracted into composable, testable helpers.
53
+ - **Windows portability** — hardened cross-platform portability across runtime, tooling, and CI.
54
+ - **Model routing transparency** — dynamic routing is skipped for interactive dispatches; model changes are always shown in the banner.
55
+ - **Capability-aware routing (ADR-004)** — full implementation of capability scoring, `before_model_select` hook, and task metadata extraction.
56
+ - **Multi-model provider strategy (ADR-005)** — infrastructure for multi-provider model selection wired into live paths.
54
57
 
55
58
  See the full [Changelog](./CHANGELOG.md) for details on every release.
56
59
 
57
60
  <details>
58
- <summary>Previous highlights (v2.67 and earlier)</summary>
61
+ <summary>Previous highlights (v2.70 and earlier)</summary>
59
62
 
63
+ - **Full workflow over MCP (v2.68)** — slice replanning, milestone management, slice completion, task completion, and core planning tools exposed over MCP
64
+ - **Transport-gated MCP (v2.68)** — workflow tool availability adapts to provider transport capabilities automatically
65
+ - **Contextual tips system (v2.68)** — TUI and web terminal surface contextual tips based on workflow state
66
+ - **Ask user questions over MCP (v2.70)** — interactive questions exposed via elicitation for external integrations
60
67
  - **Tiered Context Injection (M005)** — relevance-scoped context with 65%+ token reduction
61
68
  - **Resilient transient error recovery** — defers to Core RetryHandler and fixes cmdCtx race conditions
62
69
  - **Anthropic subscription routing** — auto-routed through Claude Code CLI provider with proper display names
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { ensureManagedTools } from './tool-bootstrap.js';
7
7
  import { loadStoredEnvKeys } from './wizard.js';
8
8
  import { migratePiCredentials } from './pi-migration.js';
9
9
  import { validateConfiguredModel } from './startup-model-validation.js';
10
+ import { shouldMigrateAnthropicToClaudeCode } from './provider-migrations.js';
10
11
  import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
11
12
  import chalk from 'chalk';
12
13
  import { checkForUpdates } from './update-check.js';
@@ -285,7 +286,7 @@ const { resolveModelsJsonPath } = await import('./models-resolver.js');
285
286
  const modelsJsonPath = resolveModelsJsonPath();
286
287
  const modelRegistry = new ModelRegistry(authStorage, modelsJsonPath);
287
288
  markStartup('ModelRegistry');
288
- const settingsManager = SettingsManager.create(agentDir);
289
+ const settingsManager = SettingsManager.create(process.cwd(), agentDir);
289
290
  applySecurityOverrides(settingsManager);
290
291
  markStartup('SettingsManager.create');
291
292
  // Run onboarding wizard on first launch (no LLM provider configured)
@@ -401,7 +402,11 @@ if (isPrintMode) {
401
402
  // Migrate anthropic OAuth users to claude-code provider when CLI is available (#3772).
402
403
  // Anthropic blocks third-party apps from using subscription quotas — routing through
403
404
  // the local claude CLI binary is TOS-compliant.
404
- if (modelRegistry.isProviderRequestReady('claude-code') && settingsManager.getDefaultProvider() === 'anthropic') {
405
+ if (shouldMigrateAnthropicToClaudeCode({
406
+ authStorage,
407
+ isClaudeCodeReady: modelRegistry.isProviderRequestReady('claude-code'),
408
+ defaultProvider: settingsManager.getDefaultProvider(),
409
+ })) {
405
410
  const currentModelId = settingsManager.getDefaultModel();
406
411
  if (currentModelId) {
407
412
  const ccModel = modelRegistry.find('claude-code', currentModelId);
@@ -576,7 +581,11 @@ markStartup('createAgentSession');
576
581
  // Migrate anthropic OAuth users to claude-code provider when CLI is available (#3772).
577
582
  // Anthropic blocks third-party apps from using subscription quotas — routing through
578
583
  // the local claude CLI binary is TOS-compliant.
579
- if (modelRegistry.isProviderRequestReady('claude-code') && settingsManager.getDefaultProvider() === 'anthropic') {
584
+ if (shouldMigrateAnthropicToClaudeCode({
585
+ authStorage,
586
+ isClaudeCodeReady: modelRegistry.isProviderRequestReady('claude-code'),
587
+ defaultProvider: settingsManager.getDefaultProvider(),
588
+ })) {
580
589
  const currentModelId = settingsManager.getDefaultModel();
581
590
  if (currentModelId) {
582
591
  const ccModel = modelRegistry.find('claude-code', currentModelId);
@@ -1,10 +1,10 @@
1
1
  // MCP SDK subpath imports use wildcard exports (./*) that NodeNext resolves
2
2
  // at runtime but TypeScript cannot statically type-check. We construct the
3
3
  // specifiers dynamically so tsc treats them as `any`.
4
- // Use createRequire to resolve wildcard subpaths — CJS resolver auto-appends
5
- // .js, which the ESM wildcard export map does not (#3603).
6
- import { createRequire } from 'node:module';
7
- const _require = createRequire(import.meta.url);
4
+ //
5
+ // Use explicit .js subpaths for modules that are loaded dynamically at runtime.
6
+ // Recent Node / SDK combinations do not reliably resolve the extensionless
7
+ // wildcard targets for `server/stdio` and `types` (#3914).
8
8
  const MCP_PKG = '@modelcontextprotocol/sdk';
9
9
  /**
10
10
  * Starts a native MCP (Model Context Protocol) server over stdin/stdout.
@@ -23,8 +23,8 @@ const MCP_PKG = '@modelcontextprotocol/sdk';
23
23
  export async function startMcpServer(options) {
24
24
  const { tools, version = '0.0.0' } = options;
25
25
  const serverMod = await import(`${MCP_PKG}/server`);
26
- const stdioMod = await import(_require.resolve(`${MCP_PKG}/server/stdio`));
27
- const typesMod = await import(_require.resolve(`${MCP_PKG}/types`));
26
+ const stdioMod = await import(`${MCP_PKG}/server/stdio.js`);
27
+ const typesMod = await import(`${MCP_PKG}/types.js`);
28
28
  const Server = serverMod.Server;
29
29
  const StdioServerTransport = stdioMod.StdioServerTransport;
30
30
  const { ListToolsRequestSchema, CallToolRequestSchema } = typesMod;
@@ -0,0 +1,10 @@
1
+ import type { AuthStorage } from "@gsd/pi-coding-agent";
2
+ type AnthropicMigrationDeps = {
3
+ authStorage: Pick<AuthStorage, "getCredentialsForProvider">;
4
+ isClaudeCodeReady: boolean;
5
+ defaultProvider: string | undefined;
6
+ env?: NodeJS.ProcessEnv;
7
+ };
8
+ export declare function hasDirectAnthropicApiKey(authStorage: Pick<AuthStorage, "getCredentialsForProvider">, env?: NodeJS.ProcessEnv): boolean;
9
+ export declare function shouldMigrateAnthropicToClaudeCode({ authStorage, isClaudeCodeReady, defaultProvider, env, }: AnthropicMigrationDeps): boolean;
10
+ export {};
@@ -0,0 +1,12 @@
1
+ export function hasDirectAnthropicApiKey(authStorage, env = process.env) {
2
+ if ((env.ANTHROPIC_API_KEY ?? "").trim()) {
3
+ return true;
4
+ }
5
+ return authStorage.getCredentialsForProvider("anthropic").some((credential) => credential?.type === "api_key" && typeof credential?.key === "string" && credential.key.trim().length > 0);
6
+ }
7
+ export function shouldMigrateAnthropicToClaudeCode({ authStorage, isClaudeCodeReady, defaultProvider, env = process.env, }) {
8
+ if (!isClaudeCodeReady || defaultProvider !== "anthropic") {
9
+ return false;
10
+ }
11
+ return !hasDirectAnthropicApiKey(authStorage, env);
12
+ }
@@ -2,7 +2,7 @@ import { DefaultResourceLoader, sortExtensionPaths } from '@gsd/pi-coding-agent'
2
2
  import { createHash } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, closeSync, readFileSync, readlinkSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
5
- import { dirname, join, relative, resolve } from 'node:path';
5
+ import { basename, dirname, join, relative, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { compareSemver } from './update-check.js';
8
8
  import { discoverExtensionEntryPaths } from './extension-discovery.js';
@@ -254,34 +254,157 @@ function copyDirRecursive(src, dest) {
254
254
  * ~/.gsd/agent/extensions/ have no ancestor node_modules, so imports of
255
255
  * @gsd/* packages fail. The symlink makes Node's standard resolution find
256
256
  * them without requiring every call site to use jiti.
257
+ *
258
+ * Layout differences by install method:
259
+ * - Source/monorepo: packageRoot/node_modules has everything → simple symlink
260
+ * - npm/bun global: deps hoisted to dirname(packageRoot), including @gsd/* → simple symlink
261
+ * - pnpm global: external deps hoisted, but @gsd/* stays in packageRoot/node_modules
262
+ * → merged directory with symlinks from both roots (#3529, #3564)
257
263
  */
258
264
  function ensureNodeModulesSymlink(agentDir) {
259
265
  const agentNodeModules = join(agentDir, 'node_modules');
260
- const gsdNodeModules = join(packageRoot, 'node_modules');
266
+ const internalNodeModules = join(packageRoot, 'node_modules');
267
+ const hoistedNodeModules = dirname(packageRoot);
268
+ const isGlobalInstall = basename(hoistedNodeModules) === 'node_modules';
269
+ if (!isGlobalInstall) {
270
+ // Source/monorepo: internal node_modules has everything
271
+ reconcileSymlink(agentNodeModules, internalNodeModules);
272
+ return;
273
+ }
274
+ // Global install: check if workspace scopes (@gsd/*) are hoisted.
275
+ // npm/bun hoist everything; pnpm keeps workspace packages internal.
276
+ if (!hasMissingWorkspaceScopes(hoistedNodeModules, internalNodeModules)) {
277
+ // Everything is hoisted — simple symlink to parent node_modules
278
+ reconcileSymlink(agentNodeModules, hoistedNodeModules);
279
+ return;
280
+ }
281
+ // pnpm-style layout: create a real directory merging both roots
282
+ reconcileMergedNodeModules(agentNodeModules, hoistedNodeModules, internalNodeModules);
283
+ }
284
+ /** Check if any @gsd* scopes exist in internal but not in hoisted node_modules */
285
+ function hasMissingWorkspaceScopes(hoisted, internal) {
286
+ if (!existsSync(internal))
287
+ return false;
261
288
  try {
262
- const stat = lstatSync(agentNodeModules);
289
+ for (const entry of readdirSync(internal, { withFileTypes: true })) {
290
+ if (entry.isDirectory() && entry.name.startsWith('@gsd') &&
291
+ !existsSync(join(hoisted, entry.name))) {
292
+ return true;
293
+ }
294
+ }
295
+ }
296
+ catch { /* non-fatal */ }
297
+ return false;
298
+ }
299
+ /** Ensure a symlink at `link` points to `target`, fixing stale/wrong entries */
300
+ function reconcileSymlink(link, target) {
301
+ try {
302
+ const stat = lstatSync(link);
263
303
  if (stat.isSymbolicLink()) {
264
- const existing = readlinkSync(agentNodeModules);
265
- // Symlink exists verify it points to the correct, existing target
266
- if (existing === gsdNodeModules && existsSync(agentNodeModules))
304
+ const existing = readlinkSync(link);
305
+ if (existing === target && existsSync(link))
267
306
  return; // correct and target exists
268
- // Stale or wrong target — remove and recreate
307
+ unlinkSync(link);
308
+ }
309
+ else {
310
+ // Real directory (or merged dir from previous pnpm fix) — remove it
311
+ rmSync(link, { recursive: true, force: true });
312
+ }
313
+ }
314
+ catch {
315
+ // lstatSync throws if path doesn't exist — fine, we'll create below
316
+ }
317
+ try {
318
+ symlinkSync(target, link, 'junction');
319
+ }
320
+ catch (err) {
321
+ console.error(`[gsd] WARN: Failed to symlink ${link} → ${target}: ${err instanceof Error ? err.message : err}`);
322
+ }
323
+ }
324
+ /**
325
+ * Create a real node_modules directory containing symlinks from both the
326
+ * hoisted root (external deps) and internal root (@gsd/* workspace packages).
327
+ * Used for pnpm global installs where @gsd/* isn't hoisted.
328
+ */
329
+ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
330
+ // Fast path: if already merged for this packageRoot + same directory contents, skip.
331
+ // The fingerprint includes entry names from both roots so `pnpm add/remove` triggers rebuild.
332
+ const marker = join(agentNodeModules, '.gsd-merged');
333
+ const fingerprint = mergedFingerprint(hoisted, internal);
334
+ try {
335
+ if (existsSync(marker) && readFileSync(marker, 'utf-8').trim() === fingerprint)
336
+ return;
337
+ }
338
+ catch { /* rebuild */ }
339
+ // Remove any existing symlink or stale merged directory
340
+ try {
341
+ const stat = lstatSync(agentNodeModules);
342
+ if (stat.isSymbolicLink()) {
269
343
  unlinkSync(agentNodeModules);
270
344
  }
271
345
  else {
272
- // Real directory (not a symlink) is blocking — remove it
273
346
  rmSync(agentNodeModules, { recursive: true, force: true });
274
347
  }
275
348
  }
276
- catch {
277
- // lstatSync throws if path doesn't exist — that's fine, we'll create below
349
+ catch { /* doesn't exist */ }
350
+ mkdirSync(agentNodeModules, { recursive: true });
351
+ let linkedCount = 0;
352
+ // Symlink entries from the hoisted node_modules (external deps)
353
+ try {
354
+ for (const entry of readdirSync(hoisted, { withFileTypes: true })) {
355
+ // Skip the gsd-pi package itself and dotfiles
356
+ if (entry.name === basename(packageRoot))
357
+ continue;
358
+ if (entry.name.startsWith('.'))
359
+ continue;
360
+ try {
361
+ symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name));
362
+ linkedCount++;
363
+ }
364
+ catch { /* skip individual */ }
365
+ }
278
366
  }
367
+ catch (err) {
368
+ console.error(`[gsd] WARN: Failed to read hoisted node_modules at ${hoisted}: ${err instanceof Error ? err.message : err}`);
369
+ }
370
+ // Overlay @gsd* workspace scopes from internal node_modules
279
371
  try {
280
- symlinkSync(gsdNodeModules, agentNodeModules, 'junction');
372
+ for (const entry of readdirSync(internal, { withFileTypes: true })) {
373
+ if (!entry.name.startsWith('@gsd'))
374
+ continue;
375
+ const link = join(agentNodeModules, entry.name);
376
+ try {
377
+ lstatSync(link);
378
+ unlinkSync(link);
379
+ }
380
+ catch { /* didn't exist */ }
381
+ try {
382
+ symlinkSync(join(internal, entry.name), link);
383
+ linkedCount++;
384
+ }
385
+ catch { /* skip individual */ }
386
+ }
281
387
  }
282
388
  catch (err) {
283
- // This failure makes GSD non-functional extensions can't resolve @gsd/* packages
284
- console.error(`[gsd] WARN: Failed to symlink ${agentNodeModules} → ${gsdNodeModules}: ${err instanceof Error ? err.message : err}`);
389
+ console.error(`[gsd] WARN: Failed to read internal node_modules at ${internal}: ${err instanceof Error ? err.message : err}`);
390
+ }
391
+ // Only stamp marker if we actually linked something — avoids caching a broken state
392
+ if (linkedCount > 0) {
393
+ try {
394
+ writeFileSync(marker, fingerprint);
395
+ }
396
+ catch { /* non-fatal */ }
397
+ }
398
+ }
399
+ /** Build a cache fingerprint from packageRoot + sorted entry names of both directories */
400
+ function mergedFingerprint(hoisted, internal) {
401
+ try {
402
+ const h = readdirSync(hoisted).sort().join(',');
403
+ const i = readdirSync(internal).sort().join(',');
404
+ return `${packageRoot}\n${h}\n${i}`;
405
+ }
406
+ catch {
407
+ return packageRoot; // fallback: at least invalidate on version change
285
408
  }
286
409
  }
287
410
  /**
@@ -275,7 +275,7 @@ Work flows through these phases. Each phase produces a file.
275
275
  **How to do it manually:**
276
276
  1. Read the roadmap to understand the scope.
277
277
  2. Identify 3-5 gray areas — implementation decisions the user cares about.
278
- 3. Use `ask_user_questions` to discuss each area.
278
+ 3. Use `ask_user_questions` to discuss each area, one round at a time. Never fabricate user input; wait for the user's actual response before the next round.
279
279
  4. Write decisions to the appropriate context file (`M###-CONTEXT.md` or `S##-CONTEXT.md`).
280
280
  5. Do NOT discuss how to implement — only what the user wants.
281
281
 
@@ -523,7 +523,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
523
523
  }
524
524
  }
525
525
  // ── DB lifecycle ──
526
- const gsdDbPath = join(s.basePath, ".gsd", "gsd.db");
526
+ const gsdDbPath = resolveProjectRootDbPath(s.basePath);
527
527
  const gsdDirPath = join(s.basePath, ".gsd");
528
528
  if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
529
529
  const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
@@ -82,7 +82,7 @@ export function clearInFlightTools() {
82
82
  * handler. When these errors occur, retrying the same unit will produce the same
83
83
  * failure, so the retry loop must be broken.
84
84
  */
85
- const TOOL_INVOCATION_ERROR_RE = /Validation failed for tool|Expected ',' or '\}' in JSON|Unexpected end of JSON|Unexpected token.*in JSON/i;
85
+ const TOOL_INVOCATION_ERROR_RE = /Validation failed for tool|Expected ',' or '\}'(?: after property value)?(?: in JSON)?|Unexpected end of JSON|Unexpected token.*in JSON/i;
86
86
  /**
87
87
  * Returns true if the error message indicates a tool invocation failure due to
88
88
  * malformed/truncated arguments (as opposed to a normal tool execution error).
@@ -111,6 +111,8 @@ export function registerHooks(pi) {
111
111
  return { cancel: true };
112
112
  }
113
113
  const basePath = process.cwd();
114
+ const { ensureDbOpen } = await import("./dynamic-tools.js");
115
+ await ensureDbOpen();
114
116
  const state = await deriveState(basePath);
115
117
  if (!state.activeMilestone || !state.activeSlice || !state.activeTask)
116
118
  return;
@@ -257,6 +257,10 @@ function buildWorktreeContextBlock() {
257
257
  */
258
258
  const RESUME_INTENT_PATTERNS = /^(continue|resume|ok|go|go ahead|proceed|keep going|carry on|next|yes|yeah|yep|sure|do it|let's go|pick up where you left off)$/;
259
259
  async function buildGuidedExecuteContextInjection(prompt, basePath) {
260
+ const ensureStateDbOpen = async () => {
261
+ const { ensureDbOpen } = await import("./dynamic-tools.js");
262
+ await ensureDbOpen();
263
+ };
260
264
  const executeMatch = prompt.match(/Execute the next task:\s+(T\d+)\s+\("([^"]+)"\)\s+in slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i);
261
265
  if (executeMatch) {
262
266
  const [, taskId, taskTitle, sliceId, milestoneId] = executeMatch;
@@ -265,6 +269,7 @@ async function buildGuidedExecuteContextInjection(prompt, basePath) {
265
269
  const resumeMatch = prompt.match(/Resume interrupted work\.[\s\S]*?slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i);
266
270
  if (resumeMatch) {
267
271
  const [, sliceId, milestoneId] = resumeMatch;
272
+ await ensureStateDbOpen();
268
273
  const state = await deriveState(basePath);
269
274
  if (state.activeMilestone?.id === milestoneId && state.activeSlice?.id === sliceId && state.activeTask) {
270
275
  return buildTaskExecutionContextInjection(basePath, milestoneId, sliceId, state.activeTask.id, state.activeTask.title);
@@ -279,6 +284,7 @@ async function buildGuidedExecuteContextInjection(prompt, basePath) {
279
284
  // replanning, gate evaluation, or other non-execution phases.
280
285
  const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
281
286
  if (RESUME_INTENT_PATTERNS.test(trimmed)) {
287
+ await ensureStateDbOpen();
282
288
  const state = await deriveState(basePath);
283
289
  if (state.phase === "executing" && state.activeTask && state.activeMilestone && state.activeSlice) {
284
290
  return buildTaskExecutionContextInjection(basePath, state.activeMilestone.id, state.activeSlice.id, state.activeTask.id, state.activeTask.title);
@@ -1,8 +1,18 @@
1
1
  import { checkRemoteAutoSession, isAutoActive, isAutoPaused, stopAutoRemote } from "../auto.js";
2
- import { assertSafeDirectory } from "../validate-directory.js";
2
+ import { validateDirectory } from "../validate-directory.js";
3
3
  import { resolveProjectRoot } from "../worktree.js";
4
4
  import { showNextAction } from "../../shared/tui.js";
5
5
  import { handleStatus } from "./handlers/core.js";
6
+ /**
7
+ * Typed error for when GSD is run outside a valid project directory.
8
+ * Command handlers catch this to show a friendly message instead of a raw exception.
9
+ */
10
+ export class GSDNoProjectError extends Error {
11
+ constructor(reason) {
12
+ super(reason);
13
+ this.name = "GSDNoProjectError";
14
+ }
15
+ }
6
16
  export function projectRoot() {
7
17
  let cwd;
8
18
  try {
@@ -13,11 +23,10 @@ export function projectRoot() {
13
23
  cwd = process.env.HOME ?? "/";
14
24
  }
15
25
  const root = resolveProjectRoot(cwd);
16
- if (root !== cwd) {
17
- assertSafeDirectory(cwd);
18
- }
19
- else {
20
- assertSafeDirectory(root);
26
+ const pathToCheck = root !== cwd ? cwd : root;
27
+ const result = validateDirectory(pathToCheck);
28
+ if (result.severity === "blocked") {
29
+ throw new GSDNoProjectError(result.reason ?? "GSD must be run inside a project directory.");
21
30
  }
22
31
  return root;
23
32
  }
@@ -1,3 +1,4 @@
1
+ import { GSDNoProjectError } from "./context.js";
1
2
  import { handleAutoCommand } from "./handlers/auto.js";
2
3
  import { handleCoreCommand } from "./handlers/core.js";
3
4
  import { handleOpsCommand } from "./handlers/ops.js";
@@ -12,10 +13,19 @@ export async function handleGSDCommand(args, ctx, pi) {
12
13
  () => handleWorkflowCommand(trimmed, ctx, pi),
13
14
  () => handleOpsCommand(trimmed, ctx, pi),
14
15
  ];
15
- for (const handler of handlers) {
16
- if (await handler()) {
16
+ try {
17
+ for (const handler of handlers) {
18
+ if (await handler()) {
19
+ return;
20
+ }
21
+ }
22
+ }
23
+ catch (err) {
24
+ if (err instanceof GSDNoProjectError) {
25
+ ctx.ui.notify(`${err.message} \`cd\` into a project directory first.`, "warning");
17
26
  return;
18
27
  }
28
+ throw err;
19
29
  }
20
30
  ctx.ui.notify(`Unknown: /gsd ${trimmed}. Run /gsd help for available commands.`, "warning");
21
31
  }
@@ -17,6 +17,7 @@ import { parse } from "yaml";
17
17
  import { readGraph, writeGraph, getNextPendingStep, markStepComplete, expandIteration, } from "./graph.js";
18
18
  import { injectContext } from "./context-injector.js";
19
19
  import { parseUnitId } from "./unit-id.js";
20
+ import { withFileLock } from "./file-lock.js";
20
21
  /** Read and parse the frozen DEFINITION.yaml from a run directory. */
21
22
  export function readFrozenDefinition(runDir) {
22
23
  const defPath = join(runDir, "DEFINITION.yaml");
@@ -135,18 +136,21 @@ export class CustomWorkflowEngine {
135
136
  * Returns "milestone-complete" when all steps are now done, "continue" otherwise.
136
137
  */
137
138
  async reconcile(state, completedStep) {
138
- // Re-read the graph from disk so we do not overwrite concurrent
139
- // workflow edits with a stale in-memory snapshot from deriveState().
140
- const graph = readGraph(this.runDir);
141
- // Extract stepId from "<workflowName>/<stepId>"
142
- const { milestone, slice, task } = parseUnitId(completedStep.unitId);
143
- const stepId = task ?? slice ?? milestone;
144
- const updatedGraph = markStepComplete(graph, stepId);
145
- writeGraph(this.runDir, updatedGraph);
146
- const allDone = updatedGraph.steps.every((s) => s.status === "complete" || s.status === "expanded");
147
- return {
148
- outcome: allDone ? "milestone-complete" : "continue",
149
- };
139
+ const graphPath = join(this.runDir, "GRAPH.yaml");
140
+ return await withFileLock(graphPath, () => {
141
+ // Re-read the graph from disk so we do not overwrite concurrent
142
+ // workflow edits with a stale in-memory snapshot from deriveState().
143
+ const graph = readGraph(this.runDir);
144
+ // Extract stepId from "<workflowName>/<stepId>"
145
+ const { milestone, slice, task } = parseUnitId(completedStep.unitId);
146
+ const stepId = task ?? slice ?? milestone;
147
+ const updatedGraph = markStepComplete(graph, stepId);
148
+ writeGraph(this.runDir, updatedGraph);
149
+ const allDone = updatedGraph.steps.every((s) => s.status === "complete" || s.status === "expanded");
150
+ return {
151
+ outcome: allDone ? "milestone-complete" : "continue",
152
+ };
153
+ });
150
154
  }
151
155
  /**
152
156
  * Return UI-facing metadata for progress display.
@@ -102,10 +102,27 @@ export function getPriorSliceCompletionBlocker(base, _mainBranch, unitType, unit
102
102
  }
103
103
  }
104
104
  else {
105
+ // Positional fallback is only a heuristic for legacy slices with no
106
+ // declared dependencies. Skip any earlier slice that depends on the
107
+ // target, directly or transitively, or we can deadlock a valid zero-dep
108
+ // slice behind its own downstream dependents (#3720).
109
+ const reverseDependents = new Set();
110
+ let changed = true;
111
+ while (changed) {
112
+ changed = false;
113
+ for (const slice of slices) {
114
+ if (reverseDependents.has(slice.id))
115
+ continue;
116
+ if (slice.depends.some((depId) => depId === targetSid || reverseDependents.has(depId))) {
117
+ reverseDependents.add(slice.id);
118
+ changed = true;
119
+ }
120
+ }
121
+ }
105
122
  const targetIndex = slices.findIndex((slice) => slice.id === targetSid);
106
123
  const incomplete = slices
107
124
  .slice(0, targetIndex)
108
- .find((slice) => !slice.done);
125
+ .find((slice) => !slice.done && !reverseDependents.has(slice.id));
109
126
  if (incomplete) {
110
127
  return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${targetMid}/${incomplete.id} is not complete.`;
111
128
  }
@@ -23,7 +23,7 @@ const RATE_LIMIT_RE = /rate.?limit|too many requests|429/i;
23
23
  const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i;
24
24
  const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
25
25
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
26
- const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
26
+ const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
27
27
  // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
28
28
  // This eliminates the need to enumerate every error message variant individually.
29
29
  const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
@@ -0,0 +1,60 @@
1
+ import { existsSync } from "node:fs";
2
+ function _require(name) {
3
+ try {
4
+ return require(name);
5
+ }
6
+ catch {
7
+ try {
8
+ const gsdPiRequire = require("module").createRequire(require("path").join(process.cwd(), "node_modules", "gsd-pi", "index.js"));
9
+ return gsdPiRequire(name);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ }
16
+ export function withFileLockSync(filePath, fn) {
17
+ const lockfile = _require("proper-lockfile");
18
+ if (!lockfile)
19
+ return fn();
20
+ if (!existsSync(filePath))
21
+ return fn();
22
+ try {
23
+ const release = lockfile.lockSync(filePath, { retries: 5, stale: 10000 });
24
+ try {
25
+ return fn();
26
+ }
27
+ finally {
28
+ release();
29
+ }
30
+ }
31
+ catch (err) {
32
+ if (err.code === "ELOCKED") {
33
+ // Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
34
+ return fn();
35
+ }
36
+ throw err;
37
+ }
38
+ }
39
+ export async function withFileLock(filePath, fn) {
40
+ const lockfile = _require("proper-lockfile");
41
+ if (!lockfile)
42
+ return await fn();
43
+ if (!existsSync(filePath))
44
+ return await fn();
45
+ try {
46
+ const release = await lockfile.lock(filePath, { retries: 5, stale: 10000 });
47
+ try {
48
+ return await fn();
49
+ }
50
+ finally {
51
+ await release();
52
+ }
53
+ }
54
+ catch (err) {
55
+ if (err.code === "ELOCKED") {
56
+ return await fn();
57
+ }
58
+ throw err;
59
+ }
60
+ }