@schoolai/shipyard 3.18.0 → 3.18.1-rc.20260629.0
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/dist/capability-detector-worker.js +6 -6
- package/dist/{chunk-CTCRVL3I.js → chunk-4AUX3FMD.js} +2 -2
- package/dist/chunk-4AUX3FMD.js.map +1 -0
- package/dist/{chunk-H2NFBWK7.js → chunk-5GY7WGIC.js} +6 -5
- package/dist/{chunk-H2NFBWK7.js.map → chunk-5GY7WGIC.js.map} +1 -1
- package/dist/{chunk-ETUNY4NF.js → chunk-BK24XTBR.js} +375 -4
- package/dist/chunk-BK24XTBR.js.map +1 -0
- package/dist/{chunk-D3JA5MXR.js → chunk-CKQL7TWQ.js} +4 -3
- package/dist/{chunk-D3JA5MXR.js.map → chunk-CKQL7TWQ.js.map} +1 -1
- package/dist/{chunk-LXIRZZFC.js → chunk-D6RYU2JU.js} +14 -5
- package/dist/{chunk-LXIRZZFC.js.map → chunk-D6RYU2JU.js.map} +1 -1
- package/dist/{chunk-72CO4KIJ.js → chunk-E74MXZWW.js} +2 -2
- package/dist/{chunk-RM4WIKPZ.js → chunk-K4HUYO2V.js} +3 -3
- package/dist/{chunk-NIVME6MT.js → chunk-LN2AW36U.js} +134 -53
- package/dist/chunk-LN2AW36U.js.map +1 -0
- package/dist/{chunk-UD4BVZOD.js → chunk-PXZCSRS5.js} +5 -5
- package/dist/chunk-PXZCSRS5.js.map +1 -0
- package/dist/{chunk-UTLVWPBT.js → chunk-VDK5N7EH.js} +38 -2
- package/dist/chunk-VDK5N7EH.js.map +1 -0
- package/dist/{chunk-DHYIAHZN.js → chunk-XXRTRTG4.js} +2 -2
- package/dist/cursor-runner.js +14 -7
- package/dist/cursor-runner.js.map +1 -1
- package/dist/electron-utility.js +2 -2
- package/dist/{git-repo-KK5ZNJUW.js → git-repo-6DHYJICW.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{login-53IPU5BJ.js → login-4AUKCCVX.js} +3 -3
- package/dist/{mcp-servers-WZF27B7W.js → mcp-servers-Z3LIT6CL.js} +2 -2
- package/dist/{plan-backfill-2AOFAPND.js → plan-backfill-X4FGEJ4G.js} +4 -4
- package/dist/{serve-NTV6KMY3.js → serve-IP4COF55.js} +12370 -11791
- package/dist/{serve-NTV6KMY3.js.map → serve-IP4COF55.js.map} +1 -1
- package/dist/{start-MW3BBTC3.js → start-P57AEVJI.js} +6 -6
- package/package.json +10 -10
- package/dist/chunk-CTCRVL3I.js.map +0 -1
- package/dist/chunk-ETUNY4NF.js.map +0 -1
- package/dist/chunk-NIVME6MT.js.map +0 -1
- package/dist/chunk-UD4BVZOD.js.map +0 -1
- package/dist/chunk-UTLVWPBT.js.map +0 -1
- /package/dist/{chunk-72CO4KIJ.js.map → chunk-E74MXZWW.js.map} +0 -0
- /package/dist/{chunk-RM4WIKPZ.js.map → chunk-K4HUYO2V.js.map} +0 -0
- /package/dist/{chunk-DHYIAHZN.js.map → chunk-XXRTRTG4.js.map} +0 -0
- /package/dist/{git-repo-KK5ZNJUW.js.map → git-repo-6DHYJICW.js.map} +0 -0
- /package/dist/{login-53IPU5BJ.js.map → login-4AUKCCVX.js.map} +0 -0
- /package/dist/{mcp-servers-WZF27B7W.js.map → mcp-servers-Z3LIT6CL.js.map} +0 -0
- /package/dist/{plan-backfill-2AOFAPND.js.map → plan-backfill-X4FGEJ4G.js.map} +0 -0
- /package/dist/{start-MW3BBTC3.js.map → start-P57AEVJI.js.map} +0 -0
|
@@ -313,7 +313,8 @@ var RunnerToDaemonSchema = external_exports.discriminatedUnion("kind", [
|
|
|
313
313
|
inputTokens: external_exports.number().int().nonnegative(),
|
|
314
314
|
outputTokens: external_exports.number().int().nonnegative(),
|
|
315
315
|
cacheReadTokens: external_exports.number().int().nonnegative(),
|
|
316
|
-
cacheWriteTokens: external_exports.number().int().nonnegative()
|
|
316
|
+
cacheWriteTokens: external_exports.number().int().nonnegative(),
|
|
317
|
+
reasoningTokens: external_exports.number().int().nonnegative().optional()
|
|
317
318
|
}).optional(),
|
|
318
319
|
/**
|
|
319
320
|
* `Run.createdAt` from `@cursor/sdk` — authoritative SDK-side run-start
|
|
@@ -368,7 +369,7 @@ var RunnerToDaemonSchema = external_exports.discriminatedUnion("kind", [
|
|
|
368
369
|
* summarization (its native "compaction") started / completed (B2). These
|
|
369
370
|
* arrive on the `SendOptions.onDelta` interaction-update stream as
|
|
370
371
|
* `SummaryStartedUpdate` / `SummaryCompletedUpdate`, NOT on the public
|
|
371
|
-
* `Run.stream()` `SDKMessage` union (verified against @cursor/sdk@1.0.
|
|
372
|
+
* `Run.stream()` `SDKMessage` union (verified against @cursor/sdk@1.0.22 —
|
|
372
373
|
* `run.d.ts` exposes only `stream()`/`conversation()`/`wait()`, and the
|
|
373
374
|
* bundle routes `summary-*` updates exclusively through `onDelta`). The
|
|
374
375
|
* daemon translates these into normalized `compaction_started` /
|
|
@@ -501,4 +502,4 @@ export {
|
|
|
501
502
|
parseDaemonToRunner,
|
|
502
503
|
parseRunnerToDaemon
|
|
503
504
|
};
|
|
504
|
-
//# sourceMappingURL=chunk-
|
|
505
|
+
//# sourceMappingURL=chunk-CKQL7TWQ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/services/session/cursor-runner-protocol.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * IPC protocol between the daemon (parent) and the `cursor-runner` forked\n * child subprocess.\n *\n * Wire format: structured-clone over the Node IPC channel\n * (`child.send(msg)` / `process.on('message', msg => ...)`). The schemas\n * below are Zod-parsed on receive so a corrupt or version-skewed peer\n * cannot drive the recipient into an undefined state.\n *\n * Stdout/stderr of the runner are inherited from the daemon (see S2b's\n * `child_process.fork({ stdio: 'inherit', silent: false })`) — those are\n * NOT carrying IPC traffic, they are for human-readable logs and native\n * panic stack traces.\n *\n * `generation` is a per-turn monotonic counter minted by the daemon-side\n * adapter. Every per-turn message in either direction is stamped with the\n * value live at the time of mint; the daemon drops inbound events whose\n * generation does not match the current run (closes Invariant #16 — the\n * transport discriminator must be present at every layer). The runner\n * does NOT drop on its own — it echoes whatever generation the daemon\n * stamped, so the daemon can correlate.\n *\n * Lifecycle events that are not per-turn (`init`, `init_ok`,\n * `init_error`, `log`, `close`) carry no generation.\n *\n * MCP server config shape: `Record<string, unknown>` here is intentional.\n * We keep the wire schema decoupled from `@cursor/sdk`'s `McpServerConfig`\n * to avoid coupling the daemon and runner to the same exact SDK version\n * at the type level. The runner re-validates against the SDK shape at\n * the call site.\n */\n\nconst McpServersSchema = z.record(z.string(), z.unknown());\nconst CursorResolvedSkillBodySchema = z.object({\n name: z.string(),\n namespace: z.string().optional(),\n body: z.string(),\n});\n\n/**\n * DaemonToRunner — outbound from the daemon-side `CursorAgentSubprocess`\n * adapter to the forked runner child.\n */\nexport const DaemonToRunnerSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('init'),\n taskId: z.string(),\n cwd: z.string(),\n apiKey: z.string(),\n modelId: z.string(),\n mcpServers: McpServersSchema,\n harnessUrl: z.string(),\n harnessToken: z.string(),\n /**\n * Stable per-runner identity for the runner-scoped harness path (C8/C9\n * warm pool). The runner puts this in the `x-shipyard-runner-id` header\n * (instead of the per-task `x-shipyard-task-id`) so the harness MCP-server\n * config — and therefore the SDK's process-scoped executor cache key — is\n * byte-identical across every task the runner serves. Optional/additive:\n * a pre-pool daemon omits it, and the runner falls back to the legacy\n * `x-shipyard-task-id` header keyed on `taskId`.\n */\n harnessRunnerId: z.string().optional(),\n /**\n * When true, the runner is pool-managed: on `close` it disposes the Agent\n * but does NOT `process.exit(0)` — it resets to a quiescent state and acks\n * `pooled_idle` so the daemon can keep the warm process (and its\n * `localExecutorCache`) alive for the next matching task. Optional/additive\n * — absent means \"throwaway runner, exit on close\" (legacy behavior).\n */\n pooled: z.boolean().optional(),\n /**\n * Absolute filesystem path where skill files for this task are\n * written (see plan v2 E#V7). The runner does not enumerate the\n * directory — it just forwards the path to the SDK so any future\n * skills-loading code has the location. May be empty string if\n * skills are disabled for this run.\n */\n skillsDir: z.string(),\n sessionId: z.string(),\n generation: z.number().int().nonnegative(),\n /**\n * Cross-runtime structured handoff (Wave 7.5). When set, the daemon\n * already wrote a fabricated `ConversationStateStructure` Merkle tree\n * to the SDK's SQLite stores during the pre-fork exclusive write\n * window. The runner MUST call `Agent.resume(injectedAgentId, ...)`\n * instead of `Agent.create(...)` so the SDK reads back the prior\n * conversation as context. Same-id `Agent.create()` would\n * PRIMARY-KEY-conflict on the agents row.\n *\n * Daemon↔forked-child IPC, NOT WebRTC — no PROTOCOL_VERSION bump.\n */\n injectedAgentId: z.string().optional(),\n /**\n * Persisted Cursor agentId from a prior `init_ok`. When set, the runner\n * calls `Agent.resume(resumeAgentId, ...)` instead of `Agent.create()` —\n * SDK reads its SQLite checkpoint store keyed by agentId and brings up\n * the prior conversation without rebuilding from scratch. Cold-build\n * cost (Statsig bootstrap + workspace runtime + MCP startup) is still\n * paid; the win is preserving conversation context across daemon\n * restarts and not depending on the JSONL replay path for that.\n *\n * Distinct from `injectedAgentId`: that one is for cross-runtime\n * structured handoff where the daemon fabricates a fake conversation;\n * this one is just rejoining a previously-real conversation. Resume\n * failures (agentId not in the SDK's local store, e.g. user wiped\n * `~/.cursor/`) fall back to `Agent.create` with the same options so\n * the spawn succeeds — just without prior context.\n */\n resumeAgentId: z.string().optional(),\n /**\n * Initial Fast Mode flag from `composerSettings.fastMode`. When true\n * AND `fastModeParam` is non-null, the runner attaches the param to\n * every `agent.send()` via `SendOptions.model.params`. When false\n * (or `fastModeParam` is null), the runner sends the plain\n * `agent.send(message)` shape. Optional/additive — pre-Fast-Mode\n * daemons omit the field and the runner treats it as false.\n */\n fastMode: z.boolean().optional(),\n /**\n * Resolved Fast Mode binding for `modelId`, sourced from the\n * daemon's `cursor-models-cache`. `null` means \"the cache has no\n * Fast-Mode binding for this model right now\" (unknown id, refresh\n * failed, or the model declares no Fast-Mode-shaped parameter).\n * The runner treats both `null` and `undefined` as \"no binding,\n * never attach params\" — gating Fast Mode the same way regardless\n * of why it's missing.\n */\n fastModeParam: z.object({ id: z.string(), value: z.string() }).nullable().optional(),\n /**\n * Named subagent catalog rendered for the Cursor runtime. When present,\n * the runner passes this to `Agent.create({agents,...})` and\n * `Agent.resume({agents,...})` so Cursor's built-in `task` tool can\n * spawn named subagents from Shipyard's catalog. Optional/additive —\n * pre-catalog daemons omit the field; the runner treats absence as an\n * empty catalog (no named subagents declared to the SDK).\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump.\n */\n agents: z\n .record(\n z.string(),\n z.object({\n description: z.string(),\n prompt: z.string(),\n model: z.union([z.object({ id: z.string() }), z.literal('inherit')]).optional(),\n mcpServers: z.array(z.string()).optional(),\n })\n )\n .optional(),\n /**\n * Native Cursor SDK conversation mode for this agent. When `'plan'`, the\n * SDK tells composer-2.5 to call its built-in `createPlan` tool instead of\n * writing files. This is the SOFT signal complementing the hard-enforcement\n * layer in `.cursor/hooks.json` deny-writes. Optional/additive — pre-1.0.17\n * runners omit it and the SDK defaults to `'agent'` mode.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump required.\n */\n mode: z.enum(['agent', 'plan']).optional(),\n }),\n z.object({\n kind: z.literal('push_message'),\n generation: z.number().int().nonnegative(),\n /**\n * Content blocks the daemon wants to send as a user turn. Shape is\n * Shipyard's internal `ContentBlock[]` — the runner translates to\n * Cursor's `SDKUserMessage` at the boundary (see cursor-content-builder.ts).\n *\n * Image attachments ride as `resource` blocks with `BlobResourceContents`\n * (mime + base64 blob). The runner extracts these into `SDKImage[]` and\n * attaches them to the `SDKUserMessage` it sends to `Agent.send()`.\n */\n content: z.array(z.unknown()),\n /**\n * SKILL.md bodies resolved by the daemon from the trusted capabilities\n * skill index for this turn. The runner must use these bodies instead of\n * any path carried on browser-authored `skill_invocation` content blocks.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump required.\n */\n skillBodies: z.array(CursorResolvedSkillBodySchema).optional(),\n /**\n * Cursor SDK local recovery flag. Only the daemon-side AgentBusyError\n * retry path sets this after confirming the errored generation is still\n * current; stale-generation retries are dropped before reaching the\n * runner, so this must never expire a legitimately live successor run.\n */\n forceLocalRun: z.boolean().optional(),\n }),\n z.object({\n kind: z.literal('interrupt'),\n generation: z.number().int().nonnegative(),\n }),\n z.object({\n kind: z.literal('set_model'),\n generation: z.number().int().nonnegative(),\n modelId: z.string(),\n }),\n z.object({\n kind: z.literal('set_mcp_servers'),\n generation: z.number().int().nonnegative(),\n servers: McpServersSchema,\n }),\n z.object({\n kind: z.literal('set_harness_task_id'),\n generation: z.number().int().nonnegative(),\n taskId: z.string(),\n token: z.string(),\n }),\n /**\n * Warm-pool reinit (C9). A pooled runner whose prior task closed is being\n * claimed for a NEW task. The runner cancels any active run, disposes the\n * current Agent, updates its state to the new task, and creates a fresh\n * Agent (`Agent.resume(resumeAgentId)` if present, else `Agent.create`) —\n * reusing the warm process-scoped executor cache (cache HIT on the next\n * send, because the harness MCP entry uses the STABLE `harnessRunnerId`\n * header so `mcpServers` is byte-identical to the prior task's).\n *\n * On success → `reinit_ok { sessionId }`; on ANY throw → `reinit_error`\n * and the runner does NOT exit (the daemon kills + respawns fresh). This\n * is the fail-closed contract: a broken reinit becomes a cold spawn, never\n * a stale/cross-task state leak.\n *\n * Carries the full per-task spawn surface (everything the cache key and the\n * Agent depend on). `mcpServers` here MUST NOT include the harness entry —\n * the runner re-adds it from `harnessRunnerId`/`harnessToken` so the entry\n * is byte-identical across init→reinit.\n */\n z.object({\n kind: z.literal('reinit'),\n taskId: z.string(),\n modelId: z.string(),\n mcpServers: McpServersSchema,\n harnessRunnerId: z.string(),\n harnessToken: z.string(),\n skillsDir: z.string(),\n generation: z.number().int().nonnegative(),\n resumeAgentId: z.string().optional(),\n fastMode: z.boolean().optional(),\n fastModeParam: z.object({ id: z.string(), value: z.string() }).nullable().optional(),\n agents: z\n .record(\n z.string(),\n z.object({\n description: z.string(),\n prompt: z.string(),\n model: z.union([z.object({ id: z.string() }), z.literal('inherit')]).optional(),\n mcpServers: z.array(z.string()).optional(),\n })\n )\n .optional(),\n /**\n * Native Cursor SDK conversation mode for the task this warm runner is\n * being claimed for. Threaded on `reinit` (not just `init`) because the\n * Agent is recreated on adoption — without this, a warm-pool-adopted\n * plan-mode task would create its Agent in the SDK's default `'agent'`\n * mode and silently lose the soft plan signal. See the `init` variant's\n * `mode` doc. Optional/additive — same backwards-compat posture.\n */\n mode: z.enum(['agent', 'plan']).optional(),\n }),\n /**\n * Flip the runner's pooled flag mid-life (C9). The daemon sends this just\n * before `close` when it intends to keep the warm process: the runner's\n * `close` handler then disposes the Agent and acks `pooled_idle` instead of\n * `process.exit(0)`. Sending `pooled: false` reverts to throwaway behavior.\n */\n z.object({\n kind: z.literal('set_pooled'),\n generation: z.number().int().nonnegative(),\n pooled: z.boolean(),\n }),\n /**\n * Daemon→runner notification that the permission mode changed. The runner\n * logs this for diagnostic observability; it takes no action (mode-file\n * is the authoritative source the hook script reads on each tool call).\n */\n z.object({\n kind: z.literal('set_permission_mode'),\n generation: z.number().int().nonnegative(),\n mode: z.enum(['default', 'accept-edits', 'plan', 'bypass', 'auto']),\n }),\n /**\n * Daemon→runner notification that Fast Mode was toggled mid-session.\n * Carries the current generation (not bumped — this is per-state, not\n * per-turn) so the runner can correlate the update. Effective on the\n * NEXT `push_message`: the runner records the flag and rebuilds\n * `SendOptions` on each `agent.send()`.\n */\n z.object({\n kind: z.literal('set_fast_mode'),\n generation: z.number().int().nonnegative(),\n fastMode: z.boolean(),\n }),\n z.object({ kind: z.literal('close') }),\n]);\nexport type DaemonToRunner = z.infer<typeof DaemonToRunnerSchema>;\n\n/**\n * The agents map as it travels the wire — the `init`/`reinit` `agents` field.\n * Narrower than `@cursor/sdk`'s `AgentDefinition` (this `mcpServers` is\n * `string[]`); the runner reconstructs the full `AgentDefinition` on receipt.\n * Daemon-side code that builds agents for the wire should use THIS type so the\n * IPC payload is type-correct by construction (no boundary cast).\n */\nexport type DaemonToRunnerAgents = NonNullable<Extract<DaemonToRunner, { kind: 'init' }>['agents']>;\n\n/**\n * RunnerToDaemon — inbound to the daemon-side adapter from the runner.\n *\n * `sdk_message.payload` is typed as `unknown` on the wire and narrowed\n * to `SDKMessage` (from `@cursor/sdk`) by the daemon-side translator\n * (`cursor-event-translator.ts`, S2b). Keeping it `unknown` at the\n * protocol boundary insulates the daemon from new SDK message variants\n * — the translator decides what to drop versus translate.\n */\nexport const RunnerToDaemonSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('init_ok'),\n sessionId: z.string(),\n tools: z.array(z.string()),\n model: z.string(),\n mcpStatus: z.array(z.object({ name: z.string(), status: z.string() })),\n }),\n z.object({\n kind: z.literal('init_error'),\n error: z.string(),\n /** SDK error class name (e.g. AuthenticationError) for daemon-side classification. */\n errorName: z.string().optional(),\n /** Stable SDK/backend error code, when available. */\n errorCode: z.string().optional(),\n statusCode: z.number().int().optional(),\n /**\n * `true` for transient failures (network, rate limit) where the\n * daemon should consider a retry; `false` for configuration errors\n * (bad API key, unknown model) where retry will not help.\n */\n retryable: z.boolean(),\n }),\n z.object({\n kind: z.literal('sdk_message'),\n generation: z.number().int().nonnegative(),\n payload: z.unknown(),\n }),\n z.object({\n kind: z.literal('run_complete'),\n generation: z.number().int().nonnegative(),\n /**\n * Cursor's `RunResult` — kept as `unknown` on the wire (see\n * `sdk_message.payload` rationale). The daemon-side adapter\n * narrows this against `RunResult` from `@cursor/sdk` at the\n * call site.\n */\n result: z.unknown(),\n /**\n * Real token usage from the `turn-ended` interaction update emitted on\n * `SendOptions.onDelta`. Optional — absent when the SDK does not surface\n * usage for this run (e.g. local composer, older SDK versions). When\n * present the daemon uses real counts instead of estimated ones.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n usage: z\n .object({\n inputTokens: z.number().int().nonnegative(),\n outputTokens: z.number().int().nonnegative(),\n cacheReadTokens: z.number().int().nonnegative(),\n cacheWriteTokens: z.number().int().nonnegative(),\n })\n .optional(),\n /**\n * `Run.createdAt` from `@cursor/sdk` — authoritative SDK-side run-start\n * epoch ms (run.d.ts:42). Forwarded so the daemon's per-run cost / latency\n * accounting uses the SDK clock instead of `Date.now()` at the IPC\n * boundary (which drifts by IPC + cold-build latency). Optional —\n * older SDK builds may not emit it.\n */\n createdAt: z.number().nonnegative().optional(),\n /**\n * Total tokens reported via `token-delta` InteractionUpdate during the\n * run. Complements `usage.outputTokens` — `token-delta` carries no\n * input/cache breakdown, so use this only as a fallback when `usage` is\n * absent. Optional, omitted when zero.\n */\n tokenDeltaTotal: z.number().int().nonnegative().optional(),\n }),\n z.object({\n kind: z.literal('run_error'),\n generation: z.number().int().nonnegative(),\n error: z.string(),\n /** SDK error class name (e.g. RateLimitError) for daemon-side classification. */\n errorName: z.string().optional(),\n errorCode: z.string().optional(),\n statusCode: z.number().int().optional(),\n isRetryable: z.boolean(),\n }),\n z.object({\n kind: z.literal('log'),\n level: z.enum(['info', 'warn', 'error']),\n message: z.string(),\n data: z.unknown().optional(),\n }),\n /**\n * Runner→daemon notification that the agent proposed a plan. The runner\n * sends this when the agent calls the SDK's native `createPlan` tool via\n * `onStep` in plan mode (requires `mode:'plan'` on the `init`/`reinit`\n * IPC). The subprocess (`cursor-subprocess.ts`) turns it into a\n * `plan_content_ready` SubprocessEvent, which `task-event-dispatch.ts`\n * routes to `plan-handler.ts` (writes `~/.shipyard/plans/{taskId}.md` and\n * broadcasts `plan_detected`). Supersedes the old turn-complete plan\n * accumulator that lived in `cursor-event-translator.ts`.\n */\n z.object({\n kind: z.literal('plan_proposed'),\n generation: z.number().int().nonnegative(),\n /** Markdown plan content emitted by the agent via createPlan. */\n plan: z.string(),\n }),\n /**\n * Runner→daemon notification that Cursor's server-managed conversation\n * summarization (its native \"compaction\") started / completed (B2). These\n * arrive on the `SendOptions.onDelta` interaction-update stream as\n * `SummaryStartedUpdate` / `SummaryCompletedUpdate`, NOT on the public\n * `Run.stream()` `SDKMessage` union (verified against @cursor/sdk@1.0.13 —\n * `run.d.ts` exposes only `stream()`/`conversation()`/`wait()`, and the\n * bundle routes `summary-*` updates exclusively through `onDelta`). The\n * daemon translates these into normalized `compaction_started` /\n * `compaction_completed` SubprocessEvents so the Compaction SM resets the\n * (estimated) context fill-bar after Cursor's native summarization.\n *\n * Cursor's `summary-completed` event carries no token counts, but the runner\n * has already captured the pre-compaction usage from the preceding\n * `turn-ended` interaction update (stored in `latestUsage.ref`). The runner\n * forwards that as the optional `preTokens` field so the daemon can emit a\n * real `compaction_completed.preTokens` instead of falling back to the\n * estimated `planPostCompactTurnStats` baseline.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('summary_started'),\n generation: z.number().int().nonnegative(),\n }),\n z.object({\n kind: z.literal('summary_completed'),\n generation: z.number().int().nonnegative(),\n /** Total input-equivalent tokens at the moment summarization completed (inputTokens + cacheRead + cacheWrite). Absent when no turn-ended usage was captured before summary-started. */\n preTokens: z.number().int().nonnegative().optional(),\n }),\n /**\n * Runner→daemon heartbeat emitted on every `SendOptions.onStep` callback\n * (each `ConversationStep` boundary from the SDK). The daemon resets its\n * per-run stall timer on receipt so an active but slow run does not trip\n * the watchdog. Stale-generation filtering applies (same as sdk_message).\n */\n z.object({\n kind: z.literal('step_progress'),\n generation: z.number().int().nonnegative(),\n }),\n /**\n * Runner→daemon liveness heartbeat emitted (throttled in the runner) on any\n * `SendOptions.onDelta` interaction-update — `text-delta`, `thinking-delta`,\n * `shell-output-delta`, `step-started`, `tool-call-started`, etc. Unlike\n * `step_progress` (which fires only at completed `ConversationStep`\n * boundaries via `onStep`), this fires DURING a step while output streams.\n * The daemon resets the stall watchdog WITHOUT retiring the cold-start\n * window, so a long-but-active step (a 2-min shell/test run, a slow MCP\n * tool, a big read) no longer trips the mid-run watchdog while it is plainly\n * still working. Stale-generation filtering applies (same as sdk_message).\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('stream_activity'),\n generation: z.number().int().nonnegative(),\n }),\n /**\n * Runner→daemon notification that the SDK persisted the user turn to its\n * local store (`user-message-appended` interaction update on `onDelta`).\n * Useful as a write-confirmed ack for cross-runtime handoff durability\n * checks — the daemon knows the user message landed in the SDK's SQLite\n * checkpoint store and is recoverable via `Agent.resume`. Daemon-side\n * handler logs only for now.\n *\n * Stale-generation filtering applies.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('user_message_persisted'),\n generation: z.number().int().nonnegative(),\n sessionId: z.string(),\n }),\n /**\n * Runner→daemon liveness edge for in-flight tool execution. Emitted only\n * on the 0→1 transition (first tool call started, phase `'started'`) and\n * the 1→0 transition (last tool call completed, phase `'settled'`). Drives\n * the daemon's `toolExecutionStallTimeoutMs` window: when a tool is\n * in-flight the stall watchdog uses the extended per-tool budget so a\n * legitimate long-running tool (e.g. `git commit` with a full `pnpm check`\n * pre-commit hook, 3–10 min) is not false-killed. On settle the watchdog\n * reverts to the shorter mid-run window. Stale-generation filtering applies.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('tool_execution'),\n generation: z.number().int().nonnegative(),\n phase: z.enum(['started', 'settled']),\n }),\n /**\n * Warm-pool reinit succeeded (C9). The runner created a fresh Agent for the\n * claimed task on the warm executor. `sessionId` is the new `agent.agentId`\n * — the daemon persists it for the next resume. No generation: reinit is a\n * session-lifecycle event, not a per-turn one.\n */\n z.object({\n kind: z.literal('reinit_ok'),\n sessionId: z.string(),\n }),\n /**\n * Warm-pool reinit failed (C9). The runner could not bring up the new\n * Agent. `retryable` is informational only — the daemon's fail-closed\n * contract is to KILL this runner and spawn a fresh cold one regardless,\n * so a broken warm process can never serve the task. The runner does NOT\n * exit on its own (it has no clean state); the daemon owns the kill.\n */\n z.object({\n kind: z.literal('reinit_error'),\n error: z.string(),\n retryable: z.boolean(),\n }),\n /**\n * Pooled-close ack (C9). A pool-managed runner received `close`, disposed\n * its per-task Agent, and reset to a quiescent state WITHOUT exiting. The\n * daemon keeps the warm process (and its executor cache) for the next\n * claim. No generation — this is a session-lifecycle event.\n */\n z.object({\n kind: z.literal('pooled_idle'),\n }),\n]);\nexport type RunnerToDaemon = z.infer<typeof RunnerToDaemonSchema>;\n\n/**\n * Parse an inbound IPC message. Returns the parsed message or `null`\n * if the payload does not match the expected schema (matching the\n * watcher-worker pattern: log and continue, never throw out of the\n * IPC handler).\n */\nexport function parseDaemonToRunner(raw: unknown): DaemonToRunner | null {\n const result = DaemonToRunnerSchema.safeParse(raw);\n return result.success ? result.data : null;\n}\n\nexport function parseRunnerToDaemon(raw: unknown): RunnerToDaemon | null {\n const result = RunnerToDaemonSchema.safeParse(raw);\n return result.success ? result.data : null;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/services/session/cursor-runner-protocol.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * IPC protocol between the daemon (parent) and the `cursor-runner` forked\n * child subprocess.\n *\n * Wire format: structured-clone over the Node IPC channel\n * (`child.send(msg)` / `process.on('message', msg => ...)`). The schemas\n * below are Zod-parsed on receive so a corrupt or version-skewed peer\n * cannot drive the recipient into an undefined state.\n *\n * Stdout/stderr of the runner are inherited from the daemon (see S2b's\n * `child_process.fork({ stdio: 'inherit', silent: false })`) — those are\n * NOT carrying IPC traffic, they are for human-readable logs and native\n * panic stack traces.\n *\n * `generation` is a per-turn monotonic counter minted by the daemon-side\n * adapter. Every per-turn message in either direction is stamped with the\n * value live at the time of mint; the daemon drops inbound events whose\n * generation does not match the current run (closes Invariant #16 — the\n * transport discriminator must be present at every layer). The runner\n * does NOT drop on its own — it echoes whatever generation the daemon\n * stamped, so the daemon can correlate.\n *\n * Lifecycle events that are not per-turn (`init`, `init_ok`,\n * `init_error`, `log`, `close`) carry no generation.\n *\n * MCP server config shape: `Record<string, unknown>` here is intentional.\n * We keep the wire schema decoupled from `@cursor/sdk`'s `McpServerConfig`\n * to avoid coupling the daemon and runner to the same exact SDK version\n * at the type level. The runner re-validates against the SDK shape at\n * the call site.\n */\n\nconst McpServersSchema = z.record(z.string(), z.unknown());\nconst CursorResolvedSkillBodySchema = z.object({\n name: z.string(),\n namespace: z.string().optional(),\n body: z.string(),\n});\n\n/**\n * DaemonToRunner — outbound from the daemon-side `CursorAgentSubprocess`\n * adapter to the forked runner child.\n */\nexport const DaemonToRunnerSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('init'),\n taskId: z.string(),\n cwd: z.string(),\n apiKey: z.string(),\n modelId: z.string(),\n mcpServers: McpServersSchema,\n harnessUrl: z.string(),\n harnessToken: z.string(),\n /**\n * Stable per-runner identity for the runner-scoped harness path (C8/C9\n * warm pool). The runner puts this in the `x-shipyard-runner-id` header\n * (instead of the per-task `x-shipyard-task-id`) so the harness MCP-server\n * config — and therefore the SDK's process-scoped executor cache key — is\n * byte-identical across every task the runner serves. Optional/additive:\n * a pre-pool daemon omits it, and the runner falls back to the legacy\n * `x-shipyard-task-id` header keyed on `taskId`.\n */\n harnessRunnerId: z.string().optional(),\n /**\n * When true, the runner is pool-managed: on `close` it disposes the Agent\n * but does NOT `process.exit(0)` — it resets to a quiescent state and acks\n * `pooled_idle` so the daemon can keep the warm process (and its\n * `localExecutorCache`) alive for the next matching task. Optional/additive\n * — absent means \"throwaway runner, exit on close\" (legacy behavior).\n */\n pooled: z.boolean().optional(),\n /**\n * Absolute filesystem path where skill files for this task are\n * written (see plan v2 E#V7). The runner does not enumerate the\n * directory — it just forwards the path to the SDK so any future\n * skills-loading code has the location. May be empty string if\n * skills are disabled for this run.\n */\n skillsDir: z.string(),\n sessionId: z.string(),\n generation: z.number().int().nonnegative(),\n /**\n * Cross-runtime structured handoff (Wave 7.5). When set, the daemon\n * already wrote a fabricated `ConversationStateStructure` Merkle tree\n * to the SDK's SQLite stores during the pre-fork exclusive write\n * window. The runner MUST call `Agent.resume(injectedAgentId, ...)`\n * instead of `Agent.create(...)` so the SDK reads back the prior\n * conversation as context. Same-id `Agent.create()` would\n * PRIMARY-KEY-conflict on the agents row.\n *\n * Daemon↔forked-child IPC, NOT WebRTC — no PROTOCOL_VERSION bump.\n */\n injectedAgentId: z.string().optional(),\n /**\n * Persisted Cursor agentId from a prior `init_ok`. When set, the runner\n * calls `Agent.resume(resumeAgentId, ...)` instead of `Agent.create()` —\n * SDK reads its SQLite checkpoint store keyed by agentId and brings up\n * the prior conversation without rebuilding from scratch. Cold-build\n * cost (Statsig bootstrap + workspace runtime + MCP startup) is still\n * paid; the win is preserving conversation context across daemon\n * restarts and not depending on the JSONL replay path for that.\n *\n * Distinct from `injectedAgentId`: that one is for cross-runtime\n * structured handoff where the daemon fabricates a fake conversation;\n * this one is just rejoining a previously-real conversation. Resume\n * failures (agentId not in the SDK's local store, e.g. user wiped\n * `~/.cursor/`) fall back to `Agent.create` with the same options so\n * the spawn succeeds — just without prior context.\n */\n resumeAgentId: z.string().optional(),\n /**\n * Initial Fast Mode flag from `composerSettings.fastMode`. When true\n * AND `fastModeParam` is non-null, the runner attaches the param to\n * every `agent.send()` via `SendOptions.model.params`. When false\n * (or `fastModeParam` is null), the runner sends the plain\n * `agent.send(message)` shape. Optional/additive — pre-Fast-Mode\n * daemons omit the field and the runner treats it as false.\n */\n fastMode: z.boolean().optional(),\n /**\n * Resolved Fast Mode binding for `modelId`, sourced from the\n * daemon's `cursor-models-cache`. `null` means \"the cache has no\n * Fast-Mode binding for this model right now\" (unknown id, refresh\n * failed, or the model declares no Fast-Mode-shaped parameter).\n * The runner treats both `null` and `undefined` as \"no binding,\n * never attach params\" — gating Fast Mode the same way regardless\n * of why it's missing.\n */\n fastModeParam: z.object({ id: z.string(), value: z.string() }).nullable().optional(),\n /**\n * Named subagent catalog rendered for the Cursor runtime. When present,\n * the runner passes this to `Agent.create({agents,...})` and\n * `Agent.resume({agents,...})` so Cursor's built-in `task` tool can\n * spawn named subagents from Shipyard's catalog. Optional/additive —\n * pre-catalog daemons omit the field; the runner treats absence as an\n * empty catalog (no named subagents declared to the SDK).\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump.\n */\n agents: z\n .record(\n z.string(),\n z.object({\n description: z.string(),\n prompt: z.string(),\n model: z.union([z.object({ id: z.string() }), z.literal('inherit')]).optional(),\n mcpServers: z.array(z.string()).optional(),\n })\n )\n .optional(),\n /**\n * Native Cursor SDK conversation mode for this agent. When `'plan'`, the\n * SDK tells composer-2.5 to call its built-in `createPlan` tool instead of\n * writing files. This is the SOFT signal complementing the hard-enforcement\n * layer in `.cursor/hooks.json` deny-writes. Optional/additive — pre-1.0.17\n * runners omit it and the SDK defaults to `'agent'` mode.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump required.\n */\n mode: z.enum(['agent', 'plan']).optional(),\n }),\n z.object({\n kind: z.literal('push_message'),\n generation: z.number().int().nonnegative(),\n /**\n * Content blocks the daemon wants to send as a user turn. Shape is\n * Shipyard's internal `ContentBlock[]` — the runner translates to\n * Cursor's `SDKUserMessage` at the boundary (see cursor-content-builder.ts).\n *\n * Image attachments ride as `resource` blocks with `BlobResourceContents`\n * (mime + base64 blob). The runner extracts these into `SDKImage[]` and\n * attaches them to the `SDKUserMessage` it sends to `Agent.send()`.\n */\n content: z.array(z.unknown()),\n /**\n * SKILL.md bodies resolved by the daemon from the trusted capabilities\n * skill index for this turn. The runner must use these bodies instead of\n * any path carried on browser-authored `skill_invocation` content blocks.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump required.\n */\n skillBodies: z.array(CursorResolvedSkillBodySchema).optional(),\n /**\n * Cursor SDK local recovery flag. Only the daemon-side AgentBusyError\n * retry path sets this after confirming the errored generation is still\n * current; stale-generation retries are dropped before reaching the\n * runner, so this must never expire a legitimately live successor run.\n */\n forceLocalRun: z.boolean().optional(),\n }),\n z.object({\n kind: z.literal('interrupt'),\n generation: z.number().int().nonnegative(),\n }),\n z.object({\n kind: z.literal('set_model'),\n generation: z.number().int().nonnegative(),\n modelId: z.string(),\n }),\n z.object({\n kind: z.literal('set_mcp_servers'),\n generation: z.number().int().nonnegative(),\n servers: McpServersSchema,\n }),\n z.object({\n kind: z.literal('set_harness_task_id'),\n generation: z.number().int().nonnegative(),\n taskId: z.string(),\n token: z.string(),\n }),\n /**\n * Warm-pool reinit (C9). A pooled runner whose prior task closed is being\n * claimed for a NEW task. The runner cancels any active run, disposes the\n * current Agent, updates its state to the new task, and creates a fresh\n * Agent (`Agent.resume(resumeAgentId)` if present, else `Agent.create`) —\n * reusing the warm process-scoped executor cache (cache HIT on the next\n * send, because the harness MCP entry uses the STABLE `harnessRunnerId`\n * header so `mcpServers` is byte-identical to the prior task's).\n *\n * On success → `reinit_ok { sessionId }`; on ANY throw → `reinit_error`\n * and the runner does NOT exit (the daemon kills + respawns fresh). This\n * is the fail-closed contract: a broken reinit becomes a cold spawn, never\n * a stale/cross-task state leak.\n *\n * Carries the full per-task spawn surface (everything the cache key and the\n * Agent depend on). `mcpServers` here MUST NOT include the harness entry —\n * the runner re-adds it from `harnessRunnerId`/`harnessToken` so the entry\n * is byte-identical across init→reinit.\n */\n z.object({\n kind: z.literal('reinit'),\n taskId: z.string(),\n modelId: z.string(),\n mcpServers: McpServersSchema,\n harnessRunnerId: z.string(),\n harnessToken: z.string(),\n skillsDir: z.string(),\n generation: z.number().int().nonnegative(),\n resumeAgentId: z.string().optional(),\n fastMode: z.boolean().optional(),\n fastModeParam: z.object({ id: z.string(), value: z.string() }).nullable().optional(),\n agents: z\n .record(\n z.string(),\n z.object({\n description: z.string(),\n prompt: z.string(),\n model: z.union([z.object({ id: z.string() }), z.literal('inherit')]).optional(),\n mcpServers: z.array(z.string()).optional(),\n })\n )\n .optional(),\n /**\n * Native Cursor SDK conversation mode for the task this warm runner is\n * being claimed for. Threaded on `reinit` (not just `init`) because the\n * Agent is recreated on adoption — without this, a warm-pool-adopted\n * plan-mode task would create its Agent in the SDK's default `'agent'`\n * mode and silently lose the soft plan signal. See the `init` variant's\n * `mode` doc. Optional/additive — same backwards-compat posture.\n */\n mode: z.enum(['agent', 'plan']).optional(),\n }),\n /**\n * Flip the runner's pooled flag mid-life (C9). The daemon sends this just\n * before `close` when it intends to keep the warm process: the runner's\n * `close` handler then disposes the Agent and acks `pooled_idle` instead of\n * `process.exit(0)`. Sending `pooled: false` reverts to throwaway behavior.\n */\n z.object({\n kind: z.literal('set_pooled'),\n generation: z.number().int().nonnegative(),\n pooled: z.boolean(),\n }),\n /**\n * Daemon→runner notification that the permission mode changed. The runner\n * logs this for diagnostic observability; it takes no action (mode-file\n * is the authoritative source the hook script reads on each tool call).\n */\n z.object({\n kind: z.literal('set_permission_mode'),\n generation: z.number().int().nonnegative(),\n mode: z.enum(['default', 'accept-edits', 'plan', 'bypass', 'auto']),\n }),\n /**\n * Daemon→runner notification that Fast Mode was toggled mid-session.\n * Carries the current generation (not bumped — this is per-state, not\n * per-turn) so the runner can correlate the update. Effective on the\n * NEXT `push_message`: the runner records the flag and rebuilds\n * `SendOptions` on each `agent.send()`.\n */\n z.object({\n kind: z.literal('set_fast_mode'),\n generation: z.number().int().nonnegative(),\n fastMode: z.boolean(),\n }),\n z.object({ kind: z.literal('close') }),\n]);\nexport type DaemonToRunner = z.infer<typeof DaemonToRunnerSchema>;\n\n/**\n * The agents map as it travels the wire — the `init`/`reinit` `agents` field.\n * Narrower than `@cursor/sdk`'s `AgentDefinition` (this `mcpServers` is\n * `string[]`); the runner reconstructs the full `AgentDefinition` on receipt.\n * Daemon-side code that builds agents for the wire should use THIS type so the\n * IPC payload is type-correct by construction (no boundary cast).\n */\nexport type DaemonToRunnerAgents = NonNullable<Extract<DaemonToRunner, { kind: 'init' }>['agents']>;\n\n/**\n * RunnerToDaemon — inbound to the daemon-side adapter from the runner.\n *\n * `sdk_message.payload` is typed as `unknown` on the wire and narrowed\n * to `SDKMessage` (from `@cursor/sdk`) by the daemon-side translator\n * (`cursor-event-translator.ts`, S2b). Keeping it `unknown` at the\n * protocol boundary insulates the daemon from new SDK message variants\n * — the translator decides what to drop versus translate.\n */\nexport const RunnerToDaemonSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('init_ok'),\n sessionId: z.string(),\n tools: z.array(z.string()),\n model: z.string(),\n mcpStatus: z.array(z.object({ name: z.string(), status: z.string() })),\n }),\n z.object({\n kind: z.literal('init_error'),\n error: z.string(),\n /** SDK error class name (e.g. AuthenticationError) for daemon-side classification. */\n errorName: z.string().optional(),\n /** Stable SDK/backend error code, when available. */\n errorCode: z.string().optional(),\n statusCode: z.number().int().optional(),\n /**\n * `true` for transient failures (network, rate limit) where the\n * daemon should consider a retry; `false` for configuration errors\n * (bad API key, unknown model) where retry will not help.\n */\n retryable: z.boolean(),\n }),\n z.object({\n kind: z.literal('sdk_message'),\n generation: z.number().int().nonnegative(),\n payload: z.unknown(),\n }),\n z.object({\n kind: z.literal('run_complete'),\n generation: z.number().int().nonnegative(),\n /**\n * Cursor's `RunResult` — kept as `unknown` on the wire (see\n * `sdk_message.payload` rationale). The daemon-side adapter\n * narrows this against `RunResult` from `@cursor/sdk` at the\n * call site.\n */\n result: z.unknown(),\n /**\n * Real token usage from the `turn-ended` interaction update emitted on\n * `SendOptions.onDelta`. Optional — absent when the SDK does not surface\n * usage for this run (e.g. local composer, older SDK versions). When\n * present the daemon uses real counts instead of estimated ones.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n usage: z\n .object({\n inputTokens: z.number().int().nonnegative(),\n outputTokens: z.number().int().nonnegative(),\n cacheReadTokens: z.number().int().nonnegative(),\n cacheWriteTokens: z.number().int().nonnegative(),\n reasoningTokens: z.number().int().nonnegative().optional(),\n })\n .optional(),\n /**\n * `Run.createdAt` from `@cursor/sdk` — authoritative SDK-side run-start\n * epoch ms (run.d.ts:42). Forwarded so the daemon's per-run cost / latency\n * accounting uses the SDK clock instead of `Date.now()` at the IPC\n * boundary (which drifts by IPC + cold-build latency). Optional —\n * older SDK builds may not emit it.\n */\n createdAt: z.number().nonnegative().optional(),\n /**\n * Total tokens reported via `token-delta` InteractionUpdate during the\n * run. Complements `usage.outputTokens` — `token-delta` carries no\n * input/cache breakdown, so use this only as a fallback when `usage` is\n * absent. Optional, omitted when zero.\n */\n tokenDeltaTotal: z.number().int().nonnegative().optional(),\n }),\n z.object({\n kind: z.literal('run_error'),\n generation: z.number().int().nonnegative(),\n error: z.string(),\n /** SDK error class name (e.g. RateLimitError) for daemon-side classification. */\n errorName: z.string().optional(),\n errorCode: z.string().optional(),\n statusCode: z.number().int().optional(),\n isRetryable: z.boolean(),\n }),\n z.object({\n kind: z.literal('log'),\n level: z.enum(['info', 'warn', 'error']),\n message: z.string(),\n data: z.unknown().optional(),\n }),\n /**\n * Runner→daemon notification that the agent proposed a plan. The runner\n * sends this when the agent calls the SDK's native `createPlan` tool via\n * `onStep` in plan mode (requires `mode:'plan'` on the `init`/`reinit`\n * IPC). The subprocess (`cursor-subprocess.ts`) turns it into a\n * `plan_content_ready` SubprocessEvent, which `task-event-dispatch.ts`\n * routes to `plan-handler.ts` (writes `~/.shipyard/plans/{taskId}.md` and\n * broadcasts `plan_detected`). Supersedes the old turn-complete plan\n * accumulator that lived in `cursor-event-translator.ts`.\n */\n z.object({\n kind: z.literal('plan_proposed'),\n generation: z.number().int().nonnegative(),\n /** Markdown plan content emitted by the agent via createPlan. */\n plan: z.string(),\n }),\n /**\n * Runner→daemon notification that Cursor's server-managed conversation\n * summarization (its native \"compaction\") started / completed (B2). These\n * arrive on the `SendOptions.onDelta` interaction-update stream as\n * `SummaryStartedUpdate` / `SummaryCompletedUpdate`, NOT on the public\n * `Run.stream()` `SDKMessage` union (verified against @cursor/sdk@1.0.22 —\n * `run.d.ts` exposes only `stream()`/`conversation()`/`wait()`, and the\n * bundle routes `summary-*` updates exclusively through `onDelta`). The\n * daemon translates these into normalized `compaction_started` /\n * `compaction_completed` SubprocessEvents so the Compaction SM resets the\n * (estimated) context fill-bar after Cursor's native summarization.\n *\n * Cursor's `summary-completed` event carries no token counts, but the runner\n * has already captured the pre-compaction usage from the preceding\n * `turn-ended` interaction update (stored in `latestUsage.ref`). The runner\n * forwards that as the optional `preTokens` field so the daemon can emit a\n * real `compaction_completed.preTokens` instead of falling back to the\n * estimated `planPostCompactTurnStats` baseline.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('summary_started'),\n generation: z.number().int().nonnegative(),\n }),\n z.object({\n kind: z.literal('summary_completed'),\n generation: z.number().int().nonnegative(),\n /** Total input-equivalent tokens at the moment summarization completed (inputTokens + cacheRead + cacheWrite). Absent when no turn-ended usage was captured before summary-started. */\n preTokens: z.number().int().nonnegative().optional(),\n }),\n /**\n * Runner→daemon heartbeat emitted on every `SendOptions.onStep` callback\n * (each `ConversationStep` boundary from the SDK). The daemon resets its\n * per-run stall timer on receipt so an active but slow run does not trip\n * the watchdog. Stale-generation filtering applies (same as sdk_message).\n */\n z.object({\n kind: z.literal('step_progress'),\n generation: z.number().int().nonnegative(),\n }),\n /**\n * Runner→daemon liveness heartbeat emitted (throttled in the runner) on any\n * `SendOptions.onDelta` interaction-update — `text-delta`, `thinking-delta`,\n * `shell-output-delta`, `step-started`, `tool-call-started`, etc. Unlike\n * `step_progress` (which fires only at completed `ConversationStep`\n * boundaries via `onStep`), this fires DURING a step while output streams.\n * The daemon resets the stall watchdog WITHOUT retiring the cold-start\n * window, so a long-but-active step (a 2-min shell/test run, a slow MCP\n * tool, a big read) no longer trips the mid-run watchdog while it is plainly\n * still working. Stale-generation filtering applies (same as sdk_message).\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('stream_activity'),\n generation: z.number().int().nonnegative(),\n }),\n /**\n * Runner→daemon notification that the SDK persisted the user turn to its\n * local store (`user-message-appended` interaction update on `onDelta`).\n * Useful as a write-confirmed ack for cross-runtime handoff durability\n * checks — the daemon knows the user message landed in the SDK's SQLite\n * checkpoint store and is recoverable via `Agent.resume`. Daemon-side\n * handler logs only for now.\n *\n * Stale-generation filtering applies.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No\n * PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('user_message_persisted'),\n generation: z.number().int().nonnegative(),\n sessionId: z.string(),\n }),\n /**\n * Runner→daemon liveness edge for in-flight tool execution. Emitted only\n * on the 0→1 transition (first tool call started, phase `'started'`) and\n * the 1→0 transition (last tool call completed, phase `'settled'`). Drives\n * the daemon's `toolExecutionStallTimeoutMs` window: when a tool is\n * in-flight the stall watchdog uses the extended per-tool budget so a\n * legitimate long-running tool (e.g. `git commit` with a full `pnpm check`\n * pre-commit hook, 3–10 min) is not false-killed. On settle the watchdog\n * reverts to the shorter mid-run window. Stale-generation filtering applies.\n *\n * Daemon↔forked-child IPC only — NOT the browser wire. No PROTOCOL_VERSION bump.\n */\n z.object({\n kind: z.literal('tool_execution'),\n generation: z.number().int().nonnegative(),\n phase: z.enum(['started', 'settled']),\n }),\n /**\n * Warm-pool reinit succeeded (C9). The runner created a fresh Agent for the\n * claimed task on the warm executor. `sessionId` is the new `agent.agentId`\n * — the daemon persists it for the next resume. No generation: reinit is a\n * session-lifecycle event, not a per-turn one.\n */\n z.object({\n kind: z.literal('reinit_ok'),\n sessionId: z.string(),\n }),\n /**\n * Warm-pool reinit failed (C9). The runner could not bring up the new\n * Agent. `retryable` is informational only — the daemon's fail-closed\n * contract is to KILL this runner and spawn a fresh cold one regardless,\n * so a broken warm process can never serve the task. The runner does NOT\n * exit on its own (it has no clean state); the daemon owns the kill.\n */\n z.object({\n kind: z.literal('reinit_error'),\n error: z.string(),\n retryable: z.boolean(),\n }),\n /**\n * Pooled-close ack (C9). A pool-managed runner received `close`, disposed\n * its per-task Agent, and reset to a quiescent state WITHOUT exiting. The\n * daemon keeps the warm process (and its executor cache) for the next\n * claim. No generation — this is a session-lifecycle event.\n */\n z.object({\n kind: z.literal('pooled_idle'),\n }),\n]);\nexport type RunnerToDaemon = z.infer<typeof RunnerToDaemonSchema>;\n\n/**\n * Parse an inbound IPC message. Returns the parsed message or `null`\n * if the payload does not match the expected schema (matching the\n * watcher-worker pattern: log and continue, never throw out of the\n * IPC handler).\n */\nexport function parseDaemonToRunner(raw: unknown): DaemonToRunner | null {\n const result = DaemonToRunnerSchema.safeParse(raw);\n return result.success ? result.data : null;\n}\n\nexport function parseRunnerToDaemon(raw: unknown): RunnerToDaemon | null {\n const result = RunnerToDaemonSchema.safeParse(raw);\n return result.success ? result.data : null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -170,6 +170,15 @@ var STALE_AFTER_MS_DEFAULT = 5 * 60 * 1e3;
|
|
|
170
170
|
var cache = null;
|
|
171
171
|
var cacheGeneration = 0;
|
|
172
172
|
var backgroundRefreshInFlight = false;
|
|
173
|
+
var pendingDiskOperations = /* @__PURE__ */ new Set();
|
|
174
|
+
function trackDiskOperation(operation) {
|
|
175
|
+
const tracked = operation.catch((err) => {
|
|
176
|
+
logger.debug({ err }, "repo-paths-cache: async disk operation failed");
|
|
177
|
+
}).finally(() => {
|
|
178
|
+
pendingDiskOperations.delete(tracked);
|
|
179
|
+
});
|
|
180
|
+
pendingDiskOperations.add(tracked);
|
|
181
|
+
}
|
|
173
182
|
function cacheFilePath2() {
|
|
174
183
|
return join2(getShipyardHome(), "data", "repo-paths-cache.json");
|
|
175
184
|
}
|
|
@@ -180,7 +189,7 @@ function getCachedRepoPaths() {
|
|
|
180
189
|
function setCachedRepoPaths(repos, worktrees, now = Date.now()) {
|
|
181
190
|
cache = { repos, worktrees, populatedAt: now };
|
|
182
191
|
cacheGeneration += 1;
|
|
183
|
-
|
|
192
|
+
trackDiskOperation(persistDisk(cache, cacheGeneration));
|
|
184
193
|
}
|
|
185
194
|
async function hydrateRepoPathsCacheFromDisk(opts) {
|
|
186
195
|
if (cache !== null) return { hit: false, stale: false };
|
|
@@ -236,7 +245,7 @@ function addWorktreeToCache(worktreePath, sourceRepoPath) {
|
|
|
236
245
|
const worktrees = cache.worktrees.includes(worktreePath) ? cache.worktrees : [...cache.worktrees, worktreePath];
|
|
237
246
|
cache = { repos, worktrees, populatedAt: cache.populatedAt };
|
|
238
247
|
cacheGeneration += 1;
|
|
239
|
-
|
|
248
|
+
trackDiskOperation(persistDisk(cache, cacheGeneration));
|
|
240
249
|
}
|
|
241
250
|
function removeWorktreeFromCache(worktreePath) {
|
|
242
251
|
if (!cache) return;
|
|
@@ -244,7 +253,7 @@ function removeWorktreeFromCache(worktreePath) {
|
|
|
244
253
|
if (next.length === cache.worktrees.length) return;
|
|
245
254
|
cache = { repos: cache.repos, worktrees: next, populatedAt: cache.populatedAt };
|
|
246
255
|
cacheGeneration += 1;
|
|
247
|
-
|
|
256
|
+
trackDiskOperation(persistDisk(cache, cacheGeneration));
|
|
248
257
|
}
|
|
249
258
|
async function refreshRepoPathsInBackground(log) {
|
|
250
259
|
if (backgroundRefreshInFlight) return;
|
|
@@ -252,7 +261,7 @@ async function refreshRepoPathsInBackground(log) {
|
|
|
252
261
|
const startGen = cacheGeneration;
|
|
253
262
|
const start = Date.now();
|
|
254
263
|
try {
|
|
255
|
-
const { findGitRepos: findGitRepos2, discoverWorktrees: discoverWorktrees2 } = await import("./git-repo-
|
|
264
|
+
const { findGitRepos: findGitRepos2, discoverWorktrees: discoverWorktrees2 } = await import("./git-repo-6DHYJICW.js");
|
|
256
265
|
const { homedir: homedir2 } = await import("os");
|
|
257
266
|
const repos = await findGitRepos2(homedir2());
|
|
258
267
|
const worktrees = await discoverWorktrees2(repos);
|
|
@@ -787,4 +796,4 @@ export {
|
|
|
787
796
|
discoverWorktrees,
|
|
788
797
|
_testing
|
|
789
798
|
};
|
|
790
|
-
//# sourceMappingURL=chunk-
|
|
799
|
+
//# sourceMappingURL=chunk-D6RYU2JU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shared/capabilities/git-repo.ts","../src/shared/capabilities/environments-cache.ts","../src/shared/capabilities/repo-paths-cache.ts","../src/shared/capabilities/shell.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport type { GitRepoInfo } from '@shipyard/session';\nimport { logger } from '../logger.js';\nimport {\n type CacheEntry,\n type GitCacheFingerprint,\n getGitCacheFingerprint,\n isCacheValid,\n loadCache,\n resolveHeadPath,\n saveCache,\n} from './environments-cache.js';\nimport { gitExecSafe } from './git-pool.js';\nimport { getCachedRepoPaths, setCachedRepoPaths } from './repo-paths-cache.js';\nimport { run, runWithTimeout, TIMEOUT_MS } from './shell.js';\n\n/**\n * Per-path hard timeout for the environments walk. Lowered from\n * `TIMEOUT_MS` (5s) so a single hung path (NFS stall, symlink loop,\n * dead network drive — the May 2026 detect_environments wedge scenario)\n * can't pin the whole detector for tens of seconds. The walk continues\n * past timed-out paths; the per-path slow log surfaces the culprit on\n * the next investigation.\n */\nconst GIT_PROBE_TIMEOUT_MS = 3_000;\n\n/** Single-path duration past which we emit a slow-log event for diagnosis. */\nconst SLOW_PATH_LOG_THRESHOLD_MS = 2_000;\n\n/**\n * Race `op` against a `timeoutMs` deadline. On timeout, resolves to `null`\n * — the walker treats this the same as a git-call failure (skip the path\n * but continue the walk). Used so a single hung NFS path can't pin the\n * whole environments walk.\n */\nasync function withProbeTimeout<T>(op: Promise<T>, timeoutMs: number): Promise<T | null> {\n return new Promise<T | null>((resolve) => {\n const timer = setTimeout(() => resolve(null), timeoutMs);\n timer.unref?.();\n op.then(\n (v) => {\n clearTimeout(timer);\n resolve(v);\n },\n () => {\n clearTimeout(timer);\n resolve(null);\n }\n );\n });\n}\n\n/**\n * Negative cache TTL for `isGitRepo`. A directory that is not yet a git\n * repo may become one (`git init`) during the daemon lifetime; positives\n * are permanent (a work-tree never stops being one) but negatives must\n * expire so we re-probe periodically.\n */\nexport const NEGATIVE_CACHE_TTL_MS = 5_000;\n\ntype GitRepoCacheEntry = { isRepo: boolean; expiresAt?: number };\n\n/** Cache of directory paths to whether they are inside a git repository. */\nexport const gitRepoCache = new Map<string, GitRepoCacheEntry>();\n\nfunction readGitRepoCache(cwd: string, now = Date.now()): boolean | undefined {\n const cached = gitRepoCache.get(cwd);\n if (cached === undefined) return undefined;\n if (cached.isRepo) return true;\n if (cached.expiresAt !== undefined && now >= cached.expiresAt) {\n gitRepoCache.delete(cwd);\n return undefined;\n }\n return false;\n}\n\n/**\n * Check whether the given directory is inside a git work-tree.\n * Results are cached per directory so repeated calls (e.g. during\n * debounced diff captures) do not re-spawn git processes.\n */\nexport async function isGitRepo(cwd: string): Promise<boolean> {\n const cached = readGitRepoCache(cwd);\n if (cached !== undefined) return cached;\n try {\n await runWithTimeout('git', ['rev-parse', '--git-dir'], cwd, TIMEOUT_MS);\n gitRepoCache.set(cwd, { isRepo: true });\n return true;\n } catch {\n gitRepoCache.set(cwd, {\n isRepo: false,\n expiresAt: Date.now() + NEGATIVE_CACHE_TTL_MS,\n });\n return false;\n }\n}\n\nlet ghAvailableCache = false;\n\nexport async function isGhAvailable(): Promise<boolean> {\n if (ghAvailableCache) return true;\n try {\n await run('which', ['gh']);\n ghAvailableCache = true;\n return true;\n } catch {\n return false;\n }\n}\n\nconst topLevelCache = new Map<string, string>();\n\nexport async function getGitTopLevel(cwd: string): Promise<string> {\n const cached = topLevelCache.get(cwd);\n if (cached !== undefined) return cached;\n const topLevel = (\n await runWithTimeout('git', ['rev-parse', '--show-toplevel'], cwd, TIMEOUT_MS)\n ).trim();\n topLevelCache.set(cwd, topLevel);\n return topLevel;\n}\n\nexport function parseOwnerRepo(remoteUrl: string): { owner: string; repo: string } | null {\n const match = remoteUrl.match(/github\\.com[:/]([^/]+)\\/([^/.]+)/);\n if (!match?.[1] || !match[2]) return null;\n return { owner: match[1], repo: match[2] };\n}\n\n/**\n * Look up the repo's default branch via the `origin/HEAD` symbolic ref.\n * Returns the branch name (e.g. `main`) or null when the symbolic ref\n * isn't set (rare — happens for repos cloned without `git remote\n * set-head` or local-only repos).\n *\n * The result is cached for 5 seconds via the per-cwd OneShotPool in\n * git-pool.ts so repeated calls from serve-factory and stack-detection\n * per request coalesce to a single subprocess.\n */\nexport async function getRepoDefaultBranch(cwd: string): Promise<string | null> {\n const head = await gitExecSafe(cwd, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);\n if (!head) return null;\n return head.replace(/^origin\\//, '') || null;\n}\n\nexport async function isAncestor(\n ancestor: string,\n descendant: string,\n cwd: string\n): Promise<boolean> {\n try {\n await runWithTimeout('git', ['merge-base', '--is-ancestor', ancestor, descendant], cwd, 5_000);\n return true;\n } catch {\n return false;\n }\n}\n\nconst EXCLUDE_DIRS = new Set([\n 'node_modules',\n 'Library',\n 'Applications',\n 'Pictures',\n 'Music',\n 'Movies',\n 'go',\n '.Trash',\n]);\n\nconst MAX_DEPTH = 4;\nconst WORKTREE_DISCOVERY_CONCURRENCY = 8;\n/**\n * Lowered from 16 → 4 (#4516): 16 concurrent stat+readFile calls per walk\n * saturated the shared libuv thread pool (default 4 workers), starving the\n * main thread's I/O and causing 10–35s event-loop stalls on the fleet.\n *\n * Budget math: worst-case (all paths time out) → ceil(N/4) × GIT_PROBE_TIMEOUT_MS.\n * At 150 repos: ceil(150/4) × 3_000ms = 114s. The full-detect worker timeout is\n * CAPABILITY_DETECTOR_FULL_WORKER_TIMEOUT_MS (105s), so the absolute worst case\n * (every single path hitting NFS timeout) would exceed the budget. In practice\n * only a handful of paths ever time out (dead mounts, not all repos), so the\n * effective walk time stays well under 30s. Raising to 6 would lower worst-case\n * to 75s if needed.\n */\nconst REPO_METADATA_CONCURRENCY = 4;\n/**\n * Per-level concurrency cap for the recursive readdir fan-out in\n * `findGitRepos`. Replaces the previous unbounded `Promise.all(promises)`,\n * which on a machine with 150+ repos could create hundreds of concurrent\n * readdir callbacks all landing on the event loop within milliseconds of\n * each other, effectively blocking it for the duration.\n */\nexport const FIND_GIT_REPOS_DIR_CONCURRENCY = 8;\n\nasync function mapLimit<T, R>(\n items: readonly T[],\n limit: number,\n mapper: (item: T, index: number) => Promise<R>\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n const indexedItems = items.map((item, index) => ({ item, index }));\n let nextIndex = 0;\n const workerCount = Math.min(Math.max(1, limit), items.length);\n\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n for (;;) {\n const next = indexedItems[nextIndex];\n nextIndex += 1;\n if (!next) return;\n results[next.index] = await mapper(next.item, next.index);\n }\n })\n );\n\n return results;\n}\n\nexport async function findGitRepos(dir: string, depth = 0): Promise<string[]> {\n if (depth > MAX_DEPTH) return [];\n\n /**\n * Yield to the event loop between depth levels (#4516). Without this, a\n * single walk issues hundreds of readdir callbacks in rapid succession —\n * all queued on the JS event loop simultaneously — which can delay timer\n * callbacks (including the 100ms event-loop watchdog) for several seconds.\n */\n await new Promise<void>((r) => {\n setImmediate(r);\n });\n\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name === '.git') {\n return [dir];\n }\n }\n\n if (depth >= MAX_DEPTH) return [];\n\n const subdirs: string[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith('.')) continue;\n if (EXCLUDE_DIRS.has(entry.name)) continue;\n subdirs.push(join(dir, entry.name));\n }\n\n /**\n * Bounded fan-out replaces the previous unbounded `Promise.all(promises)`\n * (#4516). On a machine with 150+ repos at depth 4, the old approach could\n * spawn 1000s of concurrent readdir calls, all landing on the event loop\n * within milliseconds and saturating the shared libuv thread pool.\n */\n const results = await mapLimit(subdirs, FIND_GIT_REPOS_DIR_CONCURRENCY, (subdir) =>\n findGitRepos(subdir, depth + 1)\n );\n return results.flat();\n } catch {\n return [];\n }\n}\n\n/**\n * Read the current branch name by parsing `.git/HEAD` directly.\n *\n * Format of `.git/HEAD`:\n * - Checked-out branch: `ref: refs/heads/<branch>\\n`\n * - Detached HEAD: `<sha>\\n`\n *\n * First attempts to read `.git/HEAD` directly (fast path for normal repos).\n * Falls back to `resolveHeadPath` for linked worktrees where `.git` is a\n * file pointing to a gitdir rather than a directory.\n */\nasync function readBranchFromHead(repoPath: string): Promise<string> {\n const directHeadPath = join(repoPath, '.git', 'HEAD');\n try {\n const direct = await readFile(directHeadPath, 'utf8');\n const trimmed = direct.trim();\n if (trimmed.startsWith('ref: refs/heads/')) {\n return trimmed.slice('ref: refs/heads/'.length);\n }\n if (!trimmed.startsWith('gitdir:')) {\n return 'HEAD';\n }\n } catch {}\n\n try {\n const headPath = await resolveHeadPath(repoPath);\n if (!headPath) return 'HEAD';\n const contents = await readFile(headPath, 'utf8');\n const trimmed2 = contents.trim();\n if (trimmed2.startsWith('ref: refs/heads/')) {\n return trimmed2.slice('ref: refs/heads/'.length);\n }\n return 'HEAD';\n } catch {\n return 'HEAD';\n }\n}\n\nexport async function getRepoMetadata(\n repoPath: string,\n cached?: CacheEntry\n): Promise<GitRepoInfo | null> {\n try {\n if (cached) {\n return {\n path: repoPath,\n name: basename(repoPath),\n branch: cached.branch,\n ...(cached.remote !== null && { remote: cached.remote }),\n };\n }\n\n /**\n * Both reads are bounded by `GIT_PROBE_TIMEOUT_MS`. `gitExecSafe`\n * already has the 5s `OneShotPool` TTL but no hard kill — a hung\n * `git remote get-url` would pin the walker indefinitely without\n * this race. `readBranchFromHead` is plain `fs.readFile` and is\n * usually fast, but the timeout is cheap insurance against an NFS\n * stall on the HEAD file itself.\n */\n const [branchResult, remoteResult] = await Promise.all([\n withProbeTimeout(readBranchFromHead(repoPath), GIT_PROBE_TIMEOUT_MS),\n withProbeTimeout(\n gitExecSafe(repoPath, ['remote', 'get-url', 'origin']),\n GIT_PROBE_TIMEOUT_MS\n ),\n ]);\n const branch = branchResult ?? 'HEAD';\n const remote = remoteResult ? remoteResult || undefined : undefined;\n\n return {\n path: repoPath,\n name: basename(repoPath),\n branch,\n ...(remote && { remote }),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Process a single repo path during the environments walk: resolve cache hit,\n * run the timed metadata probe, update `nextCache`, and emit slow/timeout logs.\n * Extracted from the `mapLimit` callback in `detectEnvironments` to keep that\n * function under the cognitive-complexity ceiling.\n */\nasync function processRepoPaths(\n path: string,\n idx: number,\n fingerprints: GitCacheFingerprint[],\n prevCache: Map<string, CacheEntry>,\n nextCache: Map<string, CacheEntry>\n): Promise<GitRepoInfo | null> {\n const currentFingerprint = fingerprints[idx] ?? { headMtimeMs: null, configMtimeMs: null };\n const prev = prevCache.get(path);\n const cacheHit =\n prev !== undefined &&\n currentFingerprint.headMtimeMs !== null &&\n isCacheValid(prev, currentFingerprint);\n\n const start = Date.now();\n const info = await withProbeTimeout(\n getRepoMetadata(path, cacheHit ? prev : undefined),\n GIT_PROBE_TIMEOUT_MS\n );\n const durationMs = Date.now() - start;\n\n if (info === null && durationMs >= GIT_PROBE_TIMEOUT_MS * 0.9) {\n logger.warn(\n { event: 'environments_walk_path_timed_out', path, durationMs },\n 'environments walk skipped path past per-path timeout'\n );\n } else if (durationMs >= SLOW_PATH_LOG_THRESHOLD_MS) {\n logger.warn(\n { event: 'environments_walk_path_slow', path, durationMs },\n 'environments walk path resolved slowly'\n );\n }\n\n if (info !== null && currentFingerprint.headMtimeMs !== null) {\n nextCache.set(path, {\n path,\n branch: info.branch,\n remote: info.remote ?? null,\n headMtimeMs: currentFingerprint.headMtimeMs,\n configMtimeMs: currentFingerprint.configMtimeMs,\n cachedAt: cacheHit && prev ? prev.cachedAt : Date.now(),\n });\n }\n return info;\n}\n\n/**\n * Module-level single-flight for `detectEnvironments` (#4516).\n *\n * Deduplicates concurrent callers within the SAME JS context (same V8 isolate\n * — e.g. same worker thread or the main thread). When N callers call\n * `detectEnvironments()` before the current walk resolves, they all receive\n * the same promise rather than starting N independent walks.\n *\n * SCOPE: This covers same-context re-entrancy only. Full-detect workers are\n * spawned fresh per call (each isolate starts with `inFlightDetect = null`),\n * so cross-worker concurrency is prevented separately — see the backstop gate\n * on `daemon.capabilityDetectionReady` in serve.ts, which ensures the initial\n * detection worker completes before the first backstop tick fires.\n *\n * The first caller's `lastKnown` wins for the dedup window; subsequent callers\n * receive the same resolved value. `lastKnown` is a fallback for transient-\n * empty walks only, so this is safe — the walk result is authoritative.\n */\nlet inFlightDetect: Promise<GitRepoInfo[]> | null = null;\n\n/**\n * Detect git environments under $HOME.\n *\n * `lastKnown` preserves the previously-detected environment list when the\n * underlying `findGitRepos` walk returns `[]` for a non-authoritative reason\n * (root readdir failure: EMFILE, EACCES, EBUSY). Without this, a transient\n * filesystem hiccup in the 30s capability-refresh tick collapses environments\n * to `[]`, which `applyFreshCapabilities` writes wholesale, and the browser\n * briefly renders \"No environment\" until the next successful tick. Mirror of\n * the `lastKnownAgents` pattern in `agents.ts`.\n *\n * Discrimination: a successful walk that genuinely finds zero repos is\n * indistinguishable from a swallowed-error walk in the current shape (the\n * inner `try/catch` in `findGitRepos` returns `[]` on readdir failure). The\n * `lastKnown.length > 0` guard means we only preserve when there's something\n * worth preserving; a clean machine that genuinely has zero repos still gets\n * `[]`.\n */\nexport function detectEnvironments(lastKnown?: GitRepoInfo[]): Promise<GitRepoInfo[]> {\n if (inFlightDetect) return inFlightDetect;\n const promise = runDetectEnvironments(lastKnown);\n inFlightDetect = promise;\n /**\n * Swallow the rejection on the stored ref so the module-level holder never\n * becomes a tracked-but-unhandled rejected promise. Callers receive the real\n * rejection via the `promise` reference returned to them.\n */\n promise\n .catch(() => {})\n .finally(() => {\n if (inFlightDetect === promise) inFlightDetect = null;\n });\n return promise;\n}\n\nasync function runDetectEnvironments(lastKnown?: GitRepoInfo[]): Promise<GitRepoInfo[]> {\n const { homedir } = await import('node:os');\n\n /**\n * The repo-paths cache (`repo-paths-cache.ts`) holds the result of the\n * `findGitRepos(homedir())` walk and the N parallel `git worktree list`\n * spawns — together ~12-15s on a busy machine. Per-repo branch info is\n * still resolved fresh below via `getRepoMetadata` (cached separately by\n * git HEAD + config mtimes in `environments-cache.ts`). The list cache lives\n * for the daemon lifetime; `invalidateRepoPaths()` is called by\n * worktree-service (add/remove), capability-watcher (FSEvents rescan),\n * and explicit force-refresh paths.\n */\n const cachedPaths = getCachedRepoPaths();\n let repoPaths: string[];\n let worktreePaths: string[];\n\n if (cachedPaths) {\n repoPaths = cachedPaths.repos;\n worktreePaths = cachedPaths.worktrees;\n } else {\n try {\n repoPaths = await findGitRepos(homedir());\n } catch (err) {\n if (lastKnown && lastKnown.length > 0) {\n const { logger } = await import('../logger.js');\n logger.warn(\n { err, lastKnownCount: lastKnown.length },\n 'detectEnvironments findGitRepos threw — preserving lastKnown'\n );\n return lastKnown;\n }\n return [];\n }\n\n if (repoPaths.length === 0 && lastKnown && lastKnown.length > 0) {\n /**\n * Walk returned empty but we know there used to be repos — almost\n * certainly a transient root-readdir error swallowed inside\n * `findGitRepos`. Preserve rather than wipe.\n */\n const { logger } = await import('../logger.js');\n logger.warn(\n { lastKnownCount: lastKnown.length },\n 'detectEnvironments walk empty but lastKnown non-empty — preserving lastKnown'\n );\n return lastKnown;\n }\n\n worktreePaths = await discoverWorktrees(repoPaths);\n setCachedRepoPaths(repoPaths, worktreePaths);\n }\n\n const allPaths = [...new Set([...repoPaths, ...worktreePaths])];\n\n const prevCache = await loadCache();\n const fingerprints = await mapLimit(allPaths, REPO_METADATA_CONCURRENCY, getGitCacheFingerprint);\n const nextCache = new Map<string, CacheEntry>();\n\n /**\n * Per-path timing + hard timeout. Walk continues past timed-out paths so\n * one bad mount (NFS, dead symlink, removed volume) can't pin the entire\n * environments slice. Slow paths log a structured event for diagnosis.\n */\n const resolved = await mapLimit(allPaths, REPO_METADATA_CONCURRENCY, (path, idx) =>\n processRepoPaths(path, idx, fingerprints, prevCache, nextCache)\n );\n\n await saveCache(nextCache);\n\n return resolved.filter((info): info is GitRepoInfo => info !== null);\n}\n\n/**\n * For each repo, run `git worktree list --porcelain` to discover\n * linked worktrees (which may live in dot-directories like .claude/worktrees/).\n *\n * Dedupes across repos: linked worktrees that are themselves git repos get\n * walked AGAIN by `findGitRepos`, and running `git worktree list` from each\n * one reports the same set of paths back. Without dedupe the cache grew to\n * 7,420 entries on a machine with only 149 unique worktrees (2026-05-14\n * boot wedge investigation). `dedupePerRepoWorktreeOutputs` collapses to\n * unique paths while preserving the first-seen ordering for determinism.\n */\nexport async function discoverWorktrees(repoPaths: string[]): Promise<string[]> {\n const results = await mapLimit(\n repoPaths,\n WORKTREE_DISCOVERY_CONCURRENCY,\n async (repoPath): Promise<PromiseSettledResult<string[]>> => {\n try {\n return { status: 'fulfilled', value: await listLinkedWorktrees(repoPath) };\n } catch (reason) {\n return { status: 'rejected', reason };\n }\n }\n );\n return dedupePerRepoWorktreeOutputs(results);\n}\n\nfunction dedupePerRepoWorktreeOutputs(results: PromiseSettledResult<string[]>[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const r of results) {\n if (r.status !== 'fulfilled') continue;\n for (const wt of r.value) {\n if (seen.has(wt)) continue;\n seen.add(wt);\n out.push(wt);\n }\n }\n return out;\n}\n\n/**\n * Enumerate linked worktrees for a git repository by reading the\n * `.git/worktrees/` directory directly rather than spawning a git subprocess.\n *\n * Git's on-disk format (gitrepository-layout(5), stable since git 2.5):\n * .git/worktrees/<name>/gitdir — contains the path to the worktree's\n * `.git` file, e.g. `/path/to/wt/.git`\n *\n * Stripping `/.git` from that content yields the worktree path. Missing\n * `.git/worktrees/` directory means no linked worktrees — return [].\n * Orphaned entries (gitdir file missing/unreadable) are silently skipped.\n */\nasync function listLinkedWorktrees(repoPath: string): Promise<string[]> {\n const gitWorktreesDir = join(repoPath, '.git', 'worktrees');\n let entries: string[];\n try {\n const result = await withProbeTimeout(readdir(gitWorktreesDir), GIT_PROBE_TIMEOUT_MS);\n if (result === null) return [];\n entries = result;\n } catch {\n return [];\n }\n\n /**\n * Bounded fan-out over gitdir files (#4516). A repo with many linked\n * worktrees would otherwise fan out N concurrent readFile calls, the same\n * class of issue as the unbounded `findGitRepos` fan-out. Each per-name op\n * is also timeout-guarded to match the `processRepoPaths` pattern.\n */\n const paths = await mapLimit(entries, FIND_GIT_REPOS_DIR_CONCURRENCY, async (name) => {\n const result = await withProbeTimeout(\n (async () => {\n const gitdirFile = join(gitWorktreesDir, name, 'gitdir');\n const content = await readFile(gitdirFile, 'utf8');\n const wtGitPath = content.trim();\n return wtGitPath.endsWith('/.git') ? wtGitPath.slice(0, -'/.git'.length) : null;\n })(),\n GIT_PROBE_TIMEOUT_MS\n );\n return result ?? null;\n });\n\n return paths.filter((p): p is string => p !== null && p !== repoPath);\n}\n\n/** @internal kept for tests that exercise parse logic on porcelain output */\nfunction parseWorktreeListOutput(output: string, repoPath: string): string[] {\n const paths: string[] = [];\n for (const line of output.split('\\n')) {\n if (line.startsWith('worktree ')) {\n const wt = line.slice('worktree '.length).trim();\n if (wt && wt !== repoPath) paths.push(wt);\n }\n }\n return paths;\n}\n\nexport const _testing = {\n parseWorktreeListOutput,\n dedupePerRepoWorktreeOutputs,\n listLinkedWorktrees,\n readBranchFromHead,\n mapLimit,\n withProbeTimeout,\n GIT_PROBE_TIMEOUT_MS,\n SLOW_PATH_LOG_THRESHOLD_MS,\n NEGATIVE_CACHE_TTL_MS,\n FIND_GIT_REPOS_DIR_CONCURRENCY,\n readGitRepoCache,\n resetGitRepoCache: () => {\n gitRepoCache.clear();\n },\n resetGhAvailableCache: () => {\n ghAvailableCache = false;\n },\n resetInFlightDetect: () => {\n inFlightDetect = null;\n },\n};\n","import { mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { z } from 'zod';\nimport { getShipyardHome } from '../env.js';\nimport { isEnoent } from '../fs-utils.js';\nimport { logger } from '../logger.js';\n\nconst CacheEntrySchema = z.object({\n path: z.string(),\n branch: z.string(),\n remote: z.string().nullable(),\n headMtimeMs: z.number(),\n /**\n * Added after the original cache shape. Optional so old disk caches parse,\n * but `isCacheValid` treats missing as invalid and refreshes once.\n */\n configMtimeMs: z.number().nullable().optional(),\n cachedAt: z.number(),\n});\n\nconst CacheFileSchema = z.object({\n entries: z.record(z.string(), CacheEntrySchema),\n});\n\nexport type CacheEntry = z.infer<typeof CacheEntrySchema>;\ntype CacheFile = z.infer<typeof CacheFileSchema>;\nexport interface GitCacheFingerprint {\n headMtimeMs: number | null;\n configMtimeMs: number | null;\n}\n\nfunction cacheFilePath(): string {\n return join(getShipyardHome(), 'data', 'environments-cache.json');\n}\n\nlet cacheMigrationDone = false;\n\nexport async function loadCache(): Promise<Map<string, CacheEntry>> {\n const filePath = cacheFilePath();\n try {\n const raw = await readFile(filePath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n const result = CacheFileSchema.safeParse(parsed);\n if (!result.success) {\n logger.debug({ path: filePath }, 'environments-cache: malformed, treating as empty');\n cacheMigrationDone = true;\n return new Map();\n }\n const cache = new Map(Object.entries(result.data.entries));\n if (!cacheMigrationDone) {\n cacheMigrationDone = true;\n await migrateConfigMtimes(cache);\n }\n return cache;\n } catch (err) {\n if (isEnoent(err)) {\n cacheMigrationDone = true;\n return new Map();\n }\n logger.debug({ err }, 'environments-cache: read failed, treating as empty');\n return new Map();\n }\n}\n\n/**\n * One-shot migration: entries written before #3619 lack `configMtimeMs`.\n * `isCacheValid` would force a re-spawn for every such entry, creating a\n * git subprocess storm on first boot after upgrade. Backfill the field by\n * reading `.git/config` mtimes directly, then persist so subsequent boots\n * hit the cache normally.\n */\nasync function migrateConfigMtimes(cache: Map<string, CacheEntry>): Promise<void> {\n const stale = [...cache.values()].filter((e) => e.configMtimeMs === undefined);\n if (stale.length === 0) return;\n await Promise.all(\n stale.map(async (entry) => {\n const fp = await getGitCacheFingerprint(entry.path);\n cache.set(entry.path, { ...entry, configMtimeMs: fp.configMtimeMs });\n })\n );\n logger.info({ count: stale.length }, 'environments-cache: cache_migration_seeded');\n await saveCache(cache);\n}\n\n/**\n * Resolve git metadata paths for a repository or linked worktree.\n *\n * - If `<path>/.git` is a directory, metadata lives inside that directory.\n * - If `<path>/.git` is a file, it contains `gitdir: <path>`. HEAD lives in\n * that worktree gitdir, while remotes live in the common git dir's config.\n * - Returns null on any error (missing, unreadable, malformed).\n */\nasync function readGitdirFile(dotGitPath: string): Promise<string | null> {\n const contents = await readFile(dotGitPath, 'utf8');\n const match = contents.match(/^gitdir:\\s*(.+)\\s*$/m);\n if (!match?.[1]) return null;\n const gitdirRaw = match[1].trim();\n return isAbsolute(gitdirRaw) ? gitdirRaw : resolve(dirname(dotGitPath), gitdirRaw);\n}\n\nasync function resolveCommonGitDir(gitdir: string): Promise<string> {\n try {\n const raw = await readFile(join(gitdir, 'commondir'), 'utf8');\n const commonDirRaw = raw.trim();\n if (commonDirRaw === '') return gitdir;\n return isAbsolute(commonDirRaw) ? commonDirRaw : resolve(gitdir, commonDirRaw);\n } catch {\n return gitdir;\n }\n}\n\nexport async function resolveHeadPath(worktreePath: string): Promise<string | null> {\n const paths = await resolveGitMetadataPaths(worktreePath);\n return paths?.headPath ?? null;\n}\n\nexport async function resolveGitMetadataPaths(\n worktreePath: string\n): Promise<{ headPath: string; configPath: string | null } | null> {\n const dotGitPath = join(worktreePath, '.git');\n try {\n const dotGitStat = await stat(dotGitPath);\n if (dotGitStat.isDirectory()) {\n return { headPath: join(dotGitPath, 'HEAD'), configPath: join(dotGitPath, 'config') };\n }\n if (dotGitStat.isFile()) {\n const gitdir = await readGitdirFile(dotGitPath);\n if (!gitdir) return null;\n const commonGitDir = await resolveCommonGitDir(gitdir);\n return { headPath: join(gitdir, 'HEAD'), configPath: join(commonGitDir, 'config') };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nasync function getPathMtime(path: string | null): Promise<number | null> {\n if (!path) return null;\n try {\n const pathStat = await stat(path);\n return pathStat.mtimeMs;\n } catch {\n return null;\n }\n}\n\nexport async function getGitCacheFingerprint(worktreePath: string): Promise<GitCacheFingerprint> {\n const paths = await resolveGitMetadataPaths(worktreePath);\n if (!paths) return { headMtimeMs: null, configMtimeMs: null };\n const [headMtimeMs, configMtimeMs] = await Promise.all([\n getPathMtime(paths.headPath),\n getPathMtime(paths.configPath),\n ]);\n return { headMtimeMs, configMtimeMs };\n}\n\nexport function isCacheValid(entry: CacheEntry, current: GitCacheFingerprint): boolean {\n /**\n * `.git/HEAD` invalidates branch, and `.git/config` invalidates remote.\n * Entries without `configMtimeMs` are soft-migrated at `loadCache` time\n * (see `migrateConfigMtimes`), so by the time this is called the field\n * is always present. Treat `undefined` the same as `null` for safety.\n */\n return (\n entry.headMtimeMs === current.headMtimeMs &&\n (entry.configMtimeMs ?? null) === current.configMtimeMs\n );\n}\n\nexport async function saveCache(entries: Map<string, CacheEntry>): Promise<void> {\n const filePath = cacheFilePath();\n const tmpPath = `${filePath}.tmp`;\n try {\n await mkdir(dirname(filePath), { recursive: true });\n /**\n * Merge-on-save: re-read disk immediately before writing so a concurrent\n * saveCache call's entries are preserved rather than erased (new entries\n * win for paths present in both — they are fresher).\n */\n const current = await loadCache();\n for (const [path, entry] of entries) {\n current.set(path, entry);\n }\n const payload: CacheFile = { entries: Object.fromEntries(current) };\n await writeFile(tmpPath, JSON.stringify(payload), 'utf8');\n await rename(tmpPath, filePath);\n } catch (err) {\n logger.debug({ err, filePath }, 'environments-cache: save failed');\n void unlink(tmpPath).catch(() => {});\n }\n}\n","/**\n * In-memory cache of the list of git repo paths under $HOME and the linked\n * worktree paths discovered via `git worktree list --porcelain` for each.\n *\n * Distinct from `environments-cache.ts` (which caches per-repo branch/remote\n * info on disk, keyed by git HEAD + config mtimes). This one caches the LIST of repos and\n * worktrees — i.e. the `findGitRepos(homedir())` walk and the N parallel\n * `git worktree list` spawns. Those are the ~12-50s cost on a busy machine.\n *\n * Lifetime: persisted to disk at `~/.shipyard/data/repo-paths-cache.json`\n * and rehydrated on every daemon boot. The previous implementation rejected\n * the disk cache outright when its age exceeded 1h — which is almost every\n * reboot in practice — and forced the 30s `findGitRepos($HOME)` walk to fire\n * on each cold boot. We now serve any structurally-valid cache regardless of\n * age and kick off a background re-walk when the cache is older than 5\n * minutes. Boot time drops from 30-50s to milliseconds; freshness is\n * restored ~10-30s after boot when the background walk completes.\n *\n * Invalidation:\n * - `invalidateRepoPaths()` — called by:\n * * capability-watcher.ts (FSEvents rescan + escalation paths)\n * * explicit force-refresh control message\n * - `addWorktreeToCache()` / `removeWorktreeFromCache()` — incremental\n * update used by worktree create/remove. Avoids the cold-walk recompute\n * storm that otherwise stalls the event loop 12-15s and kills the\n * daemon's WebRTC + control channels mid-creation. If there is no\n * active cache (boot before first walk), the call is a no-op — the\n * next cold walk picks up the new state.\n *\n * The walk + worktree spawns ARE NOT triggered by the capability-watcher's\n * normal events: scoped re-detect (`refreshMcpServers` etc.) skips\n * `detectEnvironments` entirely and reuses the existing `daemon.capabilities\n * ?.environments`. The cache is only consulted by full `detectCapabilities`\n * runs (manual Refresh button, boot, cwd-changed).\n */\n\nimport { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { z } from 'zod';\nimport { getShipyardHome } from '../env.js';\nimport { isEnoent } from '../fs-utils.js';\nimport type { createChildLogger } from '../logger.js';\nimport { logger } from '../logger.js';\n\nexport type { HydrateResult };\nexport {\n addWorktreeToCache,\n getCachedRepoPaths,\n hydrateAndLogRepoPathsCache,\n hydrateRepoPathsCacheFromDisk,\n invalidateRepoPaths,\n removeWorktreeFromCache,\n setCachedRepoPaths,\n};\n\ninterface CachedEntry {\n repos: string[];\n worktrees: string[];\n populatedAt: number;\n}\n\ninterface HydrateResult {\n hit: boolean;\n stale: boolean;\n}\n\nconst RepoPathsDiskSchema = z.object({\n repos: z.array(z.string()),\n worktrees: z.array(z.string()),\n populatedAt: z.number(),\n});\n\nconst STALE_AFTER_MS_DEFAULT = 5 * 60 * 1000;\n\nlet cache: CachedEntry | null = null;\n/**\n * Monotonic counter incremented on every mutating call. Async disk operations\n * capture the generation at the moment they were scheduled and no-op when\n * the generation has changed by the time they get to the syscall. Without\n * this, `invalidateRepoPaths()` (which fires `void unlinkDisk()`) racing\n * with a subsequent `setCachedRepoPaths()` (which fires `void persistDisk()`)\n * could leave the disk in either state — and if `unlinkDisk` wins after\n * `persistDisk`'s rename succeeds, the next daemon restart pays the full\n * cold-walk cost despite a valid cache having just been written.\n */\nlet cacheGeneration = 0;\nlet backgroundRefreshInFlight = false;\n\nfunction cacheFilePath(): string {\n return join(getShipyardHome(), 'data', 'repo-paths-cache.json');\n}\n\nfunction getCachedRepoPaths(): { repos: string[]; worktrees: string[] } | null {\n if (!cache) return null;\n return { repos: cache.repos, worktrees: cache.worktrees };\n}\n\nfunction setCachedRepoPaths(repos: string[], worktrees: string[], now: number = Date.now()): void {\n cache = { repos, worktrees, populatedAt: now };\n cacheGeneration += 1;\n void persistDisk(cache, cacheGeneration);\n}\n\nfunction invalidateRepoPaths(): void {\n cache = null;\n cacheGeneration += 1;\n void unlinkDisk(cacheGeneration);\n}\n\n/**\n * Best-effort hydrate the in-memory cache from disk on daemon boot. Always\n * accepts a structurally-valid on-disk entry regardless of age — the boot\n * path needs the data immediately and any staleness is repaired by a\n * background re-walk scheduled by `hydrateAndLogRepoPathsCache`. Returns\n * `{ hit, stale }` so the caller knows whether to schedule that refresh.\n *\n * The `staleAfterMs` threshold defines when the cache is \"old enough to\n * re-walk in the background\" — not when it's \"too old to serve\". Default\n * 5 minutes; data older than that triggers a background refresh but is\n * still returned to the boot path so `detect_environments` sees a populated\n * cache and skips the 30s `findGitRepos` walk + `git worktree list` storm.\n *\n * Wired from `serve.ts` BEFORE `createSignalingHandle()` so the first\n * `detectEnvironments` call inside the background `runCapabilityDetection`\n * (#3360 — detection moved off the boot path) sees the populated in-memory\n * cache. Without this, every daemon restart pays the full cold-walk cost —\n * observed at 30-50s on busy machines and the dominant component of 42.5s\n * worst-case boot.\n */\nasync function hydrateRepoPathsCacheFromDisk(opts?: {\n staleAfterMs?: number;\n}): Promise<HydrateResult> {\n if (cache !== null) return { hit: false, stale: false };\n const filePath = cacheFilePath();\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf8');\n } catch (err: unknown) {\n if (isEnoent(err)) return { hit: false, stale: false };\n logger.debug({ err, filePath }, 'repo-paths-cache: read failed');\n return { hit: false, stale: false };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { hit: false, stale: false };\n }\n const result = RepoPathsDiskSchema.safeParse(parsed);\n if (!result.success) return { hit: false, stale: false };\n\n /**\n * Race-safe: another caller may have populated the slot between our\n * `cache !== null` check and the disk read. Don't clobber the live entry.\n */\n if (cache !== null) return { hit: false, stale: false };\n\n const staleAfterMs = opts?.staleAfterMs ?? STALE_AFTER_MS_DEFAULT;\n const ageMs = Math.max(0, Date.now() - result.data.populatedAt);\n const stale = ageMs > staleAfterMs;\n\n cache = result.data;\n logger.info(\n { repos: result.data.repos.length, worktrees: result.data.worktrees.length, ageMs, stale },\n 'repo-paths-cache: hydrated from disk'\n );\n return { hit: true, stale };\n}\n\nasync function persistDisk(entry: CachedEntry, generation: number): Promise<void> {\n const filePath = cacheFilePath();\n const tmpPath = `${filePath}.tmp`;\n try {\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(tmpPath, JSON.stringify(entry), 'utf8');\n /**\n * Drop the write if a later mutation (set/invalidate/add/remove) raced\n * us. The successor's persist/unlink will reach the rename target with\n * the right state. Without this, an in-flight rename can clobber the\n * successor's just-written file or, worse, race a fresh persist after\n * an invalidate.\n */\n if (generation !== cacheGeneration) {\n await unlink(tmpPath).catch(() => {});\n return;\n }\n await rename(tmpPath, filePath);\n } catch (err: unknown) {\n logger.debug({ err, filePath }, 'repo-paths-cache: save failed');\n void unlink(tmpPath).catch(() => {});\n }\n}\n\nasync function unlinkDisk(generation: number): Promise<void> {\n /**\n * Drop the unlink if a later `setCachedRepoPaths` already raced us — its\n * `persistDisk` will write the fresh file and the file the unlink would\n * have removed is the predecessor's. Without this generation check, an\n * `invalidate → set` sequence can end up with NO disk file even though\n * `set` persisted one.\n */\n if (generation !== cacheGeneration) return;\n try {\n await unlink(cacheFilePath());\n } catch {\n /** No-op — file may not exist yet. */\n }\n}\n\n/**\n * Append a freshly-created worktree to the existing cached result. Source\n * repo path is also added (deduped) because a brand-new repo can host a\n * brand-new worktree, and the next `detectEnvironments` call needs to see\n * both. No-op when there is no cache yet — the next cold walk will discover\n * the new entries naturally.\n */\nfunction addWorktreeToCache(worktreePath: string, sourceRepoPath: string): void {\n if (!cache) return;\n const repos = cache.repos.includes(sourceRepoPath)\n ? cache.repos\n : [...cache.repos, sourceRepoPath];\n const worktrees = cache.worktrees.includes(worktreePath)\n ? cache.worktrees\n : [...cache.worktrees, worktreePath];\n cache = { repos, worktrees, populatedAt: cache.populatedAt };\n cacheGeneration += 1;\n void persistDisk(cache, cacheGeneration);\n}\n\n/**\n * Splice a worktree path out of the cached worktree list. The source repo\n * stays — removing a single linked worktree never deletes its source. No-op\n * when there is no cache yet.\n */\nfunction removeWorktreeFromCache(worktreePath: string): void {\n if (!cache) return;\n const next = cache.worktrees.filter((p) => p !== worktreePath);\n if (next.length === cache.worktrees.length) return;\n cache = { repos: cache.repos, worktrees: next, populatedAt: cache.populatedAt };\n cacheGeneration += 1;\n void persistDisk(cache, cacheGeneration);\n}\n\n/**\n * Re-walk `$HOME` and rewrite the cache. Fired by `hydrateAndLogRepoPathsCache`\n * on boot when the hydrated cache is stale, restoring freshness ~10-30s after\n * the user is already interactive. Single-flight: a second call while one is\n * in progress is a no-op. Generation-guarded: if any synchronous mutation\n * (`setCachedRepoPaths`, `addWorktreeToCache`, `removeWorktreeFromCache`,\n * `invalidateRepoPaths`) lands while the walk is in flight, the refresh\n * result is dropped so it does not clobber a more-recent incremental update.\n *\n * Lazy-imports `./git-repo.js` to break the import cycle (`git-repo` already\n * imports this module).\n */\nasync function refreshRepoPathsInBackground(\n log: ReturnType<typeof createChildLogger>\n): Promise<void> {\n if (backgroundRefreshInFlight) return;\n backgroundRefreshInFlight = true;\n const startGen = cacheGeneration;\n const start = Date.now();\n try {\n const { findGitRepos, discoverWorktrees } = await import('./git-repo.js');\n const { homedir } = await import('node:os');\n const repos = await findGitRepos(homedir());\n const worktrees = await discoverWorktrees(repos);\n if (cacheGeneration !== startGen) {\n log.debug(\n { event: 'repo_paths_cache_refresh_dropped' },\n 'repo-paths-cache: background refresh dropped (cache mutated mid-walk)'\n );\n return;\n }\n /**\n * Defense in depth: a transient root-readdir failure inside\n * `findGitRepos` returns `[]`, indistinguishable from \"no repos exist\".\n * If the prior cache had entries, preserve it rather than wipe — the\n * next refresh (capability-watcher tick) will retry. Mirrors the same\n * guard inside `detectEnvironments`.\n */\n if (repos.length === 0 && cache && cache.repos.length > 0) {\n log.warn(\n {\n event: 'repo_paths_cache_refresh_empty_preserved',\n prevRepos: cache.repos.length,\n },\n 'repo-paths-cache: background refresh returned empty; preserving prior cache'\n );\n return;\n }\n setCachedRepoPaths(repos, worktrees);\n log.info(\n {\n event: 'repo_paths_cache_refresh_complete',\n repos: repos.length,\n worktrees: worktrees.length,\n durationMs: Date.now() - start,\n },\n 'repo-paths-cache: background refresh complete'\n );\n } catch (err) {\n log.warn(\n { err, event: 'repo_paths_cache_refresh_failed', durationMs: Date.now() - start },\n 'repo-paths-cache: background refresh failed'\n );\n } finally {\n backgroundRefreshInFlight = false;\n }\n}\n\n/**\n * Hydrate from disk and log the hit/miss outcome. Boot callers wrap this in\n * `bootstrapPhase('repo_paths_cache_hydrate', ...)` — the wrapper always\n * records `outcome: 'success'` because hydrate returns a result object (no\n * throw on ENOENT). The explicit log here is what surfaces whether the\n * upcoming `detect_environments` will pay the cold walk and whether a\n * background refresh was scheduled.\n */\nasync function hydrateAndLogRepoPathsCache(\n log: ReturnType<typeof createChildLogger>\n): Promise<boolean> {\n const result = await hydrateRepoPathsCacheFromDisk();\n log.info(\n {\n event: 'repo_paths_cache_hydrate_result',\n hit: result.hit,\n stale: result.stale,\n },\n result.hit\n ? result.stale\n ? 'repo-paths cache hydrated from disk (stale; background refresh scheduled)'\n : 'repo-paths cache hydrated from disk (fresh)'\n : 'repo-paths cache cold (walk will fire inside detect_environments)'\n );\n if (result.hit && result.stale) {\n void refreshRepoPathsInBackground(log);\n }\n return result.hit;\n}\n\nexport const _testing = {\n reset: invalidateRepoPaths,\n inspect: () => cache,\n refreshRepoPathsInBackground,\n cacheFilePath,\n isBackgroundRefreshInFlight: () => backgroundRefreshInFlight,\n};\n","import { execFile } from 'node:child_process';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const TIMEOUT_MS = 5_000;\n/**\n * Cap on `claude auth status --json` during capability detection. Healthy\n * CLI responds in well under 1s; 15s was a worst-case-CLI-cold-start\n * accommodation that singlehandedly added up to 15s of boot stall when the\n * subprocess hung. 5s is generous slack while keeping the bootstrap\n * `detectCapabilities → signaling register` path bounded. See issue #3267.\n */\nexport const AUTH_STATUS_TIMEOUT_MS = 5_000;\n\n/**\n * Build the version-manager and user-local binary dirs for macOS.\n * These paths are absent when the daemon is launched from Finder/Spotlight\n * because macOS GUI apps inherit a stripped PATH that skips the user's\n * shell profile (where nvm/volta/mise/asdf entries normally live).\n *\n * Listed from most commonly needed (nvm, volta) to least common (cargo).\n * All are idempotently filtered by augmentPath — already-present entries\n * are never duplicated.\n */\nfunction buildDarwinVersionManagerPaths(home: string): string[] {\n return [\n join(home, '.nvm', 'current', 'bin'),\n join(home, '.volta', 'bin'),\n join(home, '.local', 'bin'),\n join(home, '.cargo', 'bin'),\n '/opt/homebrew/opt/node/bin',\n ];\n}\n\nexport const STANDARD_PATHS_BY_PLATFORM: Partial<Record<NodeJS.Platform, readonly string[]>> = {\n darwin: [\n '/usr/bin',\n '/usr/local/bin',\n '/opt/homebrew/bin',\n ...buildDarwinVersionManagerPaths(homedir()),\n ],\n linux: ['/usr/bin', '/usr/local/bin'],\n};\n\n/**\n * Daemons launched from Finder/Spotlight on macOS get a stripped PATH that may\n * omit /usr/bin where git lives. Prepend standard locations idempotently so\n * `execFile('git', ...)` resolves the binary. User-customized PATH entries\n * stay preferred — we only prepend dirs that aren't already present.\n */\nexport function augmentPath(currentPath: string, platform: NodeJS.Platform): string {\n const standardPaths = STANDARD_PATHS_BY_PLATFORM[platform];\n if (!standardPaths) return currentPath;\n\n const existing = currentPath.length > 0 ? currentPath.split(':') : [];\n const existingSet = new Set(existing);\n const toPrepend = standardPaths.filter((p) => !existingSet.has(p));\n if (toPrepend.length === 0) return currentPath;\n return existing.length > 0 ? `${toPrepend.join(':')}:${currentPath}` : toPrepend.join(':');\n}\n\nconst augmentedPath = augmentPath(process.env.PATH ?? '', process.platform);\nif (augmentedPath.length > 0) {\n process.env.PATH = augmentedPath;\n}\n\nexport function run(\n command: string,\n args: string[],\n cwd?: string,\n timeoutMs: number = TIMEOUT_MS\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(command, args, { timeout: timeoutMs, cwd }, (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout.trim());\n });\n });\n}\n\nexport function runWithTimeout(\n command: string,\n args: string[],\n cwd: string,\n timeoutMs: number\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n command,\n args,\n { timeout: timeoutMs, cwd, maxBuffer: 10 * 1024 * 1024 },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout.trim());\n }\n );\n });\n}\n\nexport const DIFF_TIMEOUT_MS = 15_000;\nexport const MAX_DIFF_SIZE = 500_000;\n\nexport function isMaxBufferError(err: unknown): boolean {\n return err instanceof Error && err.message.includes('maxBuffer');\n}\n\nexport const BUFFER_OVERFLOW_MSG = '... diff too large (exceeded buffer limit) ...\\n';\n\nexport function truncateDiff(result: string): string {\n if (result.length <= MAX_DIFF_SIZE) return result;\n const boundary = result.lastIndexOf('\\ndiff --git ', MAX_DIFF_SIZE);\n const cutoff = boundary > 0 ? boundary : MAX_DIFF_SIZE;\n return `${result.slice(0, cutoff)}\\n\\n... diff truncated (exceeds 500KB) ...\\n`;\n}\n"],"mappings":"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","names":["readFile","join","cache","mkdir","readFile","rename","unlink","writeFile","dirname","join","cacheFilePath","join","cacheFilePath","readFile","mkdir","dirname","writeFile","unlink","rename","findGitRepos","discoverWorktrees","homedir","join","resolve","resolve","join","readFile","homedir","logger"]}
|
|
1
|
+
{"version":3,"sources":["../src/shared/capabilities/git-repo.ts","../src/shared/capabilities/environments-cache.ts","../src/shared/capabilities/repo-paths-cache.ts","../src/shared/capabilities/shell.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport type { GitRepoInfo } from '@shipyard/session';\nimport { logger } from '../logger.js';\nimport {\n type CacheEntry,\n type GitCacheFingerprint,\n getGitCacheFingerprint,\n isCacheValid,\n loadCache,\n resolveHeadPath,\n saveCache,\n} from './environments-cache.js';\nimport { gitExecSafe } from './git-pool.js';\nimport { getCachedRepoPaths, setCachedRepoPaths } from './repo-paths-cache.js';\nimport { run, runWithTimeout, TIMEOUT_MS } from './shell.js';\n\n/**\n * Per-path hard timeout for the environments walk. Lowered from\n * `TIMEOUT_MS` (5s) so a single hung path (NFS stall, symlink loop,\n * dead network drive — the May 2026 detect_environments wedge scenario)\n * can't pin the whole detector for tens of seconds. The walk continues\n * past timed-out paths; the per-path slow log surfaces the culprit on\n * the next investigation.\n */\nconst GIT_PROBE_TIMEOUT_MS = 3_000;\n\n/** Single-path duration past which we emit a slow-log event for diagnosis. */\nconst SLOW_PATH_LOG_THRESHOLD_MS = 2_000;\n\n/**\n * Race `op` against a `timeoutMs` deadline. On timeout, resolves to `null`\n * — the walker treats this the same as a git-call failure (skip the path\n * but continue the walk). Used so a single hung NFS path can't pin the\n * whole environments walk.\n */\nasync function withProbeTimeout<T>(op: Promise<T>, timeoutMs: number): Promise<T | null> {\n return new Promise<T | null>((resolve) => {\n const timer = setTimeout(() => resolve(null), timeoutMs);\n timer.unref?.();\n op.then(\n (v) => {\n clearTimeout(timer);\n resolve(v);\n },\n () => {\n clearTimeout(timer);\n resolve(null);\n }\n );\n });\n}\n\n/**\n * Negative cache TTL for `isGitRepo`. A directory that is not yet a git\n * repo may become one (`git init`) during the daemon lifetime; positives\n * are permanent (a work-tree never stops being one) but negatives must\n * expire so we re-probe periodically.\n */\nexport const NEGATIVE_CACHE_TTL_MS = 5_000;\n\ntype GitRepoCacheEntry = { isRepo: boolean; expiresAt?: number };\n\n/** Cache of directory paths to whether they are inside a git repository. */\nexport const gitRepoCache = new Map<string, GitRepoCacheEntry>();\n\nfunction readGitRepoCache(cwd: string, now = Date.now()): boolean | undefined {\n const cached = gitRepoCache.get(cwd);\n if (cached === undefined) return undefined;\n if (cached.isRepo) return true;\n if (cached.expiresAt !== undefined && now >= cached.expiresAt) {\n gitRepoCache.delete(cwd);\n return undefined;\n }\n return false;\n}\n\n/**\n * Check whether the given directory is inside a git work-tree.\n * Results are cached per directory so repeated calls (e.g. during\n * debounced diff captures) do not re-spawn git processes.\n */\nexport async function isGitRepo(cwd: string): Promise<boolean> {\n const cached = readGitRepoCache(cwd);\n if (cached !== undefined) return cached;\n try {\n await runWithTimeout('git', ['rev-parse', '--git-dir'], cwd, TIMEOUT_MS);\n gitRepoCache.set(cwd, { isRepo: true });\n return true;\n } catch {\n gitRepoCache.set(cwd, {\n isRepo: false,\n expiresAt: Date.now() + NEGATIVE_CACHE_TTL_MS,\n });\n return false;\n }\n}\n\nlet ghAvailableCache = false;\n\nexport async function isGhAvailable(): Promise<boolean> {\n if (ghAvailableCache) return true;\n try {\n await run('which', ['gh']);\n ghAvailableCache = true;\n return true;\n } catch {\n return false;\n }\n}\n\nconst topLevelCache = new Map<string, string>();\n\nexport async function getGitTopLevel(cwd: string): Promise<string> {\n const cached = topLevelCache.get(cwd);\n if (cached !== undefined) return cached;\n const topLevel = (\n await runWithTimeout('git', ['rev-parse', '--show-toplevel'], cwd, TIMEOUT_MS)\n ).trim();\n topLevelCache.set(cwd, topLevel);\n return topLevel;\n}\n\nexport function parseOwnerRepo(remoteUrl: string): { owner: string; repo: string } | null {\n const match = remoteUrl.match(/github\\.com[:/]([^/]+)\\/([^/.]+)/);\n if (!match?.[1] || !match[2]) return null;\n return { owner: match[1], repo: match[2] };\n}\n\n/**\n * Look up the repo's default branch via the `origin/HEAD` symbolic ref.\n * Returns the branch name (e.g. `main`) or null when the symbolic ref\n * isn't set (rare — happens for repos cloned without `git remote\n * set-head` or local-only repos).\n *\n * The result is cached for 5 seconds via the per-cwd OneShotPool in\n * git-pool.ts so repeated calls from serve-factory and stack-detection\n * per request coalesce to a single subprocess.\n */\nexport async function getRepoDefaultBranch(cwd: string): Promise<string | null> {\n const head = await gitExecSafe(cwd, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);\n if (!head) return null;\n return head.replace(/^origin\\//, '') || null;\n}\n\nexport async function isAncestor(\n ancestor: string,\n descendant: string,\n cwd: string\n): Promise<boolean> {\n try {\n await runWithTimeout('git', ['merge-base', '--is-ancestor', ancestor, descendant], cwd, 5_000);\n return true;\n } catch {\n return false;\n }\n}\n\nconst EXCLUDE_DIRS = new Set([\n 'node_modules',\n 'Library',\n 'Applications',\n 'Pictures',\n 'Music',\n 'Movies',\n 'go',\n '.Trash',\n]);\n\nconst MAX_DEPTH = 4;\nconst WORKTREE_DISCOVERY_CONCURRENCY = 8;\n/**\n * Lowered from 16 → 4 (#4516): 16 concurrent stat+readFile calls per walk\n * saturated the shared libuv thread pool (default 4 workers), starving the\n * main thread's I/O and causing 10–35s event-loop stalls on the fleet.\n *\n * Budget math: worst-case (all paths time out) → ceil(N/4) × GIT_PROBE_TIMEOUT_MS.\n * At 150 repos: ceil(150/4) × 3_000ms = 114s. The full-detect worker timeout is\n * CAPABILITY_DETECTOR_FULL_WORKER_TIMEOUT_MS (105s), so the absolute worst case\n * (every single path hitting NFS timeout) would exceed the budget. In practice\n * only a handful of paths ever time out (dead mounts, not all repos), so the\n * effective walk time stays well under 30s. Raising to 6 would lower worst-case\n * to 75s if needed.\n */\nconst REPO_METADATA_CONCURRENCY = 4;\n/**\n * Per-level concurrency cap for the recursive readdir fan-out in\n * `findGitRepos`. Replaces the previous unbounded `Promise.all(promises)`,\n * which on a machine with 150+ repos could create hundreds of concurrent\n * readdir callbacks all landing on the event loop within milliseconds of\n * each other, effectively blocking it for the duration.\n */\nexport const FIND_GIT_REPOS_DIR_CONCURRENCY = 8;\n\nasync function mapLimit<T, R>(\n items: readonly T[],\n limit: number,\n mapper: (item: T, index: number) => Promise<R>\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n const indexedItems = items.map((item, index) => ({ item, index }));\n let nextIndex = 0;\n const workerCount = Math.min(Math.max(1, limit), items.length);\n\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n for (;;) {\n const next = indexedItems[nextIndex];\n nextIndex += 1;\n if (!next) return;\n results[next.index] = await mapper(next.item, next.index);\n }\n })\n );\n\n return results;\n}\n\nexport async function findGitRepos(dir: string, depth = 0): Promise<string[]> {\n if (depth > MAX_DEPTH) return [];\n\n /**\n * Yield to the event loop between depth levels (#4516). Without this, a\n * single walk issues hundreds of readdir callbacks in rapid succession —\n * all queued on the JS event loop simultaneously — which can delay timer\n * callbacks (including the 100ms event-loop watchdog) for several seconds.\n */\n await new Promise<void>((r) => {\n setImmediate(r);\n });\n\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name === '.git') {\n return [dir];\n }\n }\n\n if (depth >= MAX_DEPTH) return [];\n\n const subdirs: string[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith('.')) continue;\n if (EXCLUDE_DIRS.has(entry.name)) continue;\n subdirs.push(join(dir, entry.name));\n }\n\n /**\n * Bounded fan-out replaces the previous unbounded `Promise.all(promises)`\n * (#4516). On a machine with 150+ repos at depth 4, the old approach could\n * spawn 1000s of concurrent readdir calls, all landing on the event loop\n * within milliseconds and saturating the shared libuv thread pool.\n */\n const results = await mapLimit(subdirs, FIND_GIT_REPOS_DIR_CONCURRENCY, (subdir) =>\n findGitRepos(subdir, depth + 1)\n );\n return results.flat();\n } catch {\n return [];\n }\n}\n\n/**\n * Read the current branch name by parsing `.git/HEAD` directly.\n *\n * Format of `.git/HEAD`:\n * - Checked-out branch: `ref: refs/heads/<branch>\\n`\n * - Detached HEAD: `<sha>\\n`\n *\n * First attempts to read `.git/HEAD` directly (fast path for normal repos).\n * Falls back to `resolveHeadPath` for linked worktrees where `.git` is a\n * file pointing to a gitdir rather than a directory.\n */\nasync function readBranchFromHead(repoPath: string): Promise<string> {\n const directHeadPath = join(repoPath, '.git', 'HEAD');\n try {\n const direct = await readFile(directHeadPath, 'utf8');\n const trimmed = direct.trim();\n if (trimmed.startsWith('ref: refs/heads/')) {\n return trimmed.slice('ref: refs/heads/'.length);\n }\n if (!trimmed.startsWith('gitdir:')) {\n return 'HEAD';\n }\n } catch {}\n\n try {\n const headPath = await resolveHeadPath(repoPath);\n if (!headPath) return 'HEAD';\n const contents = await readFile(headPath, 'utf8');\n const trimmed2 = contents.trim();\n if (trimmed2.startsWith('ref: refs/heads/')) {\n return trimmed2.slice('ref: refs/heads/'.length);\n }\n return 'HEAD';\n } catch {\n return 'HEAD';\n }\n}\n\nexport async function getRepoMetadata(\n repoPath: string,\n cached?: CacheEntry\n): Promise<GitRepoInfo | null> {\n try {\n if (cached) {\n return {\n path: repoPath,\n name: basename(repoPath),\n branch: cached.branch,\n ...(cached.remote !== null && { remote: cached.remote }),\n };\n }\n\n /**\n * Both reads are bounded by `GIT_PROBE_TIMEOUT_MS`. `gitExecSafe`\n * already has the 5s `OneShotPool` TTL but no hard kill — a hung\n * `git remote get-url` would pin the walker indefinitely without\n * this race. `readBranchFromHead` is plain `fs.readFile` and is\n * usually fast, but the timeout is cheap insurance against an NFS\n * stall on the HEAD file itself.\n */\n const [branchResult, remoteResult] = await Promise.all([\n withProbeTimeout(readBranchFromHead(repoPath), GIT_PROBE_TIMEOUT_MS),\n withProbeTimeout(\n gitExecSafe(repoPath, ['remote', 'get-url', 'origin']),\n GIT_PROBE_TIMEOUT_MS\n ),\n ]);\n const branch = branchResult ?? 'HEAD';\n const remote = remoteResult ? remoteResult || undefined : undefined;\n\n return {\n path: repoPath,\n name: basename(repoPath),\n branch,\n ...(remote && { remote }),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Process a single repo path during the environments walk: resolve cache hit,\n * run the timed metadata probe, update `nextCache`, and emit slow/timeout logs.\n * Extracted from the `mapLimit` callback in `detectEnvironments` to keep that\n * function under the cognitive-complexity ceiling.\n */\nasync function processRepoPaths(\n path: string,\n idx: number,\n fingerprints: GitCacheFingerprint[],\n prevCache: Map<string, CacheEntry>,\n nextCache: Map<string, CacheEntry>\n): Promise<GitRepoInfo | null> {\n const currentFingerprint = fingerprints[idx] ?? { headMtimeMs: null, configMtimeMs: null };\n const prev = prevCache.get(path);\n const cacheHit =\n prev !== undefined &&\n currentFingerprint.headMtimeMs !== null &&\n isCacheValid(prev, currentFingerprint);\n\n const start = Date.now();\n const info = await withProbeTimeout(\n getRepoMetadata(path, cacheHit ? prev : undefined),\n GIT_PROBE_TIMEOUT_MS\n );\n const durationMs = Date.now() - start;\n\n if (info === null && durationMs >= GIT_PROBE_TIMEOUT_MS * 0.9) {\n logger.warn(\n { event: 'environments_walk_path_timed_out', path, durationMs },\n 'environments walk skipped path past per-path timeout'\n );\n } else if (durationMs >= SLOW_PATH_LOG_THRESHOLD_MS) {\n logger.warn(\n { event: 'environments_walk_path_slow', path, durationMs },\n 'environments walk path resolved slowly'\n );\n }\n\n if (info !== null && currentFingerprint.headMtimeMs !== null) {\n nextCache.set(path, {\n path,\n branch: info.branch,\n remote: info.remote ?? null,\n headMtimeMs: currentFingerprint.headMtimeMs,\n configMtimeMs: currentFingerprint.configMtimeMs,\n cachedAt: cacheHit && prev ? prev.cachedAt : Date.now(),\n });\n }\n return info;\n}\n\n/**\n * Module-level single-flight for `detectEnvironments` (#4516).\n *\n * Deduplicates concurrent callers within the SAME JS context (same V8 isolate\n * — e.g. same worker thread or the main thread). When N callers call\n * `detectEnvironments()` before the current walk resolves, they all receive\n * the same promise rather than starting N independent walks.\n *\n * SCOPE: This covers same-context re-entrancy only. Full-detect workers are\n * spawned fresh per call (each isolate starts with `inFlightDetect = null`),\n * so cross-worker concurrency is prevented separately — see the backstop gate\n * on `daemon.capabilityDetectionReady` in serve.ts, which ensures the initial\n * detection worker completes before the first backstop tick fires.\n *\n * The first caller's `lastKnown` wins for the dedup window; subsequent callers\n * receive the same resolved value. `lastKnown` is a fallback for transient-\n * empty walks only, so this is safe — the walk result is authoritative.\n */\nlet inFlightDetect: Promise<GitRepoInfo[]> | null = null;\n\n/**\n * Detect git environments under $HOME.\n *\n * `lastKnown` preserves the previously-detected environment list when the\n * underlying `findGitRepos` walk returns `[]` for a non-authoritative reason\n * (root readdir failure: EMFILE, EACCES, EBUSY). Without this, a transient\n * filesystem hiccup in the 30s capability-refresh tick collapses environments\n * to `[]`, which `applyFreshCapabilities` writes wholesale, and the browser\n * briefly renders \"No environment\" until the next successful tick. Mirror of\n * the `lastKnownAgents` pattern in `agents.ts`.\n *\n * Discrimination: a successful walk that genuinely finds zero repos is\n * indistinguishable from a swallowed-error walk in the current shape (the\n * inner `try/catch` in `findGitRepos` returns `[]` on readdir failure). The\n * `lastKnown.length > 0` guard means we only preserve when there's something\n * worth preserving; a clean machine that genuinely has zero repos still gets\n * `[]`.\n */\nexport function detectEnvironments(lastKnown?: GitRepoInfo[]): Promise<GitRepoInfo[]> {\n if (inFlightDetect) return inFlightDetect;\n const promise = runDetectEnvironments(lastKnown);\n inFlightDetect = promise;\n /**\n * Swallow the rejection on the stored ref so the module-level holder never\n * becomes a tracked-but-unhandled rejected promise. Callers receive the real\n * rejection via the `promise` reference returned to them.\n */\n promise\n .catch(() => {})\n .finally(() => {\n if (inFlightDetect === promise) inFlightDetect = null;\n });\n return promise;\n}\n\nasync function runDetectEnvironments(lastKnown?: GitRepoInfo[]): Promise<GitRepoInfo[]> {\n const { homedir } = await import('node:os');\n\n /**\n * The repo-paths cache (`repo-paths-cache.ts`) holds the result of the\n * `findGitRepos(homedir())` walk and the N parallel `git worktree list`\n * spawns — together ~12-15s on a busy machine. Per-repo branch info is\n * still resolved fresh below via `getRepoMetadata` (cached separately by\n * git HEAD + config mtimes in `environments-cache.ts`). The list cache lives\n * for the daemon lifetime; `invalidateRepoPaths()` is called by\n * worktree-service (add/remove), capability-watcher (FSEvents rescan),\n * and explicit force-refresh paths.\n */\n const cachedPaths = getCachedRepoPaths();\n let repoPaths: string[];\n let worktreePaths: string[];\n\n if (cachedPaths) {\n repoPaths = cachedPaths.repos;\n worktreePaths = cachedPaths.worktrees;\n } else {\n try {\n repoPaths = await findGitRepos(homedir());\n } catch (err) {\n if (lastKnown && lastKnown.length > 0) {\n const { logger } = await import('../logger.js');\n logger.warn(\n { err, lastKnownCount: lastKnown.length },\n 'detectEnvironments findGitRepos threw — preserving lastKnown'\n );\n return lastKnown;\n }\n return [];\n }\n\n if (repoPaths.length === 0 && lastKnown && lastKnown.length > 0) {\n /**\n * Walk returned empty but we know there used to be repos — almost\n * certainly a transient root-readdir error swallowed inside\n * `findGitRepos`. Preserve rather than wipe.\n */\n const { logger } = await import('../logger.js');\n logger.warn(\n { lastKnownCount: lastKnown.length },\n 'detectEnvironments walk empty but lastKnown non-empty — preserving lastKnown'\n );\n return lastKnown;\n }\n\n worktreePaths = await discoverWorktrees(repoPaths);\n setCachedRepoPaths(repoPaths, worktreePaths);\n }\n\n const allPaths = [...new Set([...repoPaths, ...worktreePaths])];\n\n const prevCache = await loadCache();\n const fingerprints = await mapLimit(allPaths, REPO_METADATA_CONCURRENCY, getGitCacheFingerprint);\n const nextCache = new Map<string, CacheEntry>();\n\n /**\n * Per-path timing + hard timeout. Walk continues past timed-out paths so\n * one bad mount (NFS, dead symlink, removed volume) can't pin the entire\n * environments slice. Slow paths log a structured event for diagnosis.\n */\n const resolved = await mapLimit(allPaths, REPO_METADATA_CONCURRENCY, (path, idx) =>\n processRepoPaths(path, idx, fingerprints, prevCache, nextCache)\n );\n\n await saveCache(nextCache);\n\n return resolved.filter((info): info is GitRepoInfo => info !== null);\n}\n\n/**\n * For each repo, run `git worktree list --porcelain` to discover\n * linked worktrees (which may live in dot-directories like .claude/worktrees/).\n *\n * Dedupes across repos: linked worktrees that are themselves git repos get\n * walked AGAIN by `findGitRepos`, and running `git worktree list` from each\n * one reports the same set of paths back. Without dedupe the cache grew to\n * 7,420 entries on a machine with only 149 unique worktrees (2026-05-14\n * boot wedge investigation). `dedupePerRepoWorktreeOutputs` collapses to\n * unique paths while preserving the first-seen ordering for determinism.\n */\nexport async function discoverWorktrees(repoPaths: string[]): Promise<string[]> {\n const results = await mapLimit(\n repoPaths,\n WORKTREE_DISCOVERY_CONCURRENCY,\n async (repoPath): Promise<PromiseSettledResult<string[]>> => {\n try {\n return { status: 'fulfilled', value: await listLinkedWorktrees(repoPath) };\n } catch (reason) {\n return { status: 'rejected', reason };\n }\n }\n );\n return dedupePerRepoWorktreeOutputs(results);\n}\n\nfunction dedupePerRepoWorktreeOutputs(results: PromiseSettledResult<string[]>[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const r of results) {\n if (r.status !== 'fulfilled') continue;\n for (const wt of r.value) {\n if (seen.has(wt)) continue;\n seen.add(wt);\n out.push(wt);\n }\n }\n return out;\n}\n\n/**\n * Enumerate linked worktrees for a git repository by reading the\n * `.git/worktrees/` directory directly rather than spawning a git subprocess.\n *\n * Git's on-disk format (gitrepository-layout(5), stable since git 2.5):\n * .git/worktrees/<name>/gitdir — contains the path to the worktree's\n * `.git` file, e.g. `/path/to/wt/.git`\n *\n * Stripping `/.git` from that content yields the worktree path. Missing\n * `.git/worktrees/` directory means no linked worktrees — return [].\n * Orphaned entries (gitdir file missing/unreadable) are silently skipped.\n */\nasync function listLinkedWorktrees(repoPath: string): Promise<string[]> {\n const gitWorktreesDir = join(repoPath, '.git', 'worktrees');\n let entries: string[];\n try {\n const result = await withProbeTimeout(readdir(gitWorktreesDir), GIT_PROBE_TIMEOUT_MS);\n if (result === null) return [];\n entries = result;\n } catch {\n return [];\n }\n\n /**\n * Bounded fan-out over gitdir files (#4516). A repo with many linked\n * worktrees would otherwise fan out N concurrent readFile calls, the same\n * class of issue as the unbounded `findGitRepos` fan-out. Each per-name op\n * is also timeout-guarded to match the `processRepoPaths` pattern.\n */\n const paths = await mapLimit(entries, FIND_GIT_REPOS_DIR_CONCURRENCY, async (name) => {\n const result = await withProbeTimeout(\n (async () => {\n const gitdirFile = join(gitWorktreesDir, name, 'gitdir');\n const content = await readFile(gitdirFile, 'utf8');\n const wtGitPath = content.trim();\n return wtGitPath.endsWith('/.git') ? wtGitPath.slice(0, -'/.git'.length) : null;\n })(),\n GIT_PROBE_TIMEOUT_MS\n );\n return result ?? null;\n });\n\n return paths.filter((p): p is string => p !== null && p !== repoPath);\n}\n\n/** @internal kept for tests that exercise parse logic on porcelain output */\nfunction parseWorktreeListOutput(output: string, repoPath: string): string[] {\n const paths: string[] = [];\n for (const line of output.split('\\n')) {\n if (line.startsWith('worktree ')) {\n const wt = line.slice('worktree '.length).trim();\n if (wt && wt !== repoPath) paths.push(wt);\n }\n }\n return paths;\n}\n\nexport const _testing = {\n parseWorktreeListOutput,\n dedupePerRepoWorktreeOutputs,\n listLinkedWorktrees,\n readBranchFromHead,\n mapLimit,\n withProbeTimeout,\n GIT_PROBE_TIMEOUT_MS,\n SLOW_PATH_LOG_THRESHOLD_MS,\n NEGATIVE_CACHE_TTL_MS,\n FIND_GIT_REPOS_DIR_CONCURRENCY,\n readGitRepoCache,\n resetGitRepoCache: () => {\n gitRepoCache.clear();\n },\n resetGhAvailableCache: () => {\n ghAvailableCache = false;\n },\n resetInFlightDetect: () => {\n inFlightDetect = null;\n },\n};\n","import { mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { z } from 'zod';\nimport { getShipyardHome } from '../env.js';\nimport { isEnoent } from '../fs-utils.js';\nimport { logger } from '../logger.js';\n\nconst CacheEntrySchema = z.object({\n path: z.string(),\n branch: z.string(),\n remote: z.string().nullable(),\n headMtimeMs: z.number(),\n /**\n * Added after the original cache shape. Optional so old disk caches parse,\n * but `isCacheValid` treats missing as invalid and refreshes once.\n */\n configMtimeMs: z.number().nullable().optional(),\n cachedAt: z.number(),\n});\n\nconst CacheFileSchema = z.object({\n entries: z.record(z.string(), CacheEntrySchema),\n});\n\nexport type CacheEntry = z.infer<typeof CacheEntrySchema>;\ntype CacheFile = z.infer<typeof CacheFileSchema>;\nexport interface GitCacheFingerprint {\n headMtimeMs: number | null;\n configMtimeMs: number | null;\n}\n\nfunction cacheFilePath(): string {\n return join(getShipyardHome(), 'data', 'environments-cache.json');\n}\n\nlet cacheMigrationDone = false;\n\nexport async function loadCache(): Promise<Map<string, CacheEntry>> {\n const filePath = cacheFilePath();\n try {\n const raw = await readFile(filePath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n const result = CacheFileSchema.safeParse(parsed);\n if (!result.success) {\n logger.debug({ path: filePath }, 'environments-cache: malformed, treating as empty');\n cacheMigrationDone = true;\n return new Map();\n }\n const cache = new Map(Object.entries(result.data.entries));\n if (!cacheMigrationDone) {\n cacheMigrationDone = true;\n await migrateConfigMtimes(cache);\n }\n return cache;\n } catch (err) {\n if (isEnoent(err)) {\n cacheMigrationDone = true;\n return new Map();\n }\n logger.debug({ err }, 'environments-cache: read failed, treating as empty');\n return new Map();\n }\n}\n\n/**\n * One-shot migration: entries written before #3619 lack `configMtimeMs`.\n * `isCacheValid` would force a re-spawn for every such entry, creating a\n * git subprocess storm on first boot after upgrade. Backfill the field by\n * reading `.git/config` mtimes directly, then persist so subsequent boots\n * hit the cache normally.\n */\nasync function migrateConfigMtimes(cache: Map<string, CacheEntry>): Promise<void> {\n const stale = [...cache.values()].filter((e) => e.configMtimeMs === undefined);\n if (stale.length === 0) return;\n await Promise.all(\n stale.map(async (entry) => {\n const fp = await getGitCacheFingerprint(entry.path);\n cache.set(entry.path, { ...entry, configMtimeMs: fp.configMtimeMs });\n })\n );\n logger.info({ count: stale.length }, 'environments-cache: cache_migration_seeded');\n await saveCache(cache);\n}\n\n/**\n * Resolve git metadata paths for a repository or linked worktree.\n *\n * - If `<path>/.git` is a directory, metadata lives inside that directory.\n * - If `<path>/.git` is a file, it contains `gitdir: <path>`. HEAD lives in\n * that worktree gitdir, while remotes live in the common git dir's config.\n * - Returns null on any error (missing, unreadable, malformed).\n */\nasync function readGitdirFile(dotGitPath: string): Promise<string | null> {\n const contents = await readFile(dotGitPath, 'utf8');\n const match = contents.match(/^gitdir:\\s*(.+)\\s*$/m);\n if (!match?.[1]) return null;\n const gitdirRaw = match[1].trim();\n return isAbsolute(gitdirRaw) ? gitdirRaw : resolve(dirname(dotGitPath), gitdirRaw);\n}\n\nasync function resolveCommonGitDir(gitdir: string): Promise<string> {\n try {\n const raw = await readFile(join(gitdir, 'commondir'), 'utf8');\n const commonDirRaw = raw.trim();\n if (commonDirRaw === '') return gitdir;\n return isAbsolute(commonDirRaw) ? commonDirRaw : resolve(gitdir, commonDirRaw);\n } catch {\n return gitdir;\n }\n}\n\nexport async function resolveHeadPath(worktreePath: string): Promise<string | null> {\n const paths = await resolveGitMetadataPaths(worktreePath);\n return paths?.headPath ?? null;\n}\n\nexport async function resolveGitMetadataPaths(\n worktreePath: string\n): Promise<{ headPath: string; configPath: string | null } | null> {\n const dotGitPath = join(worktreePath, '.git');\n try {\n const dotGitStat = await stat(dotGitPath);\n if (dotGitStat.isDirectory()) {\n return { headPath: join(dotGitPath, 'HEAD'), configPath: join(dotGitPath, 'config') };\n }\n if (dotGitStat.isFile()) {\n const gitdir = await readGitdirFile(dotGitPath);\n if (!gitdir) return null;\n const commonGitDir = await resolveCommonGitDir(gitdir);\n return { headPath: join(gitdir, 'HEAD'), configPath: join(commonGitDir, 'config') };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nasync function getPathMtime(path: string | null): Promise<number | null> {\n if (!path) return null;\n try {\n const pathStat = await stat(path);\n return pathStat.mtimeMs;\n } catch {\n return null;\n }\n}\n\nexport async function getGitCacheFingerprint(worktreePath: string): Promise<GitCacheFingerprint> {\n const paths = await resolveGitMetadataPaths(worktreePath);\n if (!paths) return { headMtimeMs: null, configMtimeMs: null };\n const [headMtimeMs, configMtimeMs] = await Promise.all([\n getPathMtime(paths.headPath),\n getPathMtime(paths.configPath),\n ]);\n return { headMtimeMs, configMtimeMs };\n}\n\nexport function isCacheValid(entry: CacheEntry, current: GitCacheFingerprint): boolean {\n /**\n * `.git/HEAD` invalidates branch, and `.git/config` invalidates remote.\n * Entries without `configMtimeMs` are soft-migrated at `loadCache` time\n * (see `migrateConfigMtimes`), so by the time this is called the field\n * is always present. Treat `undefined` the same as `null` for safety.\n */\n return (\n entry.headMtimeMs === current.headMtimeMs &&\n (entry.configMtimeMs ?? null) === current.configMtimeMs\n );\n}\n\nexport async function saveCache(entries: Map<string, CacheEntry>): Promise<void> {\n const filePath = cacheFilePath();\n const tmpPath = `${filePath}.tmp`;\n try {\n await mkdir(dirname(filePath), { recursive: true });\n /**\n * Merge-on-save: re-read disk immediately before writing so a concurrent\n * saveCache call's entries are preserved rather than erased (new entries\n * win for paths present in both — they are fresher).\n */\n const current = await loadCache();\n for (const [path, entry] of entries) {\n current.set(path, entry);\n }\n const payload: CacheFile = { entries: Object.fromEntries(current) };\n await writeFile(tmpPath, JSON.stringify(payload), 'utf8');\n await rename(tmpPath, filePath);\n } catch (err) {\n logger.debug({ err, filePath }, 'environments-cache: save failed');\n void unlink(tmpPath).catch(() => {});\n }\n}\n","/**\n * In-memory cache of the list of git repo paths under $HOME and the linked\n * worktree paths discovered via `git worktree list --porcelain` for each.\n *\n * Distinct from `environments-cache.ts` (which caches per-repo branch/remote\n * info on disk, keyed by git HEAD + config mtimes). This one caches the LIST of repos and\n * worktrees — i.e. the `findGitRepos(homedir())` walk and the N parallel\n * `git worktree list` spawns. Those are the ~12-50s cost on a busy machine.\n *\n * Lifetime: persisted to disk at `~/.shipyard/data/repo-paths-cache.json`\n * and rehydrated on every daemon boot. The previous implementation rejected\n * the disk cache outright when its age exceeded 1h — which is almost every\n * reboot in practice — and forced the 30s `findGitRepos($HOME)` walk to fire\n * on each cold boot. We now serve any structurally-valid cache regardless of\n * age and kick off a background re-walk when the cache is older than 5\n * minutes. Boot time drops from 30-50s to milliseconds; freshness is\n * restored ~10-30s after boot when the background walk completes.\n *\n * Invalidation:\n * - `invalidateRepoPaths()` — called by:\n * * capability-watcher.ts (FSEvents rescan + escalation paths)\n * * explicit force-refresh control message\n * - `addWorktreeToCache()` / `removeWorktreeFromCache()` — incremental\n * update used by worktree create/remove. Avoids the cold-walk recompute\n * storm that otherwise stalls the event loop 12-15s and kills the\n * daemon's WebRTC + control channels mid-creation. If there is no\n * active cache (boot before first walk), the call is a no-op — the\n * next cold walk picks up the new state.\n *\n * The walk + worktree spawns ARE NOT triggered by the capability-watcher's\n * normal events: scoped re-detect (`refreshMcpServers` etc.) skips\n * `detectEnvironments` entirely and reuses the existing `daemon.capabilities\n * ?.environments`. The cache is only consulted by full `detectCapabilities`\n * runs (manual Refresh button, boot, cwd-changed).\n */\n\nimport { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { z } from 'zod';\nimport { getShipyardHome } from '../env.js';\nimport { isEnoent } from '../fs-utils.js';\nimport type { createChildLogger } from '../logger.js';\nimport { logger } from '../logger.js';\n\nexport type { HydrateResult };\nexport {\n addWorktreeToCache,\n getCachedRepoPaths,\n hydrateAndLogRepoPathsCache,\n hydrateRepoPathsCacheFromDisk,\n invalidateRepoPaths,\n removeWorktreeFromCache,\n setCachedRepoPaths,\n};\n\ninterface CachedEntry {\n repos: string[];\n worktrees: string[];\n populatedAt: number;\n}\n\ninterface HydrateResult {\n hit: boolean;\n stale: boolean;\n}\n\nconst RepoPathsDiskSchema = z.object({\n repos: z.array(z.string()),\n worktrees: z.array(z.string()),\n populatedAt: z.number(),\n});\n\nconst STALE_AFTER_MS_DEFAULT = 5 * 60 * 1000;\n\nlet cache: CachedEntry | null = null;\n/**\n * Monotonic counter incremented on every mutating call. Async disk operations\n * capture the generation at the moment they were scheduled and no-op when\n * the generation has changed by the time they get to the syscall. Without\n * this, `invalidateRepoPaths()` (which fires `void unlinkDisk()`) racing\n * with a subsequent `setCachedRepoPaths()` (which fires `void persistDisk()`)\n * could leave the disk in either state — and if `unlinkDisk` wins after\n * `persistDisk`'s rename succeeds, the next daemon restart pays the full\n * cold-walk cost despite a valid cache having just been written.\n */\nlet cacheGeneration = 0;\nlet backgroundRefreshInFlight = false;\nconst pendingDiskOperations = new Set<Promise<void>>();\n\nfunction trackDiskOperation(operation: Promise<void>): void {\n const tracked = operation\n .catch((err: unknown) => {\n logger.debug({ err }, 'repo-paths-cache: async disk operation failed');\n })\n .finally(() => {\n pendingDiskOperations.delete(tracked);\n });\n pendingDiskOperations.add(tracked);\n}\n\nfunction cacheFilePath(): string {\n return join(getShipyardHome(), 'data', 'repo-paths-cache.json');\n}\n\nfunction getCachedRepoPaths(): { repos: string[]; worktrees: string[] } | null {\n if (!cache) return null;\n return { repos: cache.repos, worktrees: cache.worktrees };\n}\n\nfunction setCachedRepoPaths(repos: string[], worktrees: string[], now: number = Date.now()): void {\n cache = { repos, worktrees, populatedAt: now };\n cacheGeneration += 1;\n trackDiskOperation(persistDisk(cache, cacheGeneration));\n}\n\nfunction invalidateRepoPaths(): void {\n cache = null;\n cacheGeneration += 1;\n trackDiskOperation(unlinkDisk(cacheGeneration));\n}\n\n/**\n * Best-effort hydrate the in-memory cache from disk on daemon boot. Always\n * accepts a structurally-valid on-disk entry regardless of age — the boot\n * path needs the data immediately and any staleness is repaired by a\n * background re-walk scheduled by `hydrateAndLogRepoPathsCache`. Returns\n * `{ hit, stale }` so the caller knows whether to schedule that refresh.\n *\n * The `staleAfterMs` threshold defines when the cache is \"old enough to\n * re-walk in the background\" — not when it's \"too old to serve\". Default\n * 5 minutes; data older than that triggers a background refresh but is\n * still returned to the boot path so `detect_environments` sees a populated\n * cache and skips the 30s `findGitRepos` walk + `git worktree list` storm.\n *\n * Wired from `serve.ts` BEFORE `createSignalingHandle()` so the first\n * `detectEnvironments` call inside the background `runCapabilityDetection`\n * (#3360 — detection moved off the boot path) sees the populated in-memory\n * cache. Without this, every daemon restart pays the full cold-walk cost —\n * observed at 30-50s on busy machines and the dominant component of 42.5s\n * worst-case boot.\n */\nasync function hydrateRepoPathsCacheFromDisk(opts?: {\n staleAfterMs?: number;\n}): Promise<HydrateResult> {\n if (cache !== null) return { hit: false, stale: false };\n const filePath = cacheFilePath();\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf8');\n } catch (err: unknown) {\n if (isEnoent(err)) return { hit: false, stale: false };\n logger.debug({ err, filePath }, 'repo-paths-cache: read failed');\n return { hit: false, stale: false };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { hit: false, stale: false };\n }\n const result = RepoPathsDiskSchema.safeParse(parsed);\n if (!result.success) return { hit: false, stale: false };\n\n /**\n * Race-safe: another caller may have populated the slot between our\n * `cache !== null` check and the disk read. Don't clobber the live entry.\n */\n if (cache !== null) return { hit: false, stale: false };\n\n const staleAfterMs = opts?.staleAfterMs ?? STALE_AFTER_MS_DEFAULT;\n const ageMs = Math.max(0, Date.now() - result.data.populatedAt);\n const stale = ageMs > staleAfterMs;\n\n cache = result.data;\n logger.info(\n { repos: result.data.repos.length, worktrees: result.data.worktrees.length, ageMs, stale },\n 'repo-paths-cache: hydrated from disk'\n );\n return { hit: true, stale };\n}\n\nasync function persistDisk(entry: CachedEntry, generation: number): Promise<void> {\n const filePath = cacheFilePath();\n const tmpPath = `${filePath}.tmp`;\n try {\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(tmpPath, JSON.stringify(entry), 'utf8');\n /**\n * Drop the write if a later mutation (set/invalidate/add/remove) raced\n * us. The successor's persist/unlink will reach the rename target with\n * the right state. Without this, an in-flight rename can clobber the\n * successor's just-written file or, worse, race a fresh persist after\n * an invalidate.\n */\n if (generation !== cacheGeneration) {\n await unlink(tmpPath).catch(() => {});\n return;\n }\n await rename(tmpPath, filePath);\n } catch (err: unknown) {\n logger.debug({ err, filePath }, 'repo-paths-cache: save failed');\n void unlink(tmpPath).catch(() => {});\n }\n}\n\nasync function unlinkDisk(generation: number): Promise<void> {\n /**\n * Drop the unlink if a later `setCachedRepoPaths` already raced us — its\n * `persistDisk` will write the fresh file and the file the unlink would\n * have removed is the predecessor's. Without this generation check, an\n * `invalidate → set` sequence can end up with NO disk file even though\n * `set` persisted one.\n */\n if (generation !== cacheGeneration) return;\n try {\n await unlink(cacheFilePath());\n } catch {\n /** No-op — file may not exist yet. */\n }\n}\n\n/**\n * Append a freshly-created worktree to the existing cached result. Source\n * repo path is also added (deduped) because a brand-new repo can host a\n * brand-new worktree, and the next `detectEnvironments` call needs to see\n * both. No-op when there is no cache yet — the next cold walk will discover\n * the new entries naturally.\n */\nfunction addWorktreeToCache(worktreePath: string, sourceRepoPath: string): void {\n if (!cache) return;\n const repos = cache.repos.includes(sourceRepoPath)\n ? cache.repos\n : [...cache.repos, sourceRepoPath];\n const worktrees = cache.worktrees.includes(worktreePath)\n ? cache.worktrees\n : [...cache.worktrees, worktreePath];\n cache = { repos, worktrees, populatedAt: cache.populatedAt };\n cacheGeneration += 1;\n trackDiskOperation(persistDisk(cache, cacheGeneration));\n}\n\n/**\n * Splice a worktree path out of the cached worktree list. The source repo\n * stays — removing a single linked worktree never deletes its source. No-op\n * when there is no cache yet.\n */\nfunction removeWorktreeFromCache(worktreePath: string): void {\n if (!cache) return;\n const next = cache.worktrees.filter((p) => p !== worktreePath);\n if (next.length === cache.worktrees.length) return;\n cache = { repos: cache.repos, worktrees: next, populatedAt: cache.populatedAt };\n cacheGeneration += 1;\n trackDiskOperation(persistDisk(cache, cacheGeneration));\n}\n\n/**\n * Re-walk `$HOME` and rewrite the cache. Fired by `hydrateAndLogRepoPathsCache`\n * on boot when the hydrated cache is stale, restoring freshness ~10-30s after\n * the user is already interactive. Single-flight: a second call while one is\n * in progress is a no-op. Generation-guarded: if any synchronous mutation\n * (`setCachedRepoPaths`, `addWorktreeToCache`, `removeWorktreeFromCache`,\n * `invalidateRepoPaths`) lands while the walk is in flight, the refresh\n * result is dropped so it does not clobber a more-recent incremental update.\n *\n * Lazy-imports `./git-repo.js` to break the import cycle (`git-repo` already\n * imports this module).\n */\nasync function refreshRepoPathsInBackground(\n log: ReturnType<typeof createChildLogger>\n): Promise<void> {\n if (backgroundRefreshInFlight) return;\n backgroundRefreshInFlight = true;\n const startGen = cacheGeneration;\n const start = Date.now();\n try {\n const { findGitRepos, discoverWorktrees } = await import('./git-repo.js');\n const { homedir } = await import('node:os');\n const repos = await findGitRepos(homedir());\n const worktrees = await discoverWorktrees(repos);\n if (cacheGeneration !== startGen) {\n log.debug(\n { event: 'repo_paths_cache_refresh_dropped' },\n 'repo-paths-cache: background refresh dropped (cache mutated mid-walk)'\n );\n return;\n }\n /**\n * Defense in depth: a transient root-readdir failure inside\n * `findGitRepos` returns `[]`, indistinguishable from \"no repos exist\".\n * If the prior cache had entries, preserve it rather than wipe — the\n * next refresh (capability-watcher tick) will retry. Mirrors the same\n * guard inside `detectEnvironments`.\n */\n if (repos.length === 0 && cache && cache.repos.length > 0) {\n log.warn(\n {\n event: 'repo_paths_cache_refresh_empty_preserved',\n prevRepos: cache.repos.length,\n },\n 'repo-paths-cache: background refresh returned empty; preserving prior cache'\n );\n return;\n }\n setCachedRepoPaths(repos, worktrees);\n log.info(\n {\n event: 'repo_paths_cache_refresh_complete',\n repos: repos.length,\n worktrees: worktrees.length,\n durationMs: Date.now() - start,\n },\n 'repo-paths-cache: background refresh complete'\n );\n } catch (err) {\n log.warn(\n { err, event: 'repo_paths_cache_refresh_failed', durationMs: Date.now() - start },\n 'repo-paths-cache: background refresh failed'\n );\n } finally {\n backgroundRefreshInFlight = false;\n }\n}\n\n/**\n * Hydrate from disk and log the hit/miss outcome. Boot callers wrap this in\n * `bootstrapPhase('repo_paths_cache_hydrate', ...)` — the wrapper always\n * records `outcome: 'success'` because hydrate returns a result object (no\n * throw on ENOENT). The explicit log here is what surfaces whether the\n * upcoming `detect_environments` will pay the cold walk and whether a\n * background refresh was scheduled.\n */\nasync function hydrateAndLogRepoPathsCache(\n log: ReturnType<typeof createChildLogger>\n): Promise<boolean> {\n const result = await hydrateRepoPathsCacheFromDisk();\n log.info(\n {\n event: 'repo_paths_cache_hydrate_result',\n hit: result.hit,\n stale: result.stale,\n },\n result.hit\n ? result.stale\n ? 'repo-paths cache hydrated from disk (stale; background refresh scheduled)'\n : 'repo-paths cache hydrated from disk (fresh)'\n : 'repo-paths cache cold (walk will fire inside detect_environments)'\n );\n if (result.hit && result.stale) {\n void refreshRepoPathsInBackground(log);\n }\n return result.hit;\n}\n\nexport const _testing = {\n reset: invalidateRepoPaths,\n drainDiskOperations: async () => {\n await Promise.allSettled([...pendingDiskOperations]);\n },\n inspect: () => cache,\n refreshRepoPathsInBackground,\n cacheFilePath,\n isBackgroundRefreshInFlight: () => backgroundRefreshInFlight,\n};\n","import { execFile } from 'node:child_process';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const TIMEOUT_MS = 5_000;\n/**\n * Cap on `claude auth status --json` during capability detection. Healthy\n * CLI responds in well under 1s; 15s was a worst-case-CLI-cold-start\n * accommodation that singlehandedly added up to 15s of boot stall when the\n * subprocess hung. 5s is generous slack while keeping the bootstrap\n * `detectCapabilities → signaling register` path bounded. See issue #3267.\n */\nexport const AUTH_STATUS_TIMEOUT_MS = 5_000;\n\n/**\n * Build the version-manager and user-local binary dirs for macOS.\n * These paths are absent when the daemon is launched from Finder/Spotlight\n * because macOS GUI apps inherit a stripped PATH that skips the user's\n * shell profile (where nvm/volta/mise/asdf entries normally live).\n *\n * Listed from most commonly needed (nvm, volta) to least common (cargo).\n * All are idempotently filtered by augmentPath — already-present entries\n * are never duplicated.\n */\nfunction buildDarwinVersionManagerPaths(home: string): string[] {\n return [\n join(home, '.nvm', 'current', 'bin'),\n join(home, '.volta', 'bin'),\n join(home, '.local', 'bin'),\n join(home, '.cargo', 'bin'),\n '/opt/homebrew/opt/node/bin',\n ];\n}\n\nexport const STANDARD_PATHS_BY_PLATFORM: Partial<Record<NodeJS.Platform, readonly string[]>> = {\n darwin: [\n '/usr/bin',\n '/usr/local/bin',\n '/opt/homebrew/bin',\n ...buildDarwinVersionManagerPaths(homedir()),\n ],\n linux: ['/usr/bin', '/usr/local/bin'],\n};\n\n/**\n * Daemons launched from Finder/Spotlight on macOS get a stripped PATH that may\n * omit /usr/bin where git lives. Prepend standard locations idempotently so\n * `execFile('git', ...)` resolves the binary. User-customized PATH entries\n * stay preferred — we only prepend dirs that aren't already present.\n */\nexport function augmentPath(currentPath: string, platform: NodeJS.Platform): string {\n const standardPaths = STANDARD_PATHS_BY_PLATFORM[platform];\n if (!standardPaths) return currentPath;\n\n const existing = currentPath.length > 0 ? currentPath.split(':') : [];\n const existingSet = new Set(existing);\n const toPrepend = standardPaths.filter((p) => !existingSet.has(p));\n if (toPrepend.length === 0) return currentPath;\n return existing.length > 0 ? `${toPrepend.join(':')}:${currentPath}` : toPrepend.join(':');\n}\n\nconst augmentedPath = augmentPath(process.env.PATH ?? '', process.platform);\nif (augmentedPath.length > 0) {\n process.env.PATH = augmentedPath;\n}\n\nexport function run(\n command: string,\n args: string[],\n cwd?: string,\n timeoutMs: number = TIMEOUT_MS\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(command, args, { timeout: timeoutMs, cwd }, (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout.trim());\n });\n });\n}\n\nexport function runWithTimeout(\n command: string,\n args: string[],\n cwd: string,\n timeoutMs: number\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n command,\n args,\n { timeout: timeoutMs, cwd, maxBuffer: 10 * 1024 * 1024 },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout.trim());\n }\n );\n });\n}\n\nexport const DIFF_TIMEOUT_MS = 15_000;\nexport const MAX_DIFF_SIZE = 500_000;\n\nexport function isMaxBufferError(err: unknown): boolean {\n return err instanceof Error && err.message.includes('maxBuffer');\n}\n\nexport const BUFFER_OVERFLOW_MSG = '... diff too large (exceeded buffer limit) ...\\n';\n\nexport function truncateDiff(result: string): string {\n if (result.length <= MAX_DIFF_SIZE) return result;\n const boundary = result.lastIndexOf('\\ndiff --git ', MAX_DIFF_SIZE);\n const cutoff = boundary > 0 ? boundary : MAX_DIFF_SIZE;\n return `${result.slice(0, cutoff)}\\n\\n... diff truncated (exceeds 500KB) ...\\n`;\n}\n"],"mappings":"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","names":["readFile","join","cache","mkdir","readFile","rename","unlink","writeFile","dirname","join","cacheFilePath","join","cacheFilePath","readFile","mkdir","dirname","writeFile","unlink","rename","findGitRepos","discoverWorktrees","homedir","join","resolve","resolve","join","readFile","homedir","logger"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
isImageMimeType,
|
|
4
4
|
parseAssetUri
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-BK24XTBR.js";
|
|
6
6
|
import {
|
|
7
7
|
assertNever
|
|
8
8
|
} from "./chunk-X3MULCV5.js";
|
|
@@ -588,4 +588,4 @@ export {
|
|
|
588
588
|
toRecord,
|
|
589
589
|
buildCursorUserPrompt
|
|
590
590
|
};
|
|
591
|
-
//# sourceMappingURL=chunk-
|
|
591
|
+
//# sourceMappingURL=chunk-E74MXZWW.js.map
|
|
@@ -18,8 +18,8 @@ function getDaemonVersion() {
|
|
|
18
18
|
return cached;
|
|
19
19
|
}
|
|
20
20
|
function readDaemonVersion() {
|
|
21
|
-
if ("3.18.
|
|
22
|
-
return "3.18.
|
|
21
|
+
if ("3.18.1".length > 0) {
|
|
22
|
+
return "3.18.1";
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -48,4 +48,4 @@ function readDaemonVersion() {
|
|
|
48
48
|
export {
|
|
49
49
|
getDaemonVersion
|
|
50
50
|
};
|
|
51
|
-
//# sourceMappingURL=chunk-
|
|
51
|
+
//# sourceMappingURL=chunk-K4HUYO2V.js.map
|