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.
- package/README.md +24 -17
- package/dist/cli.js +12 -3
- package/dist/mcp-server.js +6 -6
- package/dist/provider-migrations.d.ts +10 -0
- package/dist/provider-migrations.js +12 -0
- package/dist/resource-loader.js +136 -13
- package/dist/resources/GSD-WORKFLOW.md +1 -1
- package/dist/resources/extensions/gsd/auto-start.js +1 -1
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
- package/dist/resources/extensions/gsd/commands/context.js +15 -6
- package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
- package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
- package/dist/resources/extensions/gsd/error-classifier.js +1 -1
- package/dist/resources/extensions/gsd/file-lock.js +60 -0
- package/dist/resources/extensions/gsd/notification-store.js +21 -1
- package/dist/resources/extensions/gsd/notification-widget.js +1 -1
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/dist/resources/extensions/gsd/state.js +234 -332
- package/dist/resources/extensions/gsd/workflow-events.js +25 -13
- package/dist/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +21 -11
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
- package/packages/mcp-server/src/workflow-tools.ts +31 -11
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
- package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
- package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +202 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +90 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +57 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +249 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +96 -2
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +65 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
- package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +17 -1
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
- package/packages/pi-tui/src/components/markdown.ts +22 -1
- package/pkg/package.json +1 -1
- package/src/resources/GSD-WORKFLOW.md +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
- package/src/resources/extensions/gsd/commands/context.ts +16 -5
- package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
- package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
- package/src/resources/extensions/gsd/error-classifier.ts +1 -1
- package/src/resources/extensions/gsd/file-lock.ts +59 -0
- package/src/resources/extensions/gsd/notification-store.ts +19 -1
- package/src/resources/extensions/gsd/notification-widget.ts +1 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/src/resources/extensions/gsd/state.ts +274 -344
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
- package/src/resources/extensions/gsd/workflow-events.ts +34 -25
- package/src/resources/skills/create-skill/SKILL.md +2 -0
- /package/dist/web/standalone/.next/static/{20e8bFnNjxQJflHNodEve → dYVdRaunb2ZSEA8fjkT-V}/_buildManifest.js +0 -0
- /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.
|
|
30
|
+
## What's New in v2.71
|
|
31
31
|
|
|
32
|
-
### MCP
|
|
32
|
+
### MCP Secure Env Collect
|
|
33
33
|
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
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
|
|
37
|
+
### MCP Reliability
|
|
39
38
|
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
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
|
|
43
|
+
### TUI Fixes
|
|
46
44
|
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
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
|
-
###
|
|
49
|
+
### Reliability & Internals
|
|
51
50
|
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
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.
|
|
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 (
|
|
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 (
|
|
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);
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
//
|
|
5
|
-
// .js
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
27
|
-
const typesMod = await import(
|
|
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
|
+
}
|
package/dist/resource-loader.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
}
|