@schoolai/shipyard 3.11.0 → 3.11.1-rc.20260611.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.
Files changed (80) hide show
  1. package/README.md +2 -2
  2. package/dist/{auth-GGM253LQ.js → auth-G3IDX4KM.js} +3 -3
  3. package/dist/capability-detector-worker.js +11 -10
  4. package/dist/capability-detector-worker.js.map +1 -1
  5. package/dist/chunk-2WDAOA7J.js +120 -0
  6. package/dist/chunk-2WDAOA7J.js.map +1 -0
  7. package/dist/{chunk-L7ELOV3S.js → chunk-3FGKZCER.js} +4 -4
  8. package/dist/chunk-3FGKZCER.js.map +1 -0
  9. package/dist/chunk-7VGYNBTE.js +62 -0
  10. package/dist/chunk-7VGYNBTE.js.map +1 -0
  11. package/dist/{chunk-ZRJTZLRF.js → chunk-AMIKTEZ4.js} +4 -4
  12. package/dist/{chunk-2EQOL57Z.js → chunk-C7LGBFWM.js} +29 -7
  13. package/dist/chunk-C7LGBFWM.js.map +1 -0
  14. package/dist/{chunk-6LINHACK.js → chunk-DIDCJ7UT.js} +47 -24
  15. package/dist/chunk-DIDCJ7UT.js.map +1 -0
  16. package/dist/{chunk-GM6MH4CD.js → chunk-I5YYBWZ2.js} +2 -2
  17. package/dist/chunk-KHDDLCZA.js +219 -0
  18. package/dist/chunk-KHDDLCZA.js.map +1 -0
  19. package/dist/{chunk-A2UK6TW2.js → chunk-LZSMNUAI.js} +18 -1
  20. package/dist/{chunk-A2UK6TW2.js.map → chunk-LZSMNUAI.js.map} +1 -1
  21. package/dist/{chunk-R3XQ6W7L.js → chunk-NGJ4JZGG.js} +5 -5
  22. package/dist/{chunk-RMLQ5DRP.js → chunk-NV6C52I4.js} +13 -3
  23. package/dist/chunk-NV6C52I4.js.map +1 -0
  24. package/dist/{chunk-C6QOTETH.js → chunk-SDEASQP2.js} +94 -26
  25. package/dist/chunk-SDEASQP2.js.map +1 -0
  26. package/dist/{chunk-Z37T5W6S.js → chunk-SOZW5NUG.js} +29 -8
  27. package/dist/chunk-SOZW5NUG.js.map +1 -0
  28. package/dist/chunk-UNGAIUEX.js +165 -0
  29. package/dist/chunk-UNGAIUEX.js.map +1 -0
  30. package/dist/{chunk-YXPPZQBJ.js → chunk-VYG2YB3N.js} +404 -42
  31. package/dist/{chunk-YXPPZQBJ.js.map → chunk-VYG2YB3N.js.map} +1 -1
  32. package/dist/{chunk-IJHF4OM4.js → chunk-XU5A53JF.js} +2 -2
  33. package/dist/{chunk-RW2OTTUA.js → chunk-Y7CX4WSH.js} +4 -4
  34. package/dist/chunk-Y7CX4WSH.js.map +1 -0
  35. package/dist/{chunk-3WEEGJJN.js → chunk-YZQGITAW.js} +2 -2
  36. package/dist/cursor-runner.js +89 -63
  37. package/dist/cursor-runner.js.map +1 -1
  38. package/dist/electron-utility.js +11 -6
  39. package/dist/electron-utility.js.map +1 -1
  40. package/dist/{git-repo-QNGPCJLI.js → git-repo-4XTTBUKC.js} +6 -4
  41. package/dist/index.js +22 -10
  42. package/dist/index.js.map +1 -1
  43. package/dist/logger-L6TIVBGW.js +23 -0
  44. package/dist/{login-U256OVOJ.js → login-S77RSOTX.js} +7 -7
  45. package/dist/{logout-HY3MPOY5.js → logout-OK7Z4VSK.js} +5 -5
  46. package/dist/{mcp-servers-ICHOWXZB.js → mcp-servers-BKOTGQSZ.js} +4 -4
  47. package/dist/{roi-YM5OOWHG.js → roi-FKC2H2O6.js} +3 -3
  48. package/dist/{serve-D5GKV2RU.js → serve-P2RGDO2X.js} +4330 -1492
  49. package/dist/{serve-D5GKV2RU.js.map → serve-P2RGDO2X.js.map} +1 -1
  50. package/dist/skills-NTIYBI3F.js +13 -0
  51. package/dist/{start-JY26XC5R.js → start-I7JTRLXU.js} +129 -14
  52. package/dist/start-I7JTRLXU.js.map +1 -0
  53. package/package.json +1 -1
  54. package/dist/chunk-2EQOL57Z.js.map +0 -1
  55. package/dist/chunk-6LINHACK.js.map +0 -1
  56. package/dist/chunk-7H34LI75.js +0 -40
  57. package/dist/chunk-7H34LI75.js.map +0 -1
  58. package/dist/chunk-C6QOTETH.js.map +0 -1
  59. package/dist/chunk-L7ELOV3S.js.map +0 -1
  60. package/dist/chunk-QJP7JCIS.js +0 -76
  61. package/dist/chunk-QJP7JCIS.js.map +0 -1
  62. package/dist/chunk-RMLQ5DRP.js.map +0 -1
  63. package/dist/chunk-RW2OTTUA.js.map +0 -1
  64. package/dist/chunk-Z37T5W6S.js.map +0 -1
  65. package/dist/logger-2F3CBS3V.js +0 -19
  66. package/dist/skills-W2Y6TWHA.js +0 -11
  67. package/dist/start-JY26XC5R.js.map +0 -1
  68. /package/dist/{auth-GGM253LQ.js.map → auth-G3IDX4KM.js.map} +0 -0
  69. /package/dist/{chunk-ZRJTZLRF.js.map → chunk-AMIKTEZ4.js.map} +0 -0
  70. /package/dist/{chunk-GM6MH4CD.js.map → chunk-I5YYBWZ2.js.map} +0 -0
  71. /package/dist/{chunk-R3XQ6W7L.js.map → chunk-NGJ4JZGG.js.map} +0 -0
  72. /package/dist/{chunk-IJHF4OM4.js.map → chunk-XU5A53JF.js.map} +0 -0
  73. /package/dist/{chunk-3WEEGJJN.js.map → chunk-YZQGITAW.js.map} +0 -0
  74. /package/dist/{git-repo-QNGPCJLI.js.map → git-repo-4XTTBUKC.js.map} +0 -0
  75. /package/dist/{logger-2F3CBS3V.js.map → logger-L6TIVBGW.js.map} +0 -0
  76. /package/dist/{login-U256OVOJ.js.map → login-S77RSOTX.js.map} +0 -0
  77. /package/dist/{logout-HY3MPOY5.js.map → logout-OK7Z4VSK.js.map} +0 -0
  78. /package/dist/{mcp-servers-ICHOWXZB.js.map → mcp-servers-BKOTGQSZ.js.map} +0 -0
  79. /package/dist/{roi-YM5OOWHG.js.map → roi-FKC2H2O6.js.map} +0 -0
  80. /package/dist/{skills-W2Y6TWHA.js.map → skills-NTIYBI3F.js.map} +0 -0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/shared/capabilities/skills.ts
4
+ import { createHash } from "crypto";
4
5
  import { readdir, readFile, stat } from "fs/promises";
5
6
  import { homedir } from "os";
6
7
  import { dirname, join, parse } from "path";
@@ -82,7 +83,16 @@ async function readCachedSkillFile(skillFile, fallbackName, ctx) {
82
83
  ...parsed,
83
84
  path: skillFile,
84
85
  bodyBytes: st.size,
85
- sourceAgent: sourceAgentForPath(skillFile)
86
+ sourceAgent: sourceAgentForPath(skillFile),
87
+ /**
88
+ * Content-hash dedup key — SAME format as `contentHashForBody` in
89
+ * `apps/daemon/src/services/skills/dedup.ts` (`hash:<sha256>`). Stamped
90
+ * here so `buildSkillCandidates` in the production detection path can pass
91
+ * it through as `body`-equivalent, enabling real content-hash dedup instead
92
+ * of name-based fallback. We replicate the one-liner here rather than
93
+ * importing from `services/skills/` to avoid a shared→services layer cycle.
94
+ */
95
+ contentHash: `hash:${createHash("sha256").update(content).digest("hex")}`
86
96
  };
87
97
  skillFileCache.set(skillFile, { mtimeMs: st.mtimeMs, size: st.size, skill });
88
98
  return cloneSkill(skill);
@@ -229,11 +239,12 @@ function inferPluginName(path) {
229
239
  if (segments.length === 2) return segments[0] ?? null;
230
240
  return segments[1] ?? null;
231
241
  }
232
- async function detectSkills(environments, lastKnown, mirrorEnabled = false) {
242
+ async function detectSkills(environments, lastKnown, options) {
243
+ const resolvedMirror = options?.mirrorEnabled ?? false;
233
244
  try {
234
- return await detectSkillsInner(environments, mirrorEnabled);
245
+ return await detectSkillsInner(environments, resolvedMirror, options?.activeRepoRoot);
235
246
  } catch (err) {
236
- const { logger } = await import("./logger-2F3CBS3V.js");
247
+ const { logger } = await import("./logger-L6TIVBGW.js");
237
248
  if (lastKnown && lastKnown.length > 0) {
238
249
  logger.warn(
239
250
  { err, lastKnownCount: lastKnown.length },
@@ -248,7 +259,16 @@ async function detectSkills(environments, lastKnown, mirrorEnabled = false) {
248
259
  function tag(skills, tier) {
249
260
  return skills.map((s) => ({ ...s, __tier: tier }));
250
261
  }
251
- async function detectSkillsInner(environments, mirrorEnabled) {
262
+ function scopeEnvironmentsToActiveRepo(environments, activeRepoRoot) {
263
+ if (!activeRepoRoot) return environments;
264
+ const root = activeRepoRoot.replace(/\/+$/, "");
265
+ const prefix = `${root}/`;
266
+ return environments.filter((env) => {
267
+ const p = env.path.replace(/\/+$/, "");
268
+ return p === root || p.startsWith(prefix);
269
+ });
270
+ }
271
+ async function detectSkillsInner(environments, mirrorEnabled, activeRepoRoot) {
252
272
  const home = homedir();
253
273
  const ctx = createSkillScanContext();
254
274
  const codexSystemDir = join(home, ".codex", "skills", ".system");
@@ -267,7 +287,8 @@ async function detectSkillsInner(environments, mirrorEnabled) {
267
287
  const projectClaude = [];
268
288
  const projectCodex = [];
269
289
  const projectCursor = [];
270
- for (const env of environments) {
290
+ const scopedEnvironments = scopeEnvironmentsToActiveRepo(environments, activeRepoRoot);
291
+ for (const env of scopedEnvironments) {
271
292
  const [claude, codex, cursor] = await Promise.all([
272
293
  readSkillsFromDir(join(env.path, ".claude", "skills"), ctx),
273
294
  readAncestorAgentsSkills(env.path, ctx),
@@ -404,6 +425,7 @@ var _resolveBucketForTesting = resolveBucket;
404
425
  export {
405
426
  classifySkill,
406
427
  detectSkills,
428
+ scopeEnvironmentsToActiveRepo,
407
429
  _resolveBucketForTesting
408
430
  };
409
- //# sourceMappingURL=chunk-2EQOL57Z.js.map
431
+ //# sourceMappingURL=chunk-C7LGBFWM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/capabilities/skills.ts","../src/shared/capabilities/skill-compat.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join, parse } from 'node:path';\nimport type { GitRepoInfo, SkillInfo } from '@shipyard/session';\n\nimport { classifySkill } from './skill-compat.js';\n\nconst SKILL_SCAN_YIELD_INTERVAL = 16;\n\ninterface SkillFileCacheEntry {\n mtimeMs: number;\n size: number;\n skill: SkillInfo;\n}\n\ninterface SkillScanContext {\n processedSinceYield: number;\n seenSkillFiles: Set<string>;\n}\n\nconst skillFileCache = new Map<string, SkillFileCacheEntry>();\n\nfunction createSkillScanContext(): SkillScanContext {\n return { processedSinceYield: 0, seenSkillFiles: new Set() };\n}\n\nfunction cachedSkillMatchesStat(\n cached: SkillFileCacheEntry | undefined,\n st: { mtimeMs: number; size: number }\n): cached is SkillFileCacheEntry {\n return cached?.mtimeMs === st.mtimeMs && cached.size === st.size;\n}\n\nfunction cloneSkill(skill: SkillInfo): SkillInfo {\n return { ...skill };\n}\n\nasync function yieldToEventLoop(): Promise<void> {\n await new Promise<void>((resolve) => {\n setImmediate(resolve);\n });\n}\n\nasync function noteSkillFileProcessed(ctx: SkillScanContext): Promise<void> {\n ctx.processedSinceYield += 1;\n if (ctx.processedSinceYield < SKILL_SCAN_YIELD_INTERVAL) return;\n ctx.processedSinceYield = 0;\n await yieldToEventLoop();\n}\n\nasync function readCachedSkillFile(\n skillFile: string,\n fallbackName: string,\n ctx: SkillScanContext\n): Promise<SkillInfo> {\n const st = await stat(skillFile);\n ctx.seenSkillFiles.add(skillFile);\n const cached = skillFileCache.get(skillFile);\n if (cachedSkillMatchesStat(cached, st)) {\n return cloneSkill(cached.skill);\n }\n\n const content = await readFile(skillFile, 'utf-8');\n const parsed = parseSkillFrontmatter(content, fallbackName);\n const skill: SkillInfo = {\n ...parsed,\n path: skillFile,\n bodyBytes: st.size,\n sourceAgent: sourceAgentForPath(skillFile),\n /**\n * Content-hash dedup key — SAME format as `contentHashForBody` in\n * `apps/daemon/src/services/skills/dedup.ts` (`hash:<sha256>`). Stamped\n * here so `buildSkillCandidates` in the production detection path can pass\n * it through as `body`-equivalent, enabling real content-hash dedup instead\n * of name-based fallback. We replicate the one-liner here rather than\n * importing from `services/skills/` to avoid a shared→services layer cycle.\n */\n contentHash: `hash:${createHash('sha256').update(content).digest('hex')}`,\n };\n skillFileCache.set(skillFile, { mtimeMs: st.mtimeMs, size: st.size, skill });\n return cloneSkill(skill);\n}\n\nfunction pruneSkillFileCache(seenSkillFiles: Set<string>): void {\n for (const skillFile of skillFileCache.keys()) {\n if (!seenSkillFiles.has(skillFile)) {\n skillFileCache.delete(skillFile);\n }\n }\n}\n\n/**\n * Extract name and description from a SKILL.md YAML frontmatter.\n * Expects `---\\nname: ...\\ndescription: \"...\"\\n---` at the top.\n */\nfunction parseSkillFrontmatter(\n content: string,\n fallbackName: string\n): { name: string; description: string } {\n const match = /^---\\n([\\s\\S]*?)\\n---/.exec(content);\n if (!match?.[1]) return { name: fallbackName, description: '' };\n\n const frontmatter = match[1];\n const nameMatch = /^name:\\s*[\"']?(.+?)[\"']?\\s*$/m.exec(frontmatter);\n const descMatch = /^description:\\s*[\"']?(.+?)[\"']?\\s*$/m.exec(frontmatter);\n\n const name = nameMatch?.[1]?.trim() ?? fallbackName;\n const description = descMatch?.[1]?.trim() ?? '';\n\n return { name, description };\n}\n\n/**\n * Infer the sourceAgent for a skill from its on-disk path.\n *\n * Paths anywhere under `.codex/` or `.agents/` (including plugin caches\n * like `.codex/plugins/cache/<marketplace>/<plugin>/<version>/skills/...`)\n * are tagged 'codex'. Paths anywhere under `.cursor/` or under the\n * Shipyard-owned cursor skills root (`~/.shipyard/cursor-skills/<taskId>/`,\n * written by `cursor-skill-writer.ts`) are tagged 'cursor'. Paths\n * anywhere under `.claude/` are tagged 'claude-code'. Anything else\n * (e.g. project `.claude/skills/` or `.agents/skills/`) is matched by the\n * same fragments. Defaults to 'claude-code' if no marker is found.\n */\nfunction sourceAgentForPath(path: string): 'claude-code' | 'codex' | 'cursor' {\n if (path.includes('/cursor-skills/') || path.includes('/.cursor/')) {\n return 'cursor';\n }\n if (path.includes('/.codex/') || path.includes('/.agents/')) {\n return 'codex';\n }\n return 'claude-code';\n}\n\n/**\n * Read a single skill directory (one level deep). Each child directory\n * must contain a `SKILL.md`. Per-file errors are swallowed (we accept the\n * skill with empty description if SKILL.md is missing/unreadable, to\n * preserve historical behaviour). Captures `bodyBytes` via stat.\n */\nasync function readSkillsFromDir(\n dirPath: string,\n ctx: SkillScanContext,\n skipDirectoryNames = new Set<string>()\n): Promise<SkillInfo[]> {\n try {\n const entries = await readdir(dirPath, { withFileTypes: true });\n const skills: SkillInfo[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (skipDirectoryNames.has(entry.name)) continue;\n const skillDir = join(dirPath, entry.name);\n const skillFile = join(skillDir, 'SKILL.md');\n try {\n skills.push(await readCachedSkillFile(skillFile, entry.name, ctx));\n } catch {\n skills.push({\n name: entry.name,\n description: '',\n sourceAgent: sourceAgentForPath(skillDir),\n });\n }\n await noteSkillFileProcessed(ctx);\n }\n\n return skills;\n } catch {\n return [];\n }\n}\n\n/**\n * Recursively walk a directory looking for SKILL.md files. Used for\n * plugin skill bundles which may nest by category. Capped at the natural\n * filesystem structure — readers swallow per-dir errors.\n */\nasync function readSkillsRecursive(\n dirPath: string,\n ctx: SkillScanContext,\n depth = 0,\n maxDepth = 4\n): Promise<SkillInfo[]> {\n if (depth > maxDepth) return [];\n const skills: SkillInfo[] = [];\n try {\n const entries = await readdir(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const subDir = join(dirPath, entry.name);\n const skillFile = join(subDir, 'SKILL.md');\n try {\n skills.push(await readCachedSkillFile(skillFile, entry.name, ctx));\n } catch {\n const nested = await readSkillsRecursive(subDir, ctx, depth + 1, maxDepth);\n skills.push(...nested);\n }\n await noteSkillFileProcessed(ctx);\n }\n } catch {}\n return skills;\n}\n\n/**\n * Read Claude plugin skills from the installed-plugins manifest.\n * Only enabled plugins contribute skills. Each plugin's `<installPath>/skills`\n * is walked recursively.\n */\nasync function readPluginSkills(ctx: SkillScanContext): Promise<SkillInfo[]> {\n const skills: SkillInfo[] = [];\n try {\n const settingsPath = join(homedir(), '.claude', 'settings.json');\n const settingsRaw = await readFile(settingsPath, 'utf-8');\n // eslint-disable-next-line no-restricted-syntax -- JSON.parse returns unknown; checking enabledPlugins field only\n const settings = JSON.parse(settingsRaw) as { enabledPlugins?: Record<string, boolean> };\n if (!settings.enabledPlugins) return [];\n\n const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');\n const installedRaw = await readFile(installedPath, 'utf-8');\n // eslint-disable-next-line no-restricted-syntax -- JSON.parse returns unknown; checking plugins field only\n const installed = JSON.parse(installedRaw) as {\n plugins?: Record<string, Array<{ installPath?: string }>>;\n };\n if (!installed.plugins) return [];\n\n for (const [pluginId, enabled] of Object.entries(settings.enabledPlugins)) {\n if (!enabled) continue;\n const installs = installed.plugins[pluginId];\n if (!installs || installs.length === 0) continue;\n const installPath = installs[0]?.installPath;\n if (!installPath) continue;\n\n const skillsDir = join(installPath, 'skills');\n const pluginSkills = await readSkillsRecursive(skillsDir, ctx);\n skills.push(...pluginSkills);\n }\n } catch {}\n return skills;\n}\n\n/**\n * Enumerate Codex plugin skill bundles under `~/.codex/plugins/cache/`.\n *\n * Walk one level into the cache root to find plugin dirs, then look for a\n * `skills/` subdir under each. We do NOT recursive-watch this tree (a\n * plugin cache can hold hundreds of files) — this enumeration runs at\n * detect time only. The skills/ subdir itself is read recursively, capped\n * at 2 levels per the plan.\n */\nasync function readCodexPluginCacheSkills(ctx: SkillScanContext): Promise<SkillInfo[]> {\n const cacheRoot = join(homedir(), '.codex', 'plugins', 'cache');\n const out: SkillInfo[] = [];\n try {\n const entries = await readdir(cacheRoot, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const skillsDir = join(cacheRoot, entry.name, 'skills');\n const found = await readSkillsRecursive(skillsDir, ctx, 0, 2);\n out.push(...found);\n }\n } catch {}\n return out;\n}\n\n/**\n * Read `~/.codex/skills/` (deprecated user dir), skipping the `.system`\n * subdirectory — the system bundle is read separately via\n * `readCodexSystemBundle`. Without this skip, every system skill would be\n * surfaced twice.\n */\nasync function readCodexUserSkillsDir(ctx: SkillScanContext): Promise<SkillInfo[]> {\n const dirPath = join(homedir(), '.codex', 'skills');\n return readSkillsFromDir(dirPath, ctx, new Set(['.system']));\n}\n\n/**\n * Walk parent directories from `startDir` to a filesystem boundary\n * looking for a `.agents/skills/` directory. Stops at a git root (file or\n * dir named `.git`) or the filesystem root. Returns skills from the first\n * `.agents/skills/` encountered; nested envs share their nearest ancestor\n * skill set.\n *\n * Pattern mirrors `findProjectRoot` in `apps/daemon/src/shared/fs-utils.ts`.\n */\nasync function readAncestorAgentsSkills(\n startDir: string,\n ctx: SkillScanContext\n): Promise<SkillInfo[]> {\n let dir = startDir;\n for (let i = 0; i < 32; i += 1) {\n const candidate = join(dir, '.agents', 'skills');\n const found = await readSkillsFromDir(candidate, ctx);\n if (found.length > 0) return found;\n /**\n * Stop at git boundary even if no .agents/skills found — don't escape\n * out of the repo into unrelated user/home directories.\n */\n try {\n const gitMarker = await stat(join(dir, '.git'));\n if (gitMarker) return [];\n } catch {}\n const parent = dirname(dir);\n if (parent === dir || parent === parse(dir).root) return [];\n dir = parent;\n }\n return [];\n}\n\n/**\n * Extract the plugin id from a skill path, IF the skill is provided by a\n * plugin. Returns null for user/project/system/pathless cases — those are\n * not plugin-namespaced as far as the agent runtimes are concerned.\n *\n * - `~/.claude/plugins/cache/<plugin-id>/skills/...` → `<plugin-id>`\n * - `~/.claude/plugins/cache/<marketplace>/<plugin-id>/<version>/skills/...` → `<plugin-id>`\n * - `~/.codex/plugins/cache/<marketplace>/<plugin-id>/<version>/skills/...` → `<plugin-id>`\n * - everything else → null\n *\n * The Claude Code SDK's `Options.skills` allowlist accepts bare skill names\n * or `plugin:name` for plugin-provided skills (sdk.d.ts:1629). Codex follows\n * the same convention. Synthetic prefixes like `project:` or `user:` are\n * NOT recognized by either runtime — emitting them produces \"Skill X is\n * not in this session's skills allowlist\" at invocation time. Returning\n * null here means the caller will not prefix the SDK name with a synthetic\n * tag.\n */\nfunction inferPluginName(path: string): string | null {\n const match = /\\/\\.(?:claude|codex)\\/plugins\\/cache\\/(.+?)\\/skills\\//.exec(path);\n if (!match?.[1]) return null;\n\n const segments = match[1].split('/').filter(Boolean);\n if (segments.length === 0) return null;\n if (segments.length === 1) return segments[0] ?? null;\n /** Legacy plugin/version caches still use the first segment as the id. */\n if (segments.length === 2) return segments[0] ?? null;\n\n /**\n * Current plugin caches are marketplace-scoped:\n * cache/<marketplace>/<plugin-id>/<version>/skills/...\n * The runtime allowlist wants `<plugin-id>:<skill>`, not the marketplace\n * owner. Legacy one-segment caches remain supported above.\n */\n return segments[1] ?? null;\n}\n\n/**\n * Options for `detectSkills`. `activeRepoRoot` scopes the per-environment\n * project-skill scan (plan A.2 / skills-holistic-fix §5): instead of walking\n * every worktree under `$HOME` (the 7+3N explosion), only environments under\n * the active repo's root are scanned, collapsing project discovery to\n * 7+3. Absent → legacy behaviour (scan all environments), so the public\n * signature stays backward compatible and existing callers are unaffected.\n */\nexport interface DetectSkillsOptions {\n mirrorEnabled?: boolean;\n /**\n * Absolute path to the active repo root. When set, the per-environment\n * project scan is restricted to environments whose path is that root or a\n * worktree under it. When unset, all environments are scanned (legacy).\n */\n activeRepoRoot?: string;\n}\n\n/**\n * Detect skills across user/plugin/project sources for both agents.\n *\n * `lastKnown` preserves the previously-detected list when this function\n * throws (rare — inner readers swallow errors — but defense in depth).\n * Mirror of the `lastKnownAgents` pattern in `agents.ts`.\n *\n * `options.mirrorEnabled` controls how `classifySkill` computes `bodyResolution`\n * and `compatibleAgents` on the returned skills. Defaults to `false`.\n *\n * `options.activeRepoRoot` scopes the project-skill scan to the active repo —\n * see `DetectSkillsOptions`.\n */\nexport async function detectSkills(\n environments: GitRepoInfo[],\n lastKnown?: SkillInfo[],\n options?: DetectSkillsOptions\n): Promise<SkillInfo[]> {\n const resolvedMirror = options?.mirrorEnabled ?? false;\n try {\n return await detectSkillsInner(environments, resolvedMirror, options?.activeRepoRoot);\n } catch (err) {\n /**\n * Preserve \"always resolves\" contract — inner readers already swallow\n * per-file errors. Never rethrow because the Promise.all in\n * detectCapabilities would tear down the whole refresh for a single\n * slice's failure.\n */\n const { logger } = await import('../logger.js');\n if (lastKnown && lastKnown.length > 0) {\n logger.warn(\n { err, lastKnownCount: lastKnown.length },\n 'detectSkills threw — preserving lastKnown'\n );\n return lastKnown;\n }\n logger.debug({ err }, 'detectSkills threw with no lastKnown — returning []');\n return [];\n }\n}\n\ntype Tier = 'user' | 'plugin' | 'project';\n\ninterface TieredSkill extends SkillInfo {\n __tier: Tier;\n}\n\n/**\n * Re-exported for unit tests in `skills.test.ts`. Underscored to flag\n * that the symbol is part of the test contract, not the public API.\n */\nexport type _TieredSkillForTesting = TieredSkill;\n\nfunction tag(skills: SkillInfo[], tier: Tier): TieredSkill[] {\n return skills.map((s) => ({ ...s, __tier: tier }));\n}\n\n/**\n * Restrict the project-skill scan to the active repo. An environment is in\n * scope when its path equals the active repo root or is a worktree nested under\n * it (prefix match on a path-segment boundary, so `/a/repo` does not match\n * `/a/repo-other`). When `activeRepoRoot` is undefined, returns the input\n * unchanged (legacy scan-all behaviour).\n *\n * Pure + exported for unit tests in `skills.test.ts`.\n */\nexport function scopeEnvironmentsToActiveRepo(\n environments: GitRepoInfo[],\n activeRepoRoot: string | undefined\n): GitRepoInfo[] {\n if (!activeRepoRoot) return environments;\n const root = activeRepoRoot.replace(/\\/+$/, '');\n const prefix = `${root}/`;\n return environments.filter((env) => {\n const p = env.path.replace(/\\/+$/, '');\n return p === root || p.startsWith(prefix);\n });\n}\n\nasync function detectSkillsInner(\n environments: GitRepoInfo[],\n mirrorEnabled: boolean,\n activeRepoRoot?: string\n): Promise<SkillInfo[]> {\n const home = homedir();\n const ctx = createSkillScanContext();\n\n const codexSystemDir = join(home, '.codex', 'skills', '.system');\n const agentsUserDir = join(home, '.agents', 'skills');\n const claudeUserDir = join(home, '.claude', 'skills');\n const cursorUserDir = join(home, '.cursor', 'skills');\n\n const [userClaude, pluginClaude, codexSystem, codexUser, agentsUser, cursorUser, pluginCodex] =\n await Promise.all([\n readSkillsFromDir(claudeUserDir, ctx),\n readPluginSkills(ctx),\n readSkillsFromDir(codexSystemDir, ctx),\n readCodexUserSkillsDir(ctx),\n readSkillsFromDir(agentsUserDir, ctx),\n readSkillsFromDir(cursorUserDir, ctx),\n readCodexPluginCacheSkills(ctx),\n ]);\n\n const projectClaude: SkillInfo[] = [];\n const projectCodex: SkillInfo[] = [];\n const projectCursor: SkillInfo[] = [];\n /*\n * Sequential per-environment scan with event-loop yield. Promise.all over\n * 16+ worktrees saturated libuv's I/O thread pool — each env triggers a\n * 32-deep ancestor walk plus per-skill-dir readFile fan-out. Yielding\n * between envs trades wall-clock for event-loop responsiveness, matching\n * the orphan-pruner.ts:71-73 tradeoff.\n *\n * When `activeRepoRoot` is set (plan A.2), only environments under the active\n * repo are scanned — the 7+3N project explosion collapses to the active\n * repo's own worktrees. User/plugin tiers above are unaffected (still global).\n */\n const scopedEnvironments = scopeEnvironmentsToActiveRepo(environments, activeRepoRoot);\n for (const env of scopedEnvironments) {\n const [claude, codex, cursor] = await Promise.all([\n readSkillsFromDir(join(env.path, '.claude', 'skills'), ctx),\n readAncestorAgentsSkills(env.path, ctx),\n readSkillsFromDir(join(env.path, '.cursor', 'skills'), ctx),\n ]);\n projectClaude.push(...claude);\n projectCodex.push(...codex);\n projectCursor.push(...cursor);\n await yieldToEventLoop();\n }\n\n /**\n * Tier order: user > plugin > project. Within \"user\" we keep both\n * Claude- and Codex-source entries (they live in different dirs and\n * have independent name spaces in the no-collision case).\n */\n const tiered: TieredSkill[] = [\n ...tag(userClaude, 'user'),\n ...tag(codexSystem, 'user'),\n ...tag(codexUser, 'user'),\n ...tag(agentsUser, 'user'),\n ...tag(cursorUser, 'user'),\n ...tag(pluginClaude, 'plugin'),\n ...tag(pluginCodex, 'plugin'),\n ...tag(projectClaude, 'project'),\n ...tag(projectCodex, 'project'),\n ...tag(projectCursor, 'project'),\n ];\n\n /**\n * Bucket candidates by name. Same-name appearances are either:\n * (a) the same skill at different tiers (user > plugin > project) —\n * drop lower tiers without namespacing.\n * (b) different sources at the same effective scope (two plugins,\n * or one Claude + one Codex) — preserve invocable variants.\n */\n const byName = new Map<string, TieredSkill[]>();\n for (const skill of tiered) {\n const bucket = byName.get(skill.name);\n if (bucket) {\n bucket.push(skill);\n } else {\n byName.set(skill.name, [skill]);\n }\n }\n\n const resolved: SkillInfo[] = [];\n for (const [, candidates] of byName) {\n resolved.push(...resolveBucket(candidates));\n }\n\n const out = resolved.map((skill) => {\n const { compatibleAgents, bodyResolution } = classifySkill(skill, mirrorEnabled);\n return { ...skill, compatibleAgents, bodyResolution };\n });\n pruneSkillFileCache(ctx.seenSkillFiles);\n return out;\n}\n\n/**\n * Resolve a single name-bucket into one or more `SkillInfo` entries.\n *\n * - Single candidate → emit as-is.\n * - Multiple candidates with one highest-tier survivor → emit the survivor.\n * - Cross-agent collision after tier collapse → one canonical entry per agent.\n * - Same-agent collision after tier collapse → one canonical entry per SDK-valid plugin id,\n * plus one bare entry for non-plugin candidates.\n */\nfunction resolveBucket(candidates: TieredSkill[]): SkillInfo[] {\n if (candidates.length === 1) {\n const only = candidates[0];\n return only ? [stripTier(only)] : [];\n }\n\n const winners = pickHighestTier(candidates);\n if (winners.length === 1) {\n const w = winners[0];\n return w ? [stripTier(w)] : [];\n }\n\n const sourceAgents = new Set(winners.map((c) => runtimeForSkill(c)));\n if (sourceAgents.size > 1) {\n return resolveCrossAgentCollision(winners);\n }\n\n return resolveSameAgentCollision(winners);\n}\n\ntype RuntimeSkillSource = 'claude-code' | 'codex' | 'cursor';\n\nfunction runtimeForSkill(skill: SkillInfo): RuntimeSkillSource {\n switch (skill.sourceAgent) {\n case 'codex':\n return 'codex';\n case 'cursor':\n return 'cursor';\n default:\n return 'claude-code';\n }\n}\n\n/**\n * Pick the lexicographically smallest path from a list of tiered skills.\n * Stable across different scan orders so the wire shape doesn't flap\n * between daemon restarts. Skills without a `path` (produced by the\n * SKILL.md-read-failure fallback in `readSkillsFromDir`) rank last —\n * a path-bearing entry is always preferred over a pathless one.\n */\nfunction canonicalWinner(candidates: TieredSkill[]): TieredSkill | undefined {\n let best: TieredSkill | undefined;\n for (const c of candidates) {\n if (!best) {\n best = c;\n } else if (!best.path && c.path) {\n best = c;\n } else if (best.path && c.path && c.path < best.path) {\n best = c;\n }\n }\n return best;\n}\n\nfunction resolveCrossAgentCollision(winners: TieredSkill[]): SkillInfo[] {\n const out: SkillInfo[] = [];\n /**\n * Lex-smallest path wins per agent so the choice is stable across scan\n * orders (e.g. different worktree enumeration sequences under Promise.all).\n * Each winner is emitted bare unless it actually lives under a plugin —\n * `sourceAgent` already differentiates the Claude vs Codex variant in the\n * UI, so a synthetic `claude-code:` / `codex:` / `cursor:` prefix would be both\n * redundant and invalid at the SDK boundary.\n */\n const sourceOrder: RuntimeSkillSource[] = ['claude-code', 'codex', 'cursor'];\n for (const source of sourceOrder) {\n const sourceWinners = winners.filter((candidate) => runtimeForSkill(candidate) === source);\n const best = canonicalWinner(sourceWinners);\n if (best) out.push(applyPluginNamespace(stripTier(best)));\n }\n return out;\n}\n\n/**\n * Stamp `namespace` only when the skill lives under a plugin install. The\n * value is the SDK-recognized plugin id (matches `Options.skills` syntax:\n * bare `name` or `plugin:name`). Synthetic categories like 'user' or\n * 'project' are never written here — see `inferPluginName` for why.\n */\nfunction applyPluginNamespace(skill: SkillInfo): SkillInfo {\n if (!skill.path) return skill;\n const pluginId = inferPluginName(skill.path);\n if (pluginId === null) return skill;\n return { ...skill, namespace: pluginId };\n}\n\n/**\n * Dedup same-agent collisions. Two distinct collapse classes:\n *\n * - Plugin collisions: two plugins ship a skill with the same name. The\n * `plugin:name` form disambiguates them on the wire AND at SDK\n * invocation, so we keep one entry per plugin id.\n * - Non-plugin dedup: 700 worktrees each carry the same project-tier\n * skill (e.g. `code-review`) and the naive \"one entry per candidate\"\n * shape ballooned the capability snapshot past the 5 MiB daemon-control\n * channel cap, bricking the composer (#composer-bricked, 2026-05-22).\n * Collapse all of them to the canonical winner; no namespace stamp,\n * because synthetic prefixes like `project:` or `user:` are NOT valid\n * SDK skill identifiers (Claude SDK accepts only bare `name` or\n * `plugin:name` — sdk.d.ts:1629).\n *\n * Canonical winner per bucket = `canonicalWinner()` (lex-smallest path;\n * path-bearing beats pathless). Pathless candidates with no resolvable\n * plugin id collapse into the same non-plugin bucket as path-bearing ones;\n * if every SKILL.md failed to read, the canonical winner is the pathless\n * fallback and the skill remains visible (just without a namespace).\n */\nfunction resolveSameAgentCollision(winners: TieredSkill[]): SkillInfo[] {\n /**\n * Bucket key: the SDK-valid plugin id when the path is plugin-provided,\n * otherwise `null` for everything else (user/project/system/pathless).\n * All non-plugin candidates share the `null` bucket and collapse to one\n * canonical entry.\n */\n const buckets = new Map<string | null, TieredSkill[]>();\n for (const candidate of winners) {\n const pluginId = candidate.path ? inferPluginName(candidate.path) : null;\n const bucket = buckets.get(pluginId);\n if (bucket) {\n bucket.push(candidate);\n } else {\n buckets.set(pluginId, [candidate]);\n }\n }\n\n const out: SkillInfo[] = [];\n for (const [pluginId, candidates] of buckets) {\n const winner = canonicalWinner(candidates);\n if (!winner) continue;\n const base = stripTier(winner);\n out.push(pluginId !== null ? { ...base, namespace: pluginId } : base);\n }\n return out;\n}\n\nfunction pickHighestTier(candidates: TieredSkill[]): TieredSkill[] {\n const order: Record<Tier, number> = { user: 0, plugin: 1, project: 2 };\n let bestRank = Number.POSITIVE_INFINITY;\n for (const c of candidates) {\n const r = order[c.__tier];\n if (r < bestRank) bestRank = r;\n }\n return candidates.filter((c) => order[c.__tier] === bestRank);\n}\n\nfunction stripTier(skill: TieredSkill): SkillInfo {\n const { __tier, ...rest } = skill;\n void __tier;\n return rest;\n}\n\n/**\n * Re-exported for unit tests in `skills.test.ts`. Underscored to flag\n * that the symbol is part of the test contract, not the public API.\n */\nexport const _resolveBucketForTesting = resolveBucket;\n","import type { SkillBodyResolution, SkillCompatibleAgent, SkillInfo } from '@shipyard/session';\n\n/**\n * Classify a detected skill's reach across agent runtimes.\n *\n * Pure function — given a `SkillInfo` and whether the user has the skill\n * mirror enabled, returns the set of agents that can invoke this skill and\n * how the body is reached (native, pass-in, mirror, unreachable).\n *\n * The detector calls this on every surviving skill after dedup and\n * overwrites `compatibleAgents` + `bodyResolution` with the result.\n *\n * Source classification:\n * - Claude-pathed (`/.claude/skills/`) — Claude resolves natively. Codex\n * can reach via `[$name](skill://path)` pass-in (see\n * `codex-rs/core-skills/src/injection.rs`).\n * - Codex-pathed (`/.codex/skills/` or `/.agents/skills/`) — Codex\n * resolves natively. Claude's SDK accepts only names from its own\n * resolver's known set; without mirror, Claude cannot invoke it.\n * - Mirror on for path-backed skills — both runtimes resolve natively\n * because symlinks make every skill present in both indexed directories.\n *\n * TODO: wire `mirrorEnabled` from `skills.mirror.enabled` user pref once\n * step 11 lands (skills-mirror subsystem).\n */\nexport function classifySkill(\n skill: SkillInfo,\n mirrorEnabled: boolean\n): { compatibleAgents: SkillCompatibleAgent[]; bodyResolution: SkillBodyResolution } {\n const path = skill.path;\n if (!path) {\n const fallback: SkillCompatibleAgent =\n skill.sourceAgent === 'codex'\n ? 'codex'\n : skill.sourceAgent === 'cursor'\n ? 'cursor'\n : 'claude-code';\n return {\n compatibleAgents: [fallback],\n bodyResolution: 'unreachable',\n };\n }\n\n if (mirrorEnabled) {\n return {\n compatibleAgents: ['claude-code', 'codex', 'cursor'],\n bodyResolution: 'mirror',\n };\n }\n\n /**\n * Cursor first — `~/.shipyard/cursor-skills/<taskId>/...` is written by\n * the daemon at spawn time; tag those before falling through to codex/claude.\n */\n if (path.includes('/cursor-skills/') || path.includes('/.cursor/')) {\n return {\n compatibleAgents: ['cursor'],\n bodyResolution: 'native',\n };\n }\n\n /**\n * Codex first — `.codex/plugins/cache/.../skills/...` would otherwise be\n * missed because it does not contain `/.codex/skills/`.\n */\n if (path.includes('/.codex/') || path.includes('/.agents/')) {\n return {\n compatibleAgents: ['codex'],\n bodyResolution: 'native',\n };\n }\n\n if (path.includes('/.claude/')) {\n return {\n compatibleAgents: ['claude-code', 'codex', 'cursor'],\n bodyResolution: 'pass-in',\n };\n }\n\n const fallback: SkillCompatibleAgent =\n skill.sourceAgent === 'codex'\n ? 'codex'\n : skill.sourceAgent === 'cursor'\n ? 'cursor'\n : 'claude-code';\n return {\n compatibleAgents: [fallback],\n bodyResolution: 'unreachable',\n };\n}\n"],"mappings":"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","names":["fallback"]}
@@ -7,10 +7,10 @@ import {
7
7
  } from "./chunk-RR6V6SNM.js";
8
8
  import {
9
9
  logger
10
- } from "./chunk-QJP7JCIS.js";
10
+ } from "./chunk-2WDAOA7J.js";
11
11
  import {
12
12
  getShipyardHome
13
- } from "./chunk-Z37T5W6S.js";
13
+ } from "./chunk-SOZW5NUG.js";
14
14
  import {
15
15
  external_exports
16
16
  } from "./chunk-CNR7O5YH.js";
@@ -252,7 +252,7 @@ async function refreshRepoPathsInBackground(log) {
252
252
  const startGen = cacheGeneration;
253
253
  const start = Date.now();
254
254
  try {
255
- const { findGitRepos: findGitRepos2, discoverWorktrees: discoverWorktrees2 } = await import("./git-repo-QNGPCJLI.js");
255
+ const { findGitRepos: findGitRepos2, discoverWorktrees: discoverWorktrees2 } = await import("./git-repo-4XTTBUKC.js");
256
256
  const { homedir: homedir2 } = await import("os");
257
257
  const repos = await findGitRepos2(homedir2());
258
258
  const worktrees = await discoverWorktrees2(repos);
@@ -483,7 +483,8 @@ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
483
483
  ]);
484
484
  var MAX_DEPTH = 4;
485
485
  var WORKTREE_DISCOVERY_CONCURRENCY = 8;
486
- var REPO_METADATA_CONCURRENCY = 16;
486
+ var REPO_METADATA_CONCURRENCY = 4;
487
+ var FIND_GIT_REPOS_DIR_CONCURRENCY = 8;
487
488
  async function mapLimit(items, limit, mapper) {
488
489
  const results = new Array(items.length);
489
490
  const indexedItems = items.map((item, index) => ({ item, index }));
@@ -503,6 +504,9 @@ async function mapLimit(items, limit, mapper) {
503
504
  }
504
505
  async function findGitRepos(dir, depth = 0) {
505
506
  if (depth > MAX_DEPTH) return [];
507
+ await new Promise((r) => {
508
+ setImmediate(r);
509
+ });
506
510
  try {
507
511
  const entries = await readdir(dir, { withFileTypes: true });
508
512
  for (const entry of entries) {
@@ -511,14 +515,18 @@ async function findGitRepos(dir, depth = 0) {
511
515
  }
512
516
  }
513
517
  if (depth >= MAX_DEPTH) return [];
514
- const promises = [];
518
+ const subdirs = [];
515
519
  for (const entry of entries) {
516
520
  if (!entry.isDirectory()) continue;
517
521
  if (entry.name.startsWith(".")) continue;
518
522
  if (EXCLUDE_DIRS.has(entry.name)) continue;
519
- promises.push(findGitRepos(join4(dir, entry.name), depth + 1));
523
+ subdirs.push(join4(dir, entry.name));
520
524
  }
521
- const results = await Promise.all(promises);
525
+ const results = await mapLimit(
526
+ subdirs,
527
+ FIND_GIT_REPOS_DIR_CONCURRENCY,
528
+ (subdir) => findGitRepos(subdir, depth + 1)
529
+ );
522
530
  return results.flat();
523
531
  } catch {
524
532
  return [];
@@ -612,7 +620,18 @@ async function processRepoPaths(path, idx, fingerprints, prevCache, nextCache) {
612
620
  }
613
621
  return info;
614
622
  }
615
- async function detectEnvironments(lastKnown) {
623
+ var inFlightDetect = null;
624
+ function detectEnvironments(lastKnown) {
625
+ if (inFlightDetect) return inFlightDetect;
626
+ const promise = runDetectEnvironments(lastKnown);
627
+ inFlightDetect = promise;
628
+ promise.catch(() => {
629
+ }).finally(() => {
630
+ if (inFlightDetect === promise) inFlightDetect = null;
631
+ });
632
+ return promise;
633
+ }
634
+ async function runDetectEnvironments(lastKnown) {
616
635
  const { homedir: homedir2 } = await import("os");
617
636
  const cachedPaths = getCachedRepoPaths();
618
637
  let repoPaths;
@@ -625,7 +644,7 @@ async function detectEnvironments(lastKnown) {
625
644
  repoPaths = await findGitRepos(homedir2());
626
645
  } catch (err) {
627
646
  if (lastKnown && lastKnown.length > 0) {
628
- const { logger: logger2 } = await import("./logger-2F3CBS3V.js");
647
+ const { logger: logger2 } = await import("./logger-L6TIVBGW.js");
629
648
  logger2.warn(
630
649
  { err, lastKnownCount: lastKnown.length },
631
650
  "detectEnvironments findGitRepos threw \u2014 preserving lastKnown"
@@ -635,7 +654,7 @@ async function detectEnvironments(lastKnown) {
635
654
  return [];
636
655
  }
637
656
  if (repoPaths.length === 0 && lastKnown && lastKnown.length > 0) {
638
- const { logger: logger2 } = await import("./logger-2F3CBS3V.js");
657
+ const { logger: logger2 } = await import("./logger-L6TIVBGW.js");
639
658
  logger2.warn(
640
659
  { lastKnownCount: lastKnown.length },
641
660
  "detectEnvironments walk empty but lastKnown non-empty \u2014 preserving lastKnown"
@@ -688,25 +707,24 @@ async function listLinkedWorktrees(repoPath) {
688
707
  const gitWorktreesDir = join4(repoPath, ".git", "worktrees");
689
708
  let entries;
690
709
  try {
691
- entries = await readdir(gitWorktreesDir);
710
+ const result = await withProbeTimeout(readdir(gitWorktreesDir), GIT_PROBE_TIMEOUT_MS);
711
+ if (result === null) return [];
712
+ entries = result;
692
713
  } catch {
693
714
  return [];
694
715
  }
695
- const paths = await Promise.all(
696
- entries.map(async (name) => {
697
- try {
716
+ const paths = await mapLimit(entries, FIND_GIT_REPOS_DIR_CONCURRENCY, async (name) => {
717
+ const result = await withProbeTimeout(
718
+ (async () => {
698
719
  const gitdirFile = join4(gitWorktreesDir, name, "gitdir");
699
720
  const content = await readFile3(gitdirFile, "utf8");
700
721
  const wtGitPath = content.trim();
701
- if (wtGitPath.endsWith("/.git")) {
702
- return wtGitPath.slice(0, -"/.git".length);
703
- }
704
- return null;
705
- } catch {
706
- return null;
707
- }
708
- })
709
- );
722
+ return wtGitPath.endsWith("/.git") ? wtGitPath.slice(0, -"/.git".length) : null;
723
+ })(),
724
+ GIT_PROBE_TIMEOUT_MS
725
+ );
726
+ return result ?? null;
727
+ });
710
728
  return paths.filter((p) => p !== null && p !== repoPath);
711
729
  }
712
730
  function parseWorktreeListOutput(output, repoPath) {
@@ -729,12 +747,16 @@ var _testing = {
729
747
  GIT_PROBE_TIMEOUT_MS,
730
748
  SLOW_PATH_LOG_THRESHOLD_MS,
731
749
  NEGATIVE_CACHE_TTL_MS,
750
+ FIND_GIT_REPOS_DIR_CONCURRENCY,
732
751
  readGitRepoCache,
733
752
  resetGitRepoCache: () => {
734
753
  gitRepoCache.clear();
735
754
  },
736
755
  resetGhAvailableCache: () => {
737
756
  ghAvailableCache = false;
757
+ },
758
+ resetInFlightDetect: () => {
759
+ inFlightDetect = null;
738
760
  }
739
761
  };
740
762
 
@@ -758,10 +780,11 @@ export {
758
780
  parseOwnerRepo,
759
781
  getRepoDefaultBranch,
760
782
  isAncestor,
783
+ FIND_GIT_REPOS_DIR_CONCURRENCY,
761
784
  findGitRepos,
762
785
  getRepoMetadata,
763
786
  detectEnvironments,
764
787
  discoverWorktrees,
765
788
  _testing
766
789
  };
767
- //# sourceMappingURL=chunk-6LINHACK.js.map
790
+ //# sourceMappingURL=chunk-DIDCJ7UT.js.map
@@ -0,0 +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 {\n getCachedRepoPaths,\n hydrateAndLogRepoPathsCache,\n hydrateRepoPathsCacheFromDisk,\n setCachedRepoPaths,\n invalidateRepoPaths,\n addWorktreeToCache,\n removeWorktreeFromCache,\n};\n\nexport type { HydrateResult };\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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveNodeExecPath
4
- } from "./chunk-Z37T5W6S.js";
4
+ } from "./chunk-SOZW5NUG.js";
5
5
 
6
6
  // src/shared/spawn-as-node.ts
7
7
  var isElectronOverride = null;
@@ -34,4 +34,4 @@ export {
34
34
  buildNodeSpawnEnv,
35
35
  rewriteElectronNodeScriptSpawn
36
36
  };
37
- //# sourceMappingURL=chunk-GM6MH4CD.js.map
37
+ //# sourceMappingURL=chunk-I5YYBWZ2.js.map