@agimon-ai/doompi-team 0.0.1-alpha.37 → 0.0.1-alpha.38

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 (155) hide show
  1. package/README.md +1 -1
  2. package/dist/runs/background/cliRunnerEntry.mjs.map +1 -1
  3. package/dist/runs/piModuleAlias.cjs +2 -0
  4. package/dist/runs/piModuleAlias.cjs.map +1 -0
  5. package/dist/runs/piModuleAlias.d.cts +9 -0
  6. package/dist/runs/piModuleAlias.d.cts.map +1 -0
  7. package/dist/runs/piModuleAlias.d.mts +9 -0
  8. package/dist/runs/piModuleAlias.d.mts.map +1 -0
  9. package/dist/runs/piModuleAlias.mjs +2 -0
  10. package/dist/runs/piModuleAlias.mjs.map +1 -0
  11. package/dist/runs/sdkRunnerEntry.mjs.map +1 -1
  12. package/dist/src/adapters/agents/discovery.mjs.map +1 -1
  13. package/dist/src/adapters/agents/loader.mjs.map +1 -1
  14. package/dist/src/adapters/agents/projectRoot.mjs.map +1 -1
  15. package/dist/src/adapters/agents/skills.mjs.map +1 -1
  16. package/dist/src/adapters/asyncJobTracker.mjs.map +1 -1
  17. package/dist/src/adapters/atomicJson.mjs.map +1 -1
  18. package/dist/src/adapters/filesystem/artifacts.mjs.map +1 -1
  19. package/dist/src/adapters/filesystem/configDir.mjs.map +1 -1
  20. package/dist/src/adapters/filesystem/paths.mjs.map +1 -1
  21. package/dist/src/adapters/intercom/nativeTeamChannel.cjs +4 -4
  22. package/dist/src/adapters/intercom/nativeTeamChannel.mjs +3 -3
  23. package/dist/src/adapters/intercom/nativeTeamChannel.mjs.map +1 -1
  24. package/dist/src/adapters/intercom/supervisorControlChannel.mjs.map +1 -1
  25. package/dist/src/adapters/pi/commands/slash/doctor.mjs.map +1 -1
  26. package/dist/src/adapters/pi/extensions/config.mjs.map +1 -1
  27. package/dist/src/adapters/pi/extensions/fablePlanBridge.mjs.map +1 -1
  28. package/dist/src/adapters/pi/extensions/managementActions.cjs +1 -1
  29. package/dist/src/adapters/pi/extensions/managementActions.mjs +1 -1
  30. package/dist/src/adapters/pi/extensions/managementActions.mjs.map +1 -1
  31. package/dist/src/adapters/pi/extensions/operationJournal.cjs +1 -1
  32. package/dist/src/adapters/pi/extensions/operationJournal.mjs +1 -1
  33. package/dist/src/adapters/pi/extensions/operationJournal.mjs.map +1 -1
  34. package/dist/src/adapters/pi/extensions/spawnPlan.mjs.map +1 -1
  35. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.cjs.map +1 -1
  36. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.d.cts.map +1 -1
  37. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.d.mts.map +1 -1
  38. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.mjs.map +1 -1
  39. package/dist/src/adapters/pi/extensions/subagentTool.cjs +3 -3
  40. package/dist/src/adapters/pi/extensions/subagentTool.mjs +3 -3
  41. package/dist/src/adapters/pi/promptRuntime.cjs +1 -1
  42. package/dist/src/adapters/pi/promptRuntime.mjs +1 -1
  43. package/dist/src/adapters/pi/teamRuntime.cjs +1 -1
  44. package/dist/src/adapters/pi/teamRuntime.mjs +1 -1
  45. package/dist/src/adapters/pi/tui/fleet.mjs.map +1 -1
  46. package/dist/src/adapters/pi/tui/fleetAgentView.mjs.map +1 -1
  47. package/dist/src/adapters/pi/tui/fleetTranscript.mjs.map +1 -1
  48. package/dist/src/adapters/process/childTranscript.mjs.map +1 -1
  49. package/dist/src/adapters/processTerminal.mjs.map +1 -1
  50. package/dist/src/adapters/resultWatcher.mjs.map +1 -1
  51. package/dist/src/adapters/runIdResolver.mjs.map +1 -1
  52. package/dist/src/adapters/runRegistry.mjs.map +1 -1
  53. package/dist/src/adapters/runs/background/asyncExecution.cjs +1 -1
  54. package/dist/src/adapters/runs/background/asyncExecution.cjs.map +1 -1
  55. package/dist/src/adapters/runs/background/asyncExecution.mjs +1 -1
  56. package/dist/src/adapters/runs/background/asyncExecution.mjs.map +1 -1
  57. package/dist/src/adapters/runs/background/runnerBootstrap.mjs.map +1 -1
  58. package/dist/src/adapters/runs/background/runnerReporting.mjs.map +1 -1
  59. package/dist/src/adapters/runs/background/spawnHandshake.mjs.map +1 -1
  60. package/dist/src/adapters/runs/background/statusWriter.mjs.map +1 -1
  61. package/dist/src/adapters/runs/background/terminalPersistence.mjs.map +1 -1
  62. package/dist/src/adapters/runs/shared/claudeFableProfile.mjs.map +1 -1
  63. package/dist/src/adapters/runs/shared/nestedPath.mjs.map +1 -1
  64. package/dist/src/adapters/runs/shared/piArgs.cjs +1 -1
  65. package/dist/src/adapters/runs/shared/piArgs.cjs.map +1 -1
  66. package/dist/src/adapters/runs/shared/piArgs.mjs +1 -1
  67. package/dist/src/adapters/runs/shared/piArgs.mjs.map +1 -1
  68. package/dist/src/adapters/runs/shared/piSpawn.cjs +1 -1
  69. package/dist/src/adapters/runs/shared/piSpawn.cjs.map +1 -1
  70. package/dist/src/adapters/runs/shared/piSpawn.mjs +1 -1
  71. package/dist/src/adapters/runs/shared/piSpawn.mjs.map +1 -1
  72. package/dist/src/adapters/runs/shared/structuredOutput.mjs.map +1 -1
  73. package/dist/src/adapters/scopeOwner.mjs.map +1 -1
  74. package/dist/src/adapters/staleRunReconciler.mjs.map +1 -1
  75. package/dist/src/adapters/statusReader.cjs +1 -1
  76. package/dist/src/adapters/statusReader.cjs.map +1 -1
  77. package/dist/src/adapters/statusReader.mjs +1 -1
  78. package/dist/src/adapters/statusReader.mjs.map +1 -1
  79. package/dist/src/adapters/subagentLaunchContract.mjs.map +1 -1
  80. package/dist/src/adapters/suspendedRuns.mjs.map +1 -1
  81. package/dist/src/adapters/webHubChannels.cjs +2 -0
  82. package/dist/src/adapters/webHubChannels.cjs.map +1 -0
  83. package/dist/src/adapters/webHubChannels.d.cts +7 -0
  84. package/dist/src/adapters/webHubChannels.d.cts.map +1 -0
  85. package/dist/src/adapters/webHubChannels.d.mts +7 -0
  86. package/dist/src/adapters/webHubChannels.d.mts.map +1 -0
  87. package/dist/src/adapters/webHubChannels.mjs +2 -0
  88. package/dist/src/adapters/webHubChannels.mjs.map +1 -0
  89. package/dist/src/adapters/webSubagentCatalogChannel.cjs +2 -0
  90. package/dist/src/adapters/webSubagentCatalogChannel.cjs.map +1 -0
  91. package/dist/src/adapters/webSubagentCatalogChannel.d.cts +19 -0
  92. package/dist/src/adapters/webSubagentCatalogChannel.d.cts.map +1 -0
  93. package/dist/src/adapters/webSubagentCatalogChannel.d.mts +19 -0
  94. package/dist/src/adapters/webSubagentCatalogChannel.d.mts.map +1 -0
  95. package/dist/src/adapters/webSubagentCatalogChannel.mjs +2 -0
  96. package/dist/src/adapters/webSubagentCatalogChannel.mjs.map +1 -0
  97. package/dist/src/adapters/webSubagentWatcher.cjs +2 -0
  98. package/dist/src/adapters/webSubagentWatcher.cjs.map +1 -0
  99. package/dist/src/adapters/webSubagentWatcher.d.cts +17 -0
  100. package/dist/src/adapters/webSubagentWatcher.d.cts.map +1 -0
  101. package/dist/src/adapters/webSubagentWatcher.d.mts +17 -0
  102. package/dist/src/adapters/webSubagentWatcher.d.mts.map +1 -0
  103. package/dist/src/adapters/webSubagentWatcher.mjs +2 -0
  104. package/dist/src/adapters/webSubagentWatcher.mjs.map +1 -0
  105. package/dist/src/adapters/webSubagentsChannel.cjs +2 -0
  106. package/dist/src/adapters/webSubagentsChannel.cjs.map +1 -0
  107. package/dist/src/adapters/webSubagentsChannel.d.cts +15 -0
  108. package/dist/src/adapters/webSubagentsChannel.d.cts.map +1 -0
  109. package/dist/src/adapters/webSubagentsChannel.d.mts +15 -0
  110. package/dist/src/adapters/webSubagentsChannel.d.mts.map +1 -0
  111. package/dist/src/adapters/webSubagentsChannel.mjs +2 -0
  112. package/dist/src/adapters/webSubagentsChannel.mjs.map +1 -0
  113. package/dist/src/services/webSubagentCatalog.cjs +2 -0
  114. package/dist/src/services/webSubagentCatalog.cjs.map +1 -0
  115. package/dist/src/services/webSubagentCatalog.d.cts +19 -0
  116. package/dist/src/services/webSubagentCatalog.d.cts.map +1 -0
  117. package/dist/src/services/webSubagentCatalog.d.mts +19 -0
  118. package/dist/src/services/webSubagentCatalog.d.mts.map +1 -0
  119. package/dist/src/services/webSubagentCatalog.mjs +2 -0
  120. package/dist/src/services/webSubagentCatalog.mjs.map +1 -0
  121. package/dist/src/services/webSubagentRuns.cjs +2 -0
  122. package/dist/src/services/webSubagentRuns.cjs.map +1 -0
  123. package/dist/src/services/webSubagentRuns.mjs +2 -0
  124. package/dist/src/services/webSubagentRuns.mjs.map +1 -0
  125. package/dist/src/types/webSubagents.cjs +2 -0
  126. package/dist/src/types/webSubagents.cjs.map +1 -0
  127. package/dist/src/types/webSubagents.d.cts +43 -0
  128. package/dist/src/types/webSubagents.d.cts.map +1 -0
  129. package/dist/src/types/webSubagents.d.mts +43 -0
  130. package/dist/src/types/webSubagents.d.mts.map +1 -0
  131. package/dist/src/types/webSubagents.mjs +2 -0
  132. package/dist/src/types/webSubagents.mjs.map +1 -0
  133. package/dist/webHub.cjs +1 -0
  134. package/dist/webHub.d.cts +4 -0
  135. package/dist/webHub.d.mts +4 -0
  136. package/dist/webHub.mjs +1 -0
  137. package/package.json +41 -16
  138. package/src/types/webSubagents.ts +74 -0
  139. package/web/AgentCatalogDrawer.tsx +233 -0
  140. package/web/AgentThreadPanel.tsx +85 -0
  141. package/web/AgentsActivitySection.tsx +106 -0
  142. package/web/IntercomToolMessage.tsx +63 -0
  143. package/web/LaunchAgentDialog.tsx +173 -0
  144. package/web/RunControl.tsx +69 -0
  145. package/web/SubagentToolMessage.tsx +68 -0
  146. package/web/SubagentsPanel.tsx +359 -0
  147. package/web/catalogStore.ts +94 -0
  148. package/web/format.ts +21 -0
  149. package/web/index.ts +45 -0
  150. package/web/launchCommand.ts +79 -0
  151. package/web/runActionsSlot.ts +8 -0
  152. package/web/subagentsStore.ts +135 -0
  153. package/web/toolRenderers.ts +13 -0
  154. package/web/toolText.ts +125 -0
  155. package/web/tsconfig.json +22 -0
package/README.md CHANGED
@@ -14,7 +14,7 @@ requests while Task remains the source of truth for task status and dependencies
14
14
 
15
15
  - Node.js 22.19.0 or newer
16
16
  - `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`,
17
- `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` 0.84.2
17
+ `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` 0.84.3
18
18
 
19
19
  ## Install
20
20
 
@@ -1 +1 @@
1
- {"version":3,"file":"cliRunnerEntry.mjs","names":[],"sources":["../../../src/adapters/runs/background/cliRunnerEntry.ts"],"sourcesContent":["/**\n * The detached child process for an external agent CLI.\n *\n * Counterpart to `sdkRunnerEntry.ts`, which does the same job for the\n * in-process Pi runtime. Both are spawned by `AsyncSubagentSpawner`, both write\n * the same `status.json` and the same terminal result, and both signal the same\n * handshake - so `AsyncJobTracker`, `ResultWatcher`, the fleet view, internal\n * completion monitors, and public `subagent` status handling cannot tell them apart.\n *\n * WHAT AN EXTERNAL CHILD DOES NOT GET, STATED PLAINLY:\n * No intercom, no steering, no structured output. Those\n * are delivered by `subagentPromptRuntime.ts`, which Pi loads in-process into\n * a Pi child; an arbitrary CLI has nowhere to load it. Team package tool\n * exclusions are therefore best effort for external runtimes. An external\n * child is a one-shot worker: it receives a prompt and returns output. Stop and interrupt\n * still work, because they are process-group signals rather than cooperative\n * file requests.\n *\n * WHY THE OUTPUT IS RECORDED, NOT PARSED:\n * A generic runner cannot know what a given CLI's output means. Trailing stdout\n * is the result; anything more specific would either be wrong for a CLI nobody\n * tested or lock the config to the two that were.\n *\n * AVOID:\n * - Vendor-specific parsing here. If a runtime needs it, that is a different\n * module and a deliberate decision\n */\n\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { SUBAGENT_ROOT_SESSION_ENV, SUBAGENT_RUN_ID_ENV } from '../../../types/environment';\nimport { writeAtomicJson, writePrivateAtomicJson } from '../../atomicJson';\nimport { adoptSessionScopeFromEnv, currentRunConfigPath } from '../../filesystem/paths';\nimport { CLAUDE_FABLE_PROFILE, cleanupClaudeFableLaunch, parseClaudeFableOutput } from '../shared/claudeFableProfile';\nimport type { AsyncRunStatus } from './asyncExecution';\nimport type { CliLaunchConfig } from './cliLaunchConfig';\nimport { startParentWatchdog } from './parentWatchdog';\nimport { readAsyncRunStatus } from '../../statusReader';\nimport { CoalescedStatusWriter } from './statusWriter';\n\n/** How much trailing stdout is kept as the run's result. */\nconst MAX_RESULT_BYTES = 64 * 1024;\n/** How much stderr is kept for a failure message. */\nconst MAX_STDERR_BYTES = 8 * 1024;\nconst PARENT_LOST_MESSAGE = 'Parent process was lost.';\nconst FORCE_KILL_SIGNAL: NodeJS.Signals = 'SIGKILL';\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction readLaunchConfig(runId: string): CliLaunchConfig {\n const parsed: unknown = JSON.parse(fs.readFileSync(currentRunConfigPath(runId), 'utf8'));\n if (!isRecord(parsed) || parsed.runId !== runId || typeof parsed.command !== 'string') {\n throw new Error(`CLI launch config for run '${runId}' is invalid.`);\n }\n return parsed as unknown as CliLaunchConfig;\n}\n\n/** Keep only a UTF-8-safe trailing value within `limit` bytes, so a chatty CLI cannot exhaust memory. */\nfunction appendBounded(buffer: string, chunk: string, limit: number): string {\n let combined = buffer + chunk;\n while (Buffer.byteLength(combined, 'utf8') > limit && combined.length > 0) {\n const excess = Buffer.byteLength(combined, 'utf8') - limit;\n combined = combined.slice(Math.max(1, Math.ceil(excess / 2)));\n }\n return combined;\n}\n\nlet childTelemetry: DoomTelemetry | undefined;\n\nasync function finishTelemetry(event: string, attributes: Record<string, unknown>, error?: unknown): Promise<void> {\n const telemetry = childTelemetry;\n childTelemetry = undefined;\n if (!telemetry) return;\n if (error === undefined) await telemetry.recordEvent(event, attributes);\n else await telemetry.recordError(event, error, attributes);\n await telemetry.flush();\n await telemetry.shutdown();\n}\n\nasync function run(): Promise<void> {\n const startedAt = Date.now();\n childTelemetry = createDoomTelemetry({\n serviceName: 'doom-team-cli-child',\n packageName: '@agimon-ai/doompi-team',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n if (!runId) throw new Error(`${SUBAGENT_RUN_ID_ENV} is required.`);\n if (!adoptSessionScopeFromEnv()) {\n throw new Error(`${SUBAGENT_ROOT_SESSION_ENV} is required: a child cannot resolve its session scope without it.`);\n }\n\n let config: CliLaunchConfig | undefined;\n try {\n config = readLaunchConfig(runId);\n // The vendor process emits its own spawn event before the ready handshake\n // is written below. Reading the config alone is not sufficient evidence\n // that the configured executable can start.\n } catch (error) {\n if (config?.handshakePath && !fs.existsSync(config.handshakePath)) {\n writeAtomicJson(config.handshakePath, { state: 'error', error: errorMessage(error) });\n }\n throw error;\n }\n\n const statusWriter = new CoalescedStatusWriter<AsyncRunStatus>();\n const now = Date.now();\n await childTelemetry?.recordEvent('doom_team.child_started', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n outcome: 'started',\n });\n statusWriter.open(\n runId,\n readAsyncRunStatus(runId) ?? {\n version: 1,\n runId,\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n cwd: config.cwd,\n runtime: config.runtime,\n state: 'queued',\n startedAt: now,\n lastUpdate: now,\n },\n );\n\n const profileLaunch = config.profile === CLAUDE_FABLE_PROFILE;\n if (config.profile !== undefined && !profileLaunch) throw new Error('Untrusted external profile.');\n if (profileLaunch && (!config.stdinPath || !config.profileResultPath)) {\n throw new Error('Fable profile launch config is incomplete.');\n }\n const stdinFd = profileLaunch ? fs.openSync(config.stdinPath!, 'r') : undefined;\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(config.command, config.args, {\n cwd: config.cwd,\n env: profileLaunch ? config.env : { ...process.env, ...config.env },\n stdio: [stdinFd ?? 'ignore', 'pipe', 'pipe'],\n // Its own group, so the watchdog below can take the CLI and anything it\n // spawned down together.\n detached: true,\n windowsHide: true,\n });\n } catch (error) {\n if (profileLaunch) {\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n }\n throw error;\n } finally {\n if (stdinFd !== undefined) fs.closeSync(stdinFd);\n }\n const cleanupProfile = (removeResult: boolean): void => {\n if (!profileLaunch) return;\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (removeResult && config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n };\n\n let requestedSignal: NodeJS.Signals | undefined;\n const stopVendor = (signal: NodeJS.Signals): void => {\n requestedSignal = signal;\n if (!child.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch (error) {\n process.stderr.write(`[doom-team cli runner] Could not signal vendor process: ${errorMessage(error)}\\n`);\n }\n };\n process.once('SIGTERM', stopVendor);\n process.once('SIGINT', stopVendor);\n\n try {\n await new Promise<void>((resolve, reject) => {\n child.once('spawn', resolve);\n child.once('error', reject);\n });\n writeAtomicJson(config.handshakePath, { state: 'ready' });\n statusWriter.updateSync((status) => {\n status.state = 'running';\n status.activityState = 'working';\n status.lastUpdate = Date.now();\n });\n } catch (error) {\n const message = errorMessage(error);\n writeAtomicJson(config.handshakePath, { state: 'error', error: message });\n statusWriter.updateSync((status) => {\n status.state = 'failed';\n status.error = message;\n status.lastUpdate = Date.now();\n status.endedAt = Date.now();\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: profileLaunch ? 'Fable Claude launch failed.' : message,\n });\n statusWriter.close();\n cleanupProfile(true);\n await finishTelemetry(\n 'doom_team.child_failed',\n { runtime: config.runtime, 'agent.name': config.agent, duration_ms: Date.now() - startedAt, outcome: 'failed' },\n error,\n );\n process.exit(1);\n }\n\n // A SIGKILLed parent cannot signal us, so terminate the vendor tree and\n // persist a terminal record before leaving when the watchdog can observe it.\n const stopWatchdog = startParentWatchdog({\n onParentLost: () => {\n stopVendor(FORCE_KILL_SIGNAL);\n const endedAt = Date.now();\n statusWriter.updateSync((status) => {\n status.state = 'stopped';\n status.error = PARENT_LOST_MESSAGE;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: PARENT_LOST_MESSAGE,\n signal: FORCE_KILL_SIGNAL,\n });\n statusWriter.close();\n cleanupProfile(true);\n void finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'parent_lost',\n }).finally(() => process.exit(0));\n },\n });\n\n let stdout = '';\n let stderr = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout = appendBounded(stdout, chunk.toString('utf8'), MAX_RESULT_BYTES);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr = appendBounded(stderr, chunk.toString('utf8'), MAX_STDERR_BYTES);\n });\n\n const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {\n child.on('error', (error) => resolve({ code: null, signal: null, error }));\n child.on('close', (code, signal) => resolve({ code, signal }));\n });\n\n stopWatchdog();\n\n const stopped = requestedSignal !== undefined;\n let profileError: Error | undefined;\n if (profileLaunch && !stopped && exit.error === undefined && exit.code === 0) {\n try {\n const parsed = parseClaudeFableOutput(stdout);\n writePrivateAtomicJson(config.profileResultPath!, { text: parsed.text, outputBytes: parsed.outputBytes });\n } catch (error) {\n profileError = error instanceof Error ? error : new Error(String(error));\n }\n }\n const success = !stopped && exit.error === undefined && exit.code === 0 && profileError === undefined;\n const failureDetail = profileLaunch\n ? (profileError?.message ?? 'Fable Claude launch failed.')\n : stderr.trim() || `${config.runtime} exited with ${exit.signal ?? `code ${exit.code ?? 'unknown'}`}.`;\n const summary = stopped\n ? `Stopped by ${requestedSignal}.`\n : success\n ? profileLaunch\n ? 'Fable profile completed.'\n : stdout.trim()\n : (exit.error?.message ?? failureDetail);\n const endedAt = Date.now();\n\n statusWriter.updateSync((status) => {\n status.state = stopped ? 'stopped' : success ? 'completed' : 'failed';\n status.activityState = 'finalizing';\n status.summary = summary;\n if (!success) status.error = summary;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success,\n summary,\n ...(exit.code === null ? {} : { exitCode: exit.code }),\n ...(exit.signal ? { signal: exit.signal } : {}),\n });\n\n statusWriter.close();\n cleanupProfile(!success);\n await finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n 'runner.stdout_bytes': Buffer.byteLength(stdout, 'utf8'),\n 'runner.stderr_bytes': Buffer.byteLength(stderr, 'utf8'),\n 'runner.exit_code': exit.code ?? 0,\n outcome: stopped ? 'stopped' : success ? 'completed' : 'failed',\n });\n // The runner exit code reports transport health. Task success or failure is\n // encoded durably in status and result data before this process exits.\n process.exit(0);\n}\n\nvoid run().catch(async (error: unknown) => {\n await finishTelemetry('doom_team.child_failed', { outcome: 'failed' }, error);\n // Keep stdout reserved for vendor output. Stderr is captured in the run's\n // runner log and makes a transport failure diagnosable by the parent.\n process.stderr.write(`[doom-team cli runner] ${errorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cliRunnerEntry.mjs","names":["fs"],"sources":["../../../src/adapters/runs/background/cliRunnerEntry.ts"],"sourcesContent":["/**\n * The detached child process for an external agent CLI.\n *\n * Counterpart to `sdkRunnerEntry.ts`, which does the same job for the\n * in-process Pi runtime. Both are spawned by `AsyncSubagentSpawner`, both write\n * the same `status.json` and the same terminal result, and both signal the same\n * handshake - so `AsyncJobTracker`, `ResultWatcher`, the fleet view, internal\n * completion monitors, and public `subagent` status handling cannot tell them apart.\n *\n * WHAT AN EXTERNAL CHILD DOES NOT GET, STATED PLAINLY:\n * No intercom, no steering, no structured output. Those\n * are delivered by `subagentPromptRuntime.ts`, which Pi loads in-process into\n * a Pi child; an arbitrary CLI has nowhere to load it. Team package tool\n * exclusions are therefore best effort for external runtimes. An external\n * child is a one-shot worker: it receives a prompt and returns output. Stop and interrupt\n * still work, because they are process-group signals rather than cooperative\n * file requests.\n *\n * WHY THE OUTPUT IS RECORDED, NOT PARSED:\n * A generic runner cannot know what a given CLI's output means. Trailing stdout\n * is the result; anything more specific would either be wrong for a CLI nobody\n * tested or lock the config to the two that were.\n *\n * AVOID:\n * - Vendor-specific parsing here. If a runtime needs it, that is a different\n * module and a deliberate decision\n */\n\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { SUBAGENT_ROOT_SESSION_ENV, SUBAGENT_RUN_ID_ENV } from '../../../types/environment';\nimport { writeAtomicJson, writePrivateAtomicJson } from '../../atomicJson';\nimport { adoptSessionScopeFromEnv, currentRunConfigPath } from '../../filesystem/paths';\nimport { CLAUDE_FABLE_PROFILE, cleanupClaudeFableLaunch, parseClaudeFableOutput } from '../shared/claudeFableProfile';\nimport type { AsyncRunStatus } from './asyncExecution';\nimport type { CliLaunchConfig } from './cliLaunchConfig';\nimport { startParentWatchdog } from './parentWatchdog';\nimport { readAsyncRunStatus } from '../../statusReader';\nimport { CoalescedStatusWriter } from './statusWriter';\n\n/** How much trailing stdout is kept as the run's result. */\nconst MAX_RESULT_BYTES = 64 * 1024;\n/** How much stderr is kept for a failure message. */\nconst MAX_STDERR_BYTES = 8 * 1024;\nconst PARENT_LOST_MESSAGE = 'Parent process was lost.';\nconst FORCE_KILL_SIGNAL: NodeJS.Signals = 'SIGKILL';\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction readLaunchConfig(runId: string): CliLaunchConfig {\n const parsed: unknown = JSON.parse(fs.readFileSync(currentRunConfigPath(runId), 'utf8'));\n if (!isRecord(parsed) || parsed.runId !== runId || typeof parsed.command !== 'string') {\n throw new Error(`CLI launch config for run '${runId}' is invalid.`);\n }\n return parsed as unknown as CliLaunchConfig;\n}\n\n/** Keep only a UTF-8-safe trailing value within `limit` bytes, so a chatty CLI cannot exhaust memory. */\nfunction appendBounded(buffer: string, chunk: string, limit: number): string {\n let combined = buffer + chunk;\n while (Buffer.byteLength(combined, 'utf8') > limit && combined.length > 0) {\n const excess = Buffer.byteLength(combined, 'utf8') - limit;\n combined = combined.slice(Math.max(1, Math.ceil(excess / 2)));\n }\n return combined;\n}\n\nlet childTelemetry: DoomTelemetry | undefined;\n\nasync function finishTelemetry(event: string, attributes: Record<string, unknown>, error?: unknown): Promise<void> {\n const telemetry = childTelemetry;\n childTelemetry = undefined;\n if (!telemetry) return;\n if (error === undefined) await telemetry.recordEvent(event, attributes);\n else await telemetry.recordError(event, error, attributes);\n await telemetry.flush();\n await telemetry.shutdown();\n}\n\nasync function run(): Promise<void> {\n const startedAt = Date.now();\n childTelemetry = createDoomTelemetry({\n serviceName: 'doom-team-cli-child',\n packageName: '@agimon-ai/doompi-team',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n if (!runId) throw new Error(`${SUBAGENT_RUN_ID_ENV} is required.`);\n if (!adoptSessionScopeFromEnv()) {\n throw new Error(`${SUBAGENT_ROOT_SESSION_ENV} is required: a child cannot resolve its session scope without it.`);\n }\n\n let config: CliLaunchConfig | undefined;\n try {\n config = readLaunchConfig(runId);\n // The vendor process emits its own spawn event before the ready handshake\n // is written below. Reading the config alone is not sufficient evidence\n // that the configured executable can start.\n } catch (error) {\n if (config?.handshakePath && !fs.existsSync(config.handshakePath)) {\n writeAtomicJson(config.handshakePath, { state: 'error', error: errorMessage(error) });\n }\n throw error;\n }\n\n const statusWriter = new CoalescedStatusWriter<AsyncRunStatus>();\n const now = Date.now();\n await childTelemetry?.recordEvent('doom_team.child_started', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n outcome: 'started',\n });\n statusWriter.open(\n runId,\n readAsyncRunStatus(runId) ?? {\n version: 1,\n runId,\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n cwd: config.cwd,\n runtime: config.runtime,\n state: 'queued',\n startedAt: now,\n lastUpdate: now,\n },\n );\n\n const profileLaunch = config.profile === CLAUDE_FABLE_PROFILE;\n if (config.profile !== undefined && !profileLaunch) throw new Error('Untrusted external profile.');\n if (profileLaunch && (!config.stdinPath || !config.profileResultPath)) {\n throw new Error('Fable profile launch config is incomplete.');\n }\n const stdinFd = profileLaunch ? fs.openSync(config.stdinPath!, 'r') : undefined;\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(config.command, config.args, {\n cwd: config.cwd,\n env: profileLaunch ? config.env : { ...process.env, ...config.env },\n stdio: [stdinFd ?? 'ignore', 'pipe', 'pipe'],\n // Its own group, so the watchdog below can take the CLI and anything it\n // spawned down together.\n detached: true,\n windowsHide: true,\n });\n } catch (error) {\n if (profileLaunch) {\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n }\n throw error;\n } finally {\n if (stdinFd !== undefined) fs.closeSync(stdinFd);\n }\n const cleanupProfile = (removeResult: boolean): void => {\n if (!profileLaunch) return;\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (removeResult && config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n };\n\n let requestedSignal: NodeJS.Signals | undefined;\n const stopVendor = (signal: NodeJS.Signals): void => {\n requestedSignal = signal;\n if (!child.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch (error) {\n process.stderr.write(`[doom-team cli runner] Could not signal vendor process: ${errorMessage(error)}\\n`);\n }\n };\n process.once('SIGTERM', stopVendor);\n process.once('SIGINT', stopVendor);\n\n try {\n await new Promise<void>((resolve, reject) => {\n child.once('spawn', resolve);\n child.once('error', reject);\n });\n writeAtomicJson(config.handshakePath, { state: 'ready' });\n statusWriter.updateSync((status) => {\n status.state = 'running';\n status.activityState = 'working';\n status.lastUpdate = Date.now();\n });\n } catch (error) {\n const message = errorMessage(error);\n writeAtomicJson(config.handshakePath, { state: 'error', error: message });\n statusWriter.updateSync((status) => {\n status.state = 'failed';\n status.error = message;\n status.lastUpdate = Date.now();\n status.endedAt = Date.now();\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: profileLaunch ? 'Fable Claude launch failed.' : message,\n });\n statusWriter.close();\n cleanupProfile(true);\n await finishTelemetry(\n 'doom_team.child_failed',\n { runtime: config.runtime, 'agent.name': config.agent, duration_ms: Date.now() - startedAt, outcome: 'failed' },\n error,\n );\n process.exit(1);\n }\n\n // A SIGKILLed parent cannot signal us, so terminate the vendor tree and\n // persist a terminal record before leaving when the watchdog can observe it.\n const stopWatchdog = startParentWatchdog({\n onParentLost: () => {\n stopVendor(FORCE_KILL_SIGNAL);\n const endedAt = Date.now();\n statusWriter.updateSync((status) => {\n status.state = 'stopped';\n status.error = PARENT_LOST_MESSAGE;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: PARENT_LOST_MESSAGE,\n signal: FORCE_KILL_SIGNAL,\n });\n statusWriter.close();\n cleanupProfile(true);\n void finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'parent_lost',\n }).finally(() => process.exit(0));\n },\n });\n\n let stdout = '';\n let stderr = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout = appendBounded(stdout, chunk.toString('utf8'), MAX_RESULT_BYTES);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr = appendBounded(stderr, chunk.toString('utf8'), MAX_STDERR_BYTES);\n });\n\n const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {\n child.on('error', (error) => resolve({ code: null, signal: null, error }));\n child.on('close', (code, signal) => resolve({ code, signal }));\n });\n\n stopWatchdog();\n\n const stopped = requestedSignal !== undefined;\n let profileError: Error | undefined;\n if (profileLaunch && !stopped && exit.error === undefined && exit.code === 0) {\n try {\n const parsed = parseClaudeFableOutput(stdout);\n writePrivateAtomicJson(config.profileResultPath!, { text: parsed.text, outputBytes: parsed.outputBytes });\n } catch (error) {\n profileError = error instanceof Error ? error : new Error(String(error));\n }\n }\n const success = !stopped && exit.error === undefined && exit.code === 0 && profileError === undefined;\n const failureDetail = profileLaunch\n ? (profileError?.message ?? 'Fable Claude launch failed.')\n : stderr.trim() || `${config.runtime} exited with ${exit.signal ?? `code ${exit.code ?? 'unknown'}`}.`;\n const summary = stopped\n ? `Stopped by ${requestedSignal}.`\n : success\n ? profileLaunch\n ? 'Fable profile completed.'\n : stdout.trim()\n : (exit.error?.message ?? failureDetail);\n const endedAt = Date.now();\n\n statusWriter.updateSync((status) => {\n status.state = stopped ? 'stopped' : success ? 'completed' : 'failed';\n status.activityState = 'finalizing';\n status.summary = summary;\n if (!success) status.error = summary;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success,\n summary,\n ...(exit.code === null ? {} : { exitCode: exit.code }),\n ...(exit.signal ? { signal: exit.signal } : {}),\n });\n\n statusWriter.close();\n cleanupProfile(!success);\n await finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n 'runner.stdout_bytes': Buffer.byteLength(stdout, 'utf8'),\n 'runner.stderr_bytes': Buffer.byteLength(stderr, 'utf8'),\n 'runner.exit_code': exit.code ?? 0,\n outcome: stopped ? 'stopped' : success ? 'completed' : 'failed',\n });\n // The runner exit code reports transport health. Task success or failure is\n // encoded durably in status and result data before this process exits.\n process.exit(0);\n}\n\nvoid run().catch(async (error: unknown) => {\n await finishTelemetry('doom_team.child_failed', { outcome: 'failed' }, error);\n // Keep stdout reserved for vendor output. Stderr is captured in the run's\n // runner log and makes a transport failure diagnosable by the parent.\n process.stderr.write(`[doom-team cli runner] ${errorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/types/environment.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("node:url"),a=require("node:module");const o=`@earendil-works/pi-coding-agent`;function s(e=process.env){let a=e[t.PI_CODING_AGENT_PACKAGE_ROOT_ENV]?.trim();if(!a)return;let o=r.join(a,`package.json`);if(n.existsSync(o))return(0,i.pathToFileURL)(o).href}function c(){let e=s();e!==void 0&&(0,a.registerHooks)({resolve(t,n,r){if(t!==o&&!t.startsWith(`${o}/`))return r(t,n);try{return r(t,n)}catch(i){try{return r(t,{...n,parentURL:e})}catch{throw i}}}})}c(),exports.resolvePiAliasAnchor=s;
2
+ //# sourceMappingURL=piModuleAlias.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piModuleAlias.cjs","names":["PI_CODING_AGENT_PACKAGE_ROOT_ENV","path","fs","pathToFileURL"],"sources":["../../src/adapters/process/piModuleAlias.ts"],"sourcesContent":["/**\n * `--import` preamble for detached SDK child runners.\n *\n * A child spawned as plain `node dist/runs/sdkRunnerEntry.mjs` resolves\n * modules from the team package's own install location. In a managed install\n * (`<project>/.pi/npm/node_modules/@agimon-ai/doompi-team`) the Pi SDK is not\n * present there: the parent process only reaches it because its host aliases\n * the specifier in-process, and that aliasing dies at the process boundary.\n * This hook re-creates it for the child: when normal resolution of the Pi SDK\n * fails, the same specifier is resolved again as if the import sat beside the\n * host's own Pi package, whose root the parent hands over in\n * PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT. Registration is a top-level side\n * effect because `--import` offers no call site.\n */\nimport * as fs from 'node:fs';\nimport { registerHooks } from 'node:module';\nimport * as path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from '../../types/environment';\n\nconst PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent';\nconst PACKAGE_MANIFEST_NAME = 'package.json';\n\n/**\n * The file URL resolution should pretend the Pi import came from, or\n * undefined when the parent provided no usable package root.\n */\nexport function resolvePiAliasAnchor(env: NodeJS.ProcessEnv = process.env): string | undefined {\n const root = env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]?.trim();\n if (!root) return undefined;\n const manifest = path.join(root, PACKAGE_MANIFEST_NAME);\n if (!fs.existsSync(manifest)) return undefined;\n return pathToFileURL(manifest).href;\n}\n\nfunction installPiModuleAlias(): void {\n const anchor = resolvePiAliasAnchor();\n if (anchor === undefined) return;\n registerHooks({\n resolve(specifier, context, nextResolve) {\n if (specifier !== PI_PACKAGE_NAME && !specifier.startsWith(`${PI_PACKAGE_NAME}/`)) {\n return nextResolve(specifier, context);\n }\n try {\n return nextResolve(specifier, context);\n } catch (error) {\n try {\n return nextResolve(specifier, { ...context, parentURL: anchor });\n } catch {\n // The anchored retry is best-effort; the original failure names the\n // import site the reader actually cares about.\n throw error;\n }\n }\n },\n });\n}\n\ninstallPiModuleAlias();\n"],"mappings":"2SAoBA,MAAM,EAAkB,kCAOxB,SAAgB,EAAqB,EAAyB,QAAQ,IAAyB,CAC7F,IAAM,EAAO,EAAIA,EAAAA,iCAAiC,EAAE,KAAK,EACzD,GAAI,CAAC,EAAM,OACX,IAAM,EAAWC,EAAK,KAAK,EAAM,cAAqB,EACjDC,KAAG,WAAW,CAAQ,EAC3B,OAAA,EAAOC,EAAAA,cAAAA,CAAc,CAAQ,CAAC,CAAC,IACjC,CAEA,SAAS,GAA6B,CACpC,IAAM,EAAS,EAAqB,EAChC,IAAW,IAAA,KACf,EAAA,EAAA,cAAA,CAAc,CACZ,QAAQ,EAAW,EAAS,EAAa,CACvC,GAAI,IAAc,GAAmB,CAAC,EAAU,WAAW,GAAG,EAAgB,EAAE,EAC9E,OAAO,EAAY,EAAW,CAAO,EAEvC,GAAI,CACF,OAAO,EAAY,EAAW,CAAO,CACvC,OAAS,EAAO,CACd,GAAI,CACF,OAAO,EAAY,EAAW,CAAE,GAAG,EAAS,UAAW,CAAO,CAAC,CACjE,MAAQ,CAGN,MAAM,CACR,CACF,CACF,CACF,CAAC,CACH,CAEA,EAAqB"}
@@ -0,0 +1,9 @@
1
+ //#region src/adapters/process/piModuleAlias.d.ts
2
+ /**
3
+ * The file URL resolution should pretend the Pi import came from, or
4
+ * undefined when the parent provided no usable package root.
5
+ */
6
+ declare function resolvePiAliasAnchor(env?: NodeJS.ProcessEnv): string | undefined;
7
+ //#endregion
8
+ export { resolvePiAliasAnchor };
9
+ //# sourceMappingURL=piModuleAlias.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piModuleAlias.d.cts","names":[],"sources":["../../src/adapters/process/piModuleAlias.ts"],"mappings":";;;;;iBA2BgB,qBAAqB,MAAK,OAAO"}
@@ -0,0 +1,9 @@
1
+ //#region src/adapters/process/piModuleAlias.d.ts
2
+ /**
3
+ * The file URL resolution should pretend the Pi import came from, or
4
+ * undefined when the parent provided no usable package root.
5
+ */
6
+ declare function resolvePiAliasAnchor(env?: NodeJS.ProcessEnv): string | undefined;
7
+ //#endregion
8
+ export { resolvePiAliasAnchor };
9
+ //# sourceMappingURL=piModuleAlias.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piModuleAlias.d.mts","names":[],"sources":["../../src/adapters/process/piModuleAlias.ts"],"mappings":";;;;;iBA2BgB,qBAAqB,MAAK,OAAO"}
@@ -0,0 +1,2 @@
1
+ import{PI_CODING_AGENT_PACKAGE_ROOT_ENV as e,init_environment as t}from"../src/types/environment.mjs";import{registerHooks as n}from"node:module";import*as r from"node:fs";import*as i from"node:path";import{pathToFileURL as a}from"node:url";t();const o=`@earendil-works/pi-coding-agent`;function s(t=process.env){let n=t[e]?.trim();if(!n)return;let o=i.join(n,`package.json`);if(r.existsSync(o))return a(o).href}function c(){let e=s();e!==void 0&&n({resolve(t,n,r){if(t!==o&&!t.startsWith(`${o}/`))return r(t,n);try{return r(t,n)}catch(i){try{return r(t,{...n,parentURL:e})}catch{throw i}}}})}c();export{s as resolvePiAliasAnchor};
2
+ //# sourceMappingURL=piModuleAlias.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piModuleAlias.mjs","names":["path","fs"],"sources":["../../src/adapters/process/piModuleAlias.ts"],"sourcesContent":["/**\n * `--import` preamble for detached SDK child runners.\n *\n * A child spawned as plain `node dist/runs/sdkRunnerEntry.mjs` resolves\n * modules from the team package's own install location. In a managed install\n * (`<project>/.pi/npm/node_modules/@agimon-ai/doompi-team`) the Pi SDK is not\n * present there: the parent process only reaches it because its host aliases\n * the specifier in-process, and that aliasing dies at the process boundary.\n * This hook re-creates it for the child: when normal resolution of the Pi SDK\n * fails, the same specifier is resolved again as if the import sat beside the\n * host's own Pi package, whose root the parent hands over in\n * PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT. Registration is a top-level side\n * effect because `--import` offers no call site.\n */\nimport * as fs from 'node:fs';\nimport { registerHooks } from 'node:module';\nimport * as path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from '../../types/environment';\n\nconst PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent';\nconst PACKAGE_MANIFEST_NAME = 'package.json';\n\n/**\n * The file URL resolution should pretend the Pi import came from, or\n * undefined when the parent provided no usable package root.\n */\nexport function resolvePiAliasAnchor(env: NodeJS.ProcessEnv = process.env): string | undefined {\n const root = env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]?.trim();\n if (!root) return undefined;\n const manifest = path.join(root, PACKAGE_MANIFEST_NAME);\n if (!fs.existsSync(manifest)) return undefined;\n return pathToFileURL(manifest).href;\n}\n\nfunction installPiModuleAlias(): void {\n const anchor = resolvePiAliasAnchor();\n if (anchor === undefined) return;\n registerHooks({\n resolve(specifier, context, nextResolve) {\n if (specifier !== PI_PACKAGE_NAME && !specifier.startsWith(`${PI_PACKAGE_NAME}/`)) {\n return nextResolve(specifier, context);\n }\n try {\n return nextResolve(specifier, context);\n } catch (error) {\n try {\n return nextResolve(specifier, { ...context, parentURL: anchor });\n } catch {\n // The anchored retry is best-effort; the original failure names the\n // import site the reader actually cares about.\n throw error;\n }\n }\n },\n });\n}\n\ninstallPiModuleAlias();\n"],"mappings":"iPAkB0E,EAAA,EAE1E,MAAM,EAAkB,kCAOxB,SAAgB,EAAqB,EAAyB,QAAQ,IAAyB,CAC7F,IAAM,EAAO,EAAI,EAAiC,EAAE,KAAK,EACzD,GAAI,CAAC,EAAM,OACX,IAAM,EAAWA,EAAK,KAAK,EAAM,cAAqB,EACjDC,KAAG,WAAW,CAAQ,EAC3B,OAAO,EAAc,CAAQ,CAAC,CAAC,IACjC,CAEA,SAAS,GAA6B,CACpC,IAAM,EAAS,EAAqB,EAChC,IAAW,IAAA,IACf,EAAc,CACZ,QAAQ,EAAW,EAAS,EAAa,CACvC,GAAI,IAAc,GAAmB,CAAC,EAAU,WAAW,GAAG,EAAgB,EAAE,EAC9E,OAAO,EAAY,EAAW,CAAO,EAEvC,GAAI,CACF,OAAO,EAAY,EAAW,CAAO,CACvC,OAAS,EAAO,CACd,GAAI,CACF,OAAO,EAAY,EAAW,CAAE,GAAG,EAAS,UAAW,CAAO,CAAC,CACjE,MAAQ,CAGN,MAAM,CACR,CACF,CACF,CACF,CAAC,CACH,CAEA,EAAqB"}
@@ -1 +1 @@
1
- {"version":3,"file":"sdkRunnerEntry.mjs","names":[],"sources":["../../src/adapters/process/sdkRunnerEntry.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport {\n createAgentSessionFromServices,\n createAgentSessionServices,\n getAgentDir,\n resolveCliModel,\n SessionManager,\n} from '@earendil-works/pi-coding-agent';\n\nimport type { LaunchConfig } from '../runs/background/asyncExecution';\nimport { startParentWatchdog } from '../runs/background/parentWatchdog';\nimport { recordNonBlockingExtensionError } from '../../services/runs/extensionErrorTelemetry';\nimport { writeAtomicJson } from '../atomicJson';\nimport { createChildTranscriptWriter } from './childTranscript';\nimport { adoptSessionScopeFromEnv, currentRunConfigPath } from '../filesystem/paths';\nimport { SUBAGENT_ROOT_SESSION_ENV, SUBAGENT_RUN_ID_ENV } from '../../types/environment';\n\nconst SDK_RUNTIME = 'sdk';\nconst CHILD_STARTED_EVENT = 'doom_team.child_started';\nconst CHILD_FINISHED_EVENT = 'doom_team.child_finished';\nconst CHILD_FAILED_EVENT = 'doom_team.child_failed';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction readLaunchConfig(runId: string): LaunchConfig {\n const configPath = currentRunConfigPath(runId);\n const parsed: unknown = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n if (!isRecord(parsed) || parsed.runId !== runId || !isRecord(parsed.sdk)) {\n throw new Error(`Launch config for run '${runId}' is invalid.`);\n }\n return parsed as unknown as LaunchConfig;\n}\n\nfunction createSessionManager(config: LaunchConfig): SessionManager {\n const { cwd, sdk } = config;\n if (sdk.sessionFile) return SessionManager.open(sdk.sessionFile, sdk.sessionDir, cwd);\n if (sdk.sessionEnabled) return SessionManager.create(cwd, sdk.sessionDir);\n return SessionManager.inMemory(cwd);\n}\n\nfunction writeStartupError(config: LaunchConfig | undefined, error: unknown): void {\n const handshakePath = config?.handshakePath;\n if (!handshakePath || fs.existsSync(handshakePath)) return;\n writeAtomicJson(handshakePath, { state: 'error', error: errorMessage(error) });\n}\n\nlet stopWatchdog: (() => void) | undefined;\nlet childTelemetry: DoomTelemetry | undefined;\n\nasync function finishTelemetry(event: string, attributes: Record<string, unknown>, error?: unknown): Promise<void> {\n const telemetry = childTelemetry;\n childTelemetry = undefined;\n if (!telemetry) return;\n if (error === undefined) await telemetry.recordEvent(event, attributes);\n else await telemetry.recordError(event, error, attributes);\n await telemetry.flush();\n await telemetry.shutdown();\n}\n\nexport async function runSdkChild(): Promise<void> {\n const startedAt = Date.now();\n childTelemetry = createDoomTelemetry({\n serviceName: 'doom-team-sdk-child',\n packageName: '@agimon-ai/doompi-team',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n if (!runId) throw new Error(`${SUBAGENT_RUN_ID_ENV} is required.`);\n // Before the launch config is read: its own path is scoped, so a child that\n // was spawned without a scope cannot find anything and should say why.\n if (!adoptSessionScopeFromEnv()) {\n throw new Error(`${SUBAGENT_ROOT_SESSION_ENV} is required: a child cannot resolve its session scope without it.`);\n }\n\n let config: LaunchConfig | undefined;\n try {\n config = readLaunchConfig(runId);\n const launchConfig = config;\n await childTelemetry?.recordEvent(CHILD_STARTED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n ...(launchConfig.sdk.model ? { 'model.id': launchConfig.sdk.model } : {}),\n outcome: 'started',\n });\n const { sdk } = launchConfig;\n const agentDir = getAgentDir();\n const services = await createAgentSessionServices({\n cwd: config.cwd,\n agentDir,\n resourceLoaderOptions: {\n additionalExtensionPaths: sdk.extensions,\n additionalSkillPaths: sdk.skillPaths,\n noExtensions: sdk.noAmbientExtensions,\n noSkills: sdk.noSkills,\n noContextFiles: sdk.noContextFiles,\n ...(sdk.systemPromptMode === 'replace' ? { systemPrompt: sdk.systemPrompt } : {}),\n ...(sdk.systemPrompt && sdk.systemPromptMode !== 'replace' ? { appendSystemPrompt: [sdk.systemPrompt] } : {}),\n },\n });\n const serviceError = services.diagnostics.find((diagnostic) => diagnostic.type === 'error');\n if (serviceError) throw new Error(serviceError.message);\n\n const resolved = sdk.model\n ? resolveCliModel({ cliModel: sdk.model, modelRuntime: services.modelRuntime })\n : undefined;\n if (resolved && !resolved.model) throw new Error(resolved.error ?? `Model '${sdk.model}' was not found.`);\n\n const { session } = await createAgentSessionFromServices({\n services,\n sessionManager: createSessionManager(config),\n ...(resolved?.model ? { model: resolved.model } : {}),\n ...(resolved?.thinkingLevel ? { thinkingLevel: resolved.thinkingLevel } : {}),\n ...(sdk.tools ? { tools: sdk.tools } : {}),\n ...(sdk.noTools ? { noTools: sdk.noTools } : {}),\n ...(sdk.excludeTools ? { excludeTools: sdk.excludeTools } : {}),\n });\n // The parent cannot signal us if it was SIGKILLed, and we are in our own\n // process group so its terminal cannot either. This is the only thing that\n // ends this run in that case.\n stopWatchdog = startParentWatchdog({\n onParentLost: () => {\n session.dispose();\n void finishTelemetry(CHILD_FINISHED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'parent_lost',\n }).finally(() => process.exit(0));\n },\n });\n\n const transcript = config.transcriptPath\n ? createChildTranscriptWriter({\n transcriptPath: config.transcriptPath,\n source: 'async',\n runId: config.runId,\n agent: config.agent,\n childIndex: config.childIndex,\n cwd: config.cwd,\n })\n : undefined;\n transcript?.writeInitialUserMessage(config.task);\n const unsubscribe = session.subscribe((event) => {\n transcript?.writeChildEvent(event);\n if (event.type !== 'agent_end') return;\n // Pi emits session listeners only after awaiting every extension's\n // agent_end handler, including Doom Team's terminal persistence. Defer\n // one event-loop turn so every listener observes the event, then end\n // this dedicated one-session process unless an agent_end handler queued\n // a nudge or follow-up that Pi still needs to run.\n setImmediate(() => {\n if (session.agent.hasQueuedMessages()) return;\n session.dispose();\n void finishTelemetry(CHILD_FINISHED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'completed',\n }).finally(() => process.exit(0));\n });\n });\n\n try {\n await session.bindExtensions({\n mode: 'print',\n onError: (failure) => {\n recordNonBlockingExtensionError(childTelemetry, failure);\n },\n });\n await session.prompt(config.task);\n } finally {\n stopWatchdog?.();\n unsubscribe();\n session.dispose();\n await finishTelemetry(CHILD_FINISHED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'completed',\n });\n }\n } catch (error) {\n writeStartupError(config, error);\n await finishTelemetry(CHILD_FAILED_EVENT, { outcome: 'failed' }, error);\n throw error;\n }\n}\n\nvoid runSdkChild().then(\n () => {\n // This entry point owns a dedicated detached process for exactly one SDK\n // session. Extensions may retain polling intervals after session.dispose(),\n // so a successful run must terminate explicitly once synchronous result\n // persistence and session cleanup have completed.\n process.exit(0);\n },\n async (error: unknown) => {\n await finishTelemetry(CHILD_FAILED_EVENT, { outcome: 'failed' }, error);\n const failure = error instanceof Error ? error : new Error('Doom Team SDK child failed', { cause: error });\n setImmediate(() => {\n throw failure;\n });\n },\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"sdkRunnerEntry.mjs","names":["fs"],"sources":["../../src/adapters/process/sdkRunnerEntry.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport {\n createAgentSessionFromServices,\n createAgentSessionServices,\n getAgentDir,\n resolveCliModel,\n SessionManager,\n} from '@earendil-works/pi-coding-agent';\n\nimport type { LaunchConfig } from '../runs/background/asyncExecution';\nimport { startParentWatchdog } from '../runs/background/parentWatchdog';\nimport { recordNonBlockingExtensionError } from '../../services/runs/extensionErrorTelemetry';\nimport { writeAtomicJson } from '../atomicJson';\nimport { createChildTranscriptWriter } from './childTranscript';\nimport { adoptSessionScopeFromEnv, currentRunConfigPath } from '../filesystem/paths';\nimport { SUBAGENT_ROOT_SESSION_ENV, SUBAGENT_RUN_ID_ENV } from '../../types/environment';\n\nconst SDK_RUNTIME = 'sdk';\nconst CHILD_STARTED_EVENT = 'doom_team.child_started';\nconst CHILD_FINISHED_EVENT = 'doom_team.child_finished';\nconst CHILD_FAILED_EVENT = 'doom_team.child_failed';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction readLaunchConfig(runId: string): LaunchConfig {\n const configPath = currentRunConfigPath(runId);\n const parsed: unknown = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n if (!isRecord(parsed) || parsed.runId !== runId || !isRecord(parsed.sdk)) {\n throw new Error(`Launch config for run '${runId}' is invalid.`);\n }\n return parsed as unknown as LaunchConfig;\n}\n\nfunction createSessionManager(config: LaunchConfig): SessionManager {\n const { cwd, sdk } = config;\n if (sdk.sessionFile) return SessionManager.open(sdk.sessionFile, sdk.sessionDir, cwd);\n if (sdk.sessionEnabled) return SessionManager.create(cwd, sdk.sessionDir);\n return SessionManager.inMemory(cwd);\n}\n\nfunction writeStartupError(config: LaunchConfig | undefined, error: unknown): void {\n const handshakePath = config?.handshakePath;\n if (!handshakePath || fs.existsSync(handshakePath)) return;\n writeAtomicJson(handshakePath, { state: 'error', error: errorMessage(error) });\n}\n\nlet stopWatchdog: (() => void) | undefined;\nlet childTelemetry: DoomTelemetry | undefined;\n\nasync function finishTelemetry(event: string, attributes: Record<string, unknown>, error?: unknown): Promise<void> {\n const telemetry = childTelemetry;\n childTelemetry = undefined;\n if (!telemetry) return;\n if (error === undefined) await telemetry.recordEvent(event, attributes);\n else await telemetry.recordError(event, error, attributes);\n await telemetry.flush();\n await telemetry.shutdown();\n}\n\nexport async function runSdkChild(): Promise<void> {\n const startedAt = Date.now();\n childTelemetry = createDoomTelemetry({\n serviceName: 'doom-team-sdk-child',\n packageName: '@agimon-ai/doompi-team',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n if (!runId) throw new Error(`${SUBAGENT_RUN_ID_ENV} is required.`);\n // Before the launch config is read: its own path is scoped, so a child that\n // was spawned without a scope cannot find anything and should say why.\n if (!adoptSessionScopeFromEnv()) {\n throw new Error(`${SUBAGENT_ROOT_SESSION_ENV} is required: a child cannot resolve its session scope without it.`);\n }\n\n let config: LaunchConfig | undefined;\n try {\n config = readLaunchConfig(runId);\n const launchConfig = config;\n await childTelemetry?.recordEvent(CHILD_STARTED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n ...(launchConfig.sdk.model ? { 'model.id': launchConfig.sdk.model } : {}),\n outcome: 'started',\n });\n const { sdk } = launchConfig;\n const agentDir = getAgentDir();\n const services = await createAgentSessionServices({\n cwd: config.cwd,\n agentDir,\n resourceLoaderOptions: {\n additionalExtensionPaths: sdk.extensions,\n additionalSkillPaths: sdk.skillPaths,\n noExtensions: sdk.noAmbientExtensions,\n noSkills: sdk.noSkills,\n noContextFiles: sdk.noContextFiles,\n ...(sdk.systemPromptMode === 'replace' ? { systemPrompt: sdk.systemPrompt } : {}),\n ...(sdk.systemPrompt && sdk.systemPromptMode !== 'replace' ? { appendSystemPrompt: [sdk.systemPrompt] } : {}),\n },\n });\n const serviceError = services.diagnostics.find((diagnostic) => diagnostic.type === 'error');\n if (serviceError) throw new Error(serviceError.message);\n\n const resolved = sdk.model\n ? resolveCliModel({ cliModel: sdk.model, modelRuntime: services.modelRuntime })\n : undefined;\n if (resolved && !resolved.model) throw new Error(resolved.error ?? `Model '${sdk.model}' was not found.`);\n\n const { session } = await createAgentSessionFromServices({\n services,\n sessionManager: createSessionManager(config),\n ...(resolved?.model ? { model: resolved.model } : {}),\n ...(resolved?.thinkingLevel ? { thinkingLevel: resolved.thinkingLevel } : {}),\n ...(sdk.tools ? { tools: sdk.tools } : {}),\n ...(sdk.noTools ? { noTools: sdk.noTools } : {}),\n ...(sdk.excludeTools ? { excludeTools: sdk.excludeTools } : {}),\n });\n // The parent cannot signal us if it was SIGKILLed, and we are in our own\n // process group so its terminal cannot either. This is the only thing that\n // ends this run in that case.\n stopWatchdog = startParentWatchdog({\n onParentLost: () => {\n session.dispose();\n void finishTelemetry(CHILD_FINISHED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'parent_lost',\n }).finally(() => process.exit(0));\n },\n });\n\n const transcript = config.transcriptPath\n ? createChildTranscriptWriter({\n transcriptPath: config.transcriptPath,\n source: 'async',\n runId: config.runId,\n agent: config.agent,\n childIndex: config.childIndex,\n cwd: config.cwd,\n })\n : undefined;\n transcript?.writeInitialUserMessage(config.task);\n const unsubscribe = session.subscribe((event) => {\n transcript?.writeChildEvent(event);\n if (event.type !== 'agent_end') return;\n // Pi emits session listeners only after awaiting every extension's\n // agent_end handler, including Doom Team's terminal persistence. Defer\n // one event-loop turn so every listener observes the event, then end\n // this dedicated one-session process unless an agent_end handler queued\n // a nudge or follow-up that Pi still needs to run.\n setImmediate(() => {\n if (session.agent.hasQueuedMessages()) return;\n session.dispose();\n void finishTelemetry(CHILD_FINISHED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'completed',\n }).finally(() => process.exit(0));\n });\n });\n\n try {\n await session.bindExtensions({\n mode: 'print',\n onError: (failure) => {\n recordNonBlockingExtensionError(childTelemetry, failure);\n },\n });\n await session.prompt(config.task);\n } finally {\n stopWatchdog?.();\n unsubscribe();\n session.dispose();\n await finishTelemetry(CHILD_FINISHED_EVENT, {\n runtime: SDK_RUNTIME,\n 'agent.name': launchConfig.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'completed',\n });\n }\n } catch (error) {\n writeStartupError(config, error);\n await finishTelemetry(CHILD_FAILED_EVENT, { outcome: 'failed' }, error);\n throw error;\n }\n}\n\nvoid runSdkChild().then(\n () => {\n // This entry point owns a dedicated detached process for exactly one SDK\n // session. Extensions may retain polling intervals after session.dispose(),\n // so a successful run must terminate explicitly once synchronous result\n // persistence and session cleanup have completed.\n process.exit(0);\n },\n async (error: unknown) => {\n await finishTelemetry(CHILD_FAILED_EVENT, { outcome: 'failed' }, error);\n const failure = error instanceof Error ? error : new Error('Doom Team SDK child failed', { cause: error });\n setImmediate(() => {\n throw failure;\n });\n },\n);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.mjs","names":[],"sources":["../../../../src/adapters/agents/discovery.ts"],"sourcesContent":["/**\n * Cached discovery of the agents visible from a working directory.\n *\n * WHY THIS IS A SERVICE AND NOT A FUNCTION:\n * Discovery reads two settings files and walks every agent directory in scope,\n * parsing the frontmatter of every file it finds. The slash-command completion\n * path asks for the agent list on each keystroke, and preflight asks again for\n * each launch, so the predecessor's plain function re-walked the tree many times\n * per second. Caching needs somewhere to live, and invalidation needs an owner.\n *\n * DESIGN PATTERNS:\n * - Two-layer validity. A short TTL bounds how stale an answer can be, and a\n * cheap source fingerprint catches the common edits sooner than the TTL would\n * - Bounded by an LRU keyed on cwd and scope, so a long session that visits many\n * directories cannot grow the cache without limit\n * - Mutations invalidate explicitly rather than relying on the TTL, so an agent\n * the user just created is visible immediately\n *\n * WHY THE FINGERPRINT IS NOT ENOUGH ON ITS OWN:\n * It stats the settings files and the agent directories, not every agent file.\n * A directory's mtime moves when a file is added or removed but not when an\n * existing file's contents change, so an in-place edit is caught by the TTL\n * rather than by the fingerprint. Stat'ing every agent file on every keystroke\n * would reintroduce the cost this cache exists to remove.\n *\n * AVOID:\n * - Handing out the cached array without copying it; a caller that sorts or\n * splices in place would corrupt every later reader\n * - Raising the TTL to hide a missing invalidate call\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { getHarnessState, loadMajorModesConfig, resolvePackageConfigurations } from '@agimon-ai/doompi-config';\n\nimport {\n DOOMPI_TEAM_PACKAGE,\n mergeTeamPackageConfigurations,\n type TeamPackageConfig,\n} from '../../schemas/team/packageConfig';\nimport { LruCache } from '../../services/support/lruCache';\nimport { loadAgentsFromDir, pluginAgentDirs } from './loader';\nimport {\n canonicalizeDiscoveryCwd,\n getProjectAgentSettingsPath,\n getUserAgentSettingsPath,\n resolveNearestProjectAgentDirs,\n userAgentDirs,\n} from './projectRoot';\nimport {\n applyCustomAgentOverrides,\n applySubagentDefaults,\n EMPTY_SUBAGENT_SETTINGS,\n readSubagentSettings,\n resolveSubagentDefaultExtensions,\n resolveSubagentDefaultModel,\n resolveSubagentDefaultThinking,\n} from './settings';\nimport type { AgentConfig, AgentDiscoveryResult, AgentScope, AgentDiscoveryContract, SubagentSettings } from './types';\n\n/** How long a discovery result may be reused without revalidation. */\nconst DISCOVERY_TTL_MS = 5_000;\n\n/**\n * How many (cwd, scope) combinations stay cached.\n *\n * A session works in a handful of directories and there are three scopes, so\n * this is far above real usage; it exists to bound a pathological case.\n */\nconst MAX_CACHED_DISCOVERIES = 64;\n\nconst FINGERPRINT_FIELD_SEPARATOR = ':';\nconst FINGERPRINT_ENTRY_SEPARATOR = '|';\nconst MISSING_SOURCE_MARKER = '-';\nconst MAJOR_MODES_CONFIG_RELATIVE_PATH = path.join('.doom', 'modes.yaml');\nconst THINKING_SUFFIX = /:(?:off|minimal|low|medium|high|xhigh|max)$/;\n\ninterface CacheEntry {\n result: AgentDiscoveryResult;\n expiresAt: number;\n fingerprint: string;\n}\n\n/**\n * Merge the sources into one list, later sources shadowing earlier ones.\n *\n * Precedence is plugin, then user, then project: the definition closest to the\n * work being done wins, which is what lets a repository pin an agent that the\n * user has also defined globally.\n */\nexport function mergeAgentsForScope(\n scope: AgentScope,\n userAgents: AgentConfig[],\n projectAgents: AgentConfig[],\n pluginAgents: AgentConfig[] = [],\n): AgentConfig[] {\n const byName = new Map<string, AgentConfig>();\n for (const agent of pluginAgents) byName.set(agent.name, agent);\n if (scope !== 'project') {\n for (const agent of userAgents) byName.set(agent.name, agent);\n }\n if (scope !== 'user') {\n for (const agent of projectAgents) byName.set(agent.name, agent);\n }\n return [...byName.values()];\n}\n\n/** Size and mtime of one path, or a marker when it does not exist. */\nfunction stampOf(target: string): string {\n try {\n const stat = fs.statSync(target);\n return `${target}${FINGERPRINT_FIELD_SEPARATOR}${stat.mtimeMs}${FINGERPRINT_FIELD_SEPARATOR}${stat.size}`;\n } catch {\n // A missing settings file or agent directory is a normal state, and it has\n // to be distinguishable from a present one so that creating it invalidates.\n return `${target}${FINGERPRINT_FIELD_SEPARATOR}${MISSING_SOURCE_MARKER}`;\n }\n}\n\nexport interface ActiveTeamPackageConfig {\n config: TeamPackageConfig;\n path: string;\n}\n\n/** Resolve Team-owned package configuration from the current parent harness state. */\nexport function resolveActiveTeamPackageConfig(): ActiveTeamPackageConfig | undefined {\n const harness = getHarnessState();\n if (!harness.root) return undefined;\n const layers = loadMajorModesConfig(harness.root);\n const entries = resolvePackageConfigurations(layers, harness.layers, DOOMPI_TEAM_PACKAGE);\n const config = mergeTeamPackageConfigurations(entries);\n return config ? { config, path: path.join(harness.root, MAJOR_MODES_CONFIG_RELATIVE_PATH) } : undefined;\n}\n\nfunction teamModelSpecs(config: TeamPackageConfig | undefined): string[] | undefined {\n if (!config?.models) return undefined;\n return config.models.map(({ model, thinking }) =>\n thinking ? `${model.replace(THINKING_SUFFIX, '')}:${thinking}` : model,\n );\n}\n\n/** Resolve Team's ordered package model policy without merging it into an agent definition. */\nexport function resolveActiveTeamModelSpecs(): string[] | undefined {\n return teamModelSpecs(resolveActiveTeamPackageConfig()?.config);\n}\n\nexport class AgentDiscoveryService implements AgentDiscoveryContract {\n private readonly cache = new LruCache<string, CacheEntry>(MAX_CACHED_DISCOVERIES);\n\n /**\n * Seams for tests, as protected members rather than constructor arguments.\n *\n * Runtime tuning is not part of this service's dependency contract. A test\n * subclass overrides these values without abstracting the clock.\n */\n protected readonly ttlMs: number = DISCOVERY_TTL_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n discover(cwd: string, scope: AgentScope): AgentDiscoveryResult {\n const resolvedCwd = path.resolve(cwd);\n const canonicalCwd = canonicalizeDiscoveryCwd(resolvedCwd);\n const key = `${scope}${FINGERPRINT_ENTRY_SEPARATOR}${canonicalCwd}`;\n const cached = this.cache.get(key);\n const timestamp = this.now();\n\n if (cached && cached.expiresAt > timestamp && cached.fingerprint === this.fingerprint(canonicalCwd)) {\n return cloneResult(cached.result);\n }\n\n const result = this.load(resolvedCwd, scope);\n this.cache.set(key, {\n result,\n expiresAt: timestamp + this.ttlMs,\n fingerprint: this.fingerprint(canonicalCwd),\n });\n return cloneResult(result);\n }\n\n find(cwd: string, scope: AgentScope, name: string): AgentConfig | undefined {\n return this.discover(cwd, scope).agents.find((agent) => agent.name === name);\n }\n\n invalidate(): void {\n this.cache.clear();\n }\n\n /**\n * A cheap stamp of everything discovery reads at the directory level.\n *\n * Deliberately independent of scope: a user-scoped and a project-scoped lookup\n * from the same directory read overlapping sources, and computing one stamp\n * keeps the stat count fixed regardless of how many scopes are queried.\n */\n private fingerprint(cwd: string): string {\n const harness = getHarnessState();\n const sources = [\n getUserAgentSettingsPath(),\n getProjectAgentSettingsPath(cwd) ?? MISSING_SOURCE_MARKER,\n ...userAgentDirs(),\n ...resolveNearestProjectAgentDirs(cwd).readDirs,\n ...pluginAgentDirs(),\n ];\n if (harness.root) sources.push(path.join(harness.root, MAJOR_MODES_CONFIG_RELATIVE_PATH));\n return [...sources.map(stampOf), `active-layers${FINGERPRINT_FIELD_SEPARATOR}${harness.layers.join(',')}`].join(\n FINGERPRINT_ENTRY_SEPARATOR,\n );\n }\n\n /**\n * The uncached read. Protected so a test can count how often it actually runs,\n * which is the only direct way to assert that the cache is doing its job.\n */\n protected load(cwd: string, scope: AgentScope): AgentDiscoveryResult {\n const userSettingsPath = getUserAgentSettingsPath();\n const projectSettingsPath = getProjectAgentSettingsPath(cwd);\n const userSettings: SubagentSettings =\n scope === 'project' ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);\n const projectSettings: SubagentSettings =\n scope === 'user' ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);\n\n const legacyDefaultModel = resolveSubagentDefaultModel(\n userSettings,\n projectSettings,\n userSettingsPath,\n projectSettingsPath,\n );\n const teamPackage = resolveActiveTeamPackageConfig();\n const packageModels = teamModelSpecs(teamPackage?.config);\n const packagePrimaryModel = packageModels?.[0];\n const defaultModel =\n packagePrimaryModel && teamPackage\n ? {\n type: 'packages.team.defaultModel' as const,\n scope: 'package' as const,\n path: teamPackage.path,\n model: packagePrimaryModel,\n }\n : legacyDefaultModel;\n const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);\n const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);\n\n const resolve = (agents: AgentConfig[]): AgentConfig[] =>\n applyCustomAgentOverrides(\n applySubagentDefaults(agents, defaultModel, defaultThinking, defaultExtensions),\n userSettings,\n projectSettings,\n userSettingsPath,\n projectSettingsPath,\n );\n\n const plugin = resolve(pluginAgentDirs().flatMap((directory) => loadAgentsFromDir(directory, 'plugin')));\n const user =\n scope === 'project' ? [] : resolve(userAgentDirs().flatMap((directory) => loadAgentsFromDir(directory, 'user')));\n const project =\n scope === 'user'\n ? []\n : resolve(\n resolveNearestProjectAgentDirs(cwd).readDirs.flatMap((directory) =>\n loadAgentsFromDir(directory, 'project'),\n ),\n );\n\n return {\n // A disabled agent is dropped last, so that an override which re-enables\n // one has already been applied by the time this runs.\n agents: mergeAgentsForScope(scope, user, project, plugin).filter((agent) => agent.disabled !== true),\n projectAgentsDir: resolveNearestProjectAgentDirs(cwd).preferredDir,\n modelScope: projectSettings.modelScope ?? userSettings.modelScope,\n };\n }\n}\n\n/**\n * Copy the parts a caller could mutate in place.\n *\n * The agent objects themselves are treated as immutable by every consumer, so\n * only the array is copied; deep-cloning every config on each keystroke would\n * cost more than the walk this cache replaced.\n */\nfunction cloneResult(result: AgentDiscoveryResult): AgentDiscoveryResult {\n return { ...result, agents: [...result.agents] };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"discovery.mjs","names":["path","fs"],"sources":["../../../../src/adapters/agents/discovery.ts"],"sourcesContent":["/**\n * Cached discovery of the agents visible from a working directory.\n *\n * WHY THIS IS A SERVICE AND NOT A FUNCTION:\n * Discovery reads two settings files and walks every agent directory in scope,\n * parsing the frontmatter of every file it finds. The slash-command completion\n * path asks for the agent list on each keystroke, and preflight asks again for\n * each launch, so the predecessor's plain function re-walked the tree many times\n * per second. Caching needs somewhere to live, and invalidation needs an owner.\n *\n * DESIGN PATTERNS:\n * - Two-layer validity. A short TTL bounds how stale an answer can be, and a\n * cheap source fingerprint catches the common edits sooner than the TTL would\n * - Bounded by an LRU keyed on cwd and scope, so a long session that visits many\n * directories cannot grow the cache without limit\n * - Mutations invalidate explicitly rather than relying on the TTL, so an agent\n * the user just created is visible immediately\n *\n * WHY THE FINGERPRINT IS NOT ENOUGH ON ITS OWN:\n * It stats the settings files and the agent directories, not every agent file.\n * A directory's mtime moves when a file is added or removed but not when an\n * existing file's contents change, so an in-place edit is caught by the TTL\n * rather than by the fingerprint. Stat'ing every agent file on every keystroke\n * would reintroduce the cost this cache exists to remove.\n *\n * AVOID:\n * - Handing out the cached array without copying it; a caller that sorts or\n * splices in place would corrupt every later reader\n * - Raising the TTL to hide a missing invalidate call\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { getHarnessState, loadMajorModesConfig, resolvePackageConfigurations } from '@agimon-ai/doompi-config';\n\nimport {\n DOOMPI_TEAM_PACKAGE,\n mergeTeamPackageConfigurations,\n type TeamPackageConfig,\n} from '../../schemas/team/packageConfig';\nimport { LruCache } from '../../services/support/lruCache';\nimport { loadAgentsFromDir, pluginAgentDirs } from './loader';\nimport {\n canonicalizeDiscoveryCwd,\n getProjectAgentSettingsPath,\n getUserAgentSettingsPath,\n resolveNearestProjectAgentDirs,\n userAgentDirs,\n} from './projectRoot';\nimport {\n applyCustomAgentOverrides,\n applySubagentDefaults,\n EMPTY_SUBAGENT_SETTINGS,\n readSubagentSettings,\n resolveSubagentDefaultExtensions,\n resolveSubagentDefaultModel,\n resolveSubagentDefaultThinking,\n} from './settings';\nimport type { AgentConfig, AgentDiscoveryResult, AgentScope, AgentDiscoveryContract, SubagentSettings } from './types';\n\n/** How long a discovery result may be reused without revalidation. */\nconst DISCOVERY_TTL_MS = 5_000;\n\n/**\n * How many (cwd, scope) combinations stay cached.\n *\n * A session works in a handful of directories and there are three scopes, so\n * this is far above real usage; it exists to bound a pathological case.\n */\nconst MAX_CACHED_DISCOVERIES = 64;\n\nconst FINGERPRINT_FIELD_SEPARATOR = ':';\nconst FINGERPRINT_ENTRY_SEPARATOR = '|';\nconst MISSING_SOURCE_MARKER = '-';\nconst MAJOR_MODES_CONFIG_RELATIVE_PATH = path.join('.doom', 'modes.yaml');\nconst THINKING_SUFFIX = /:(?:off|minimal|low|medium|high|xhigh|max)$/;\n\ninterface CacheEntry {\n result: AgentDiscoveryResult;\n expiresAt: number;\n fingerprint: string;\n}\n\n/**\n * Merge the sources into one list, later sources shadowing earlier ones.\n *\n * Precedence is plugin, then user, then project: the definition closest to the\n * work being done wins, which is what lets a repository pin an agent that the\n * user has also defined globally.\n */\nexport function mergeAgentsForScope(\n scope: AgentScope,\n userAgents: AgentConfig[],\n projectAgents: AgentConfig[],\n pluginAgents: AgentConfig[] = [],\n): AgentConfig[] {\n const byName = new Map<string, AgentConfig>();\n for (const agent of pluginAgents) byName.set(agent.name, agent);\n if (scope !== 'project') {\n for (const agent of userAgents) byName.set(agent.name, agent);\n }\n if (scope !== 'user') {\n for (const agent of projectAgents) byName.set(agent.name, agent);\n }\n return [...byName.values()];\n}\n\n/** Size and mtime of one path, or a marker when it does not exist. */\nfunction stampOf(target: string): string {\n try {\n const stat = fs.statSync(target);\n return `${target}${FINGERPRINT_FIELD_SEPARATOR}${stat.mtimeMs}${FINGERPRINT_FIELD_SEPARATOR}${stat.size}`;\n } catch {\n // A missing settings file or agent directory is a normal state, and it has\n // to be distinguishable from a present one so that creating it invalidates.\n return `${target}${FINGERPRINT_FIELD_SEPARATOR}${MISSING_SOURCE_MARKER}`;\n }\n}\n\nexport interface ActiveTeamPackageConfig {\n config: TeamPackageConfig;\n path: string;\n}\n\n/** Resolve Team-owned package configuration from the current parent harness state. */\nexport function resolveActiveTeamPackageConfig(): ActiveTeamPackageConfig | undefined {\n const harness = getHarnessState();\n if (!harness.root) return undefined;\n const layers = loadMajorModesConfig(harness.root);\n const entries = resolvePackageConfigurations(layers, harness.layers, DOOMPI_TEAM_PACKAGE);\n const config = mergeTeamPackageConfigurations(entries);\n return config ? { config, path: path.join(harness.root, MAJOR_MODES_CONFIG_RELATIVE_PATH) } : undefined;\n}\n\nfunction teamModelSpecs(config: TeamPackageConfig | undefined): string[] | undefined {\n if (!config?.models) return undefined;\n return config.models.map(({ model, thinking }) =>\n thinking ? `${model.replace(THINKING_SUFFIX, '')}:${thinking}` : model,\n );\n}\n\n/** Resolve Team's ordered package model policy without merging it into an agent definition. */\nexport function resolveActiveTeamModelSpecs(): string[] | undefined {\n return teamModelSpecs(resolveActiveTeamPackageConfig()?.config);\n}\n\nexport class AgentDiscoveryService implements AgentDiscoveryContract {\n private readonly cache = new LruCache<string, CacheEntry>(MAX_CACHED_DISCOVERIES);\n\n /**\n * Seams for tests, as protected members rather than constructor arguments.\n *\n * Runtime tuning is not part of this service's dependency contract. A test\n * subclass overrides these values without abstracting the clock.\n */\n protected readonly ttlMs: number = DISCOVERY_TTL_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n discover(cwd: string, scope: AgentScope): AgentDiscoveryResult {\n const resolvedCwd = path.resolve(cwd);\n const canonicalCwd = canonicalizeDiscoveryCwd(resolvedCwd);\n const key = `${scope}${FINGERPRINT_ENTRY_SEPARATOR}${canonicalCwd}`;\n const cached = this.cache.get(key);\n const timestamp = this.now();\n\n if (cached && cached.expiresAt > timestamp && cached.fingerprint === this.fingerprint(canonicalCwd)) {\n return cloneResult(cached.result);\n }\n\n const result = this.load(resolvedCwd, scope);\n this.cache.set(key, {\n result,\n expiresAt: timestamp + this.ttlMs,\n fingerprint: this.fingerprint(canonicalCwd),\n });\n return cloneResult(result);\n }\n\n find(cwd: string, scope: AgentScope, name: string): AgentConfig | undefined {\n return this.discover(cwd, scope).agents.find((agent) => agent.name === name);\n }\n\n invalidate(): void {\n this.cache.clear();\n }\n\n /**\n * A cheap stamp of everything discovery reads at the directory level.\n *\n * Deliberately independent of scope: a user-scoped and a project-scoped lookup\n * from the same directory read overlapping sources, and computing one stamp\n * keeps the stat count fixed regardless of how many scopes are queried.\n */\n private fingerprint(cwd: string): string {\n const harness = getHarnessState();\n const sources = [\n getUserAgentSettingsPath(),\n getProjectAgentSettingsPath(cwd) ?? MISSING_SOURCE_MARKER,\n ...userAgentDirs(),\n ...resolveNearestProjectAgentDirs(cwd).readDirs,\n ...pluginAgentDirs(),\n ];\n if (harness.root) sources.push(path.join(harness.root, MAJOR_MODES_CONFIG_RELATIVE_PATH));\n return [...sources.map(stampOf), `active-layers${FINGERPRINT_FIELD_SEPARATOR}${harness.layers.join(',')}`].join(\n FINGERPRINT_ENTRY_SEPARATOR,\n );\n }\n\n /**\n * The uncached read. Protected so a test can count how often it actually runs,\n * which is the only direct way to assert that the cache is doing its job.\n */\n protected load(cwd: string, scope: AgentScope): AgentDiscoveryResult {\n const userSettingsPath = getUserAgentSettingsPath();\n const projectSettingsPath = getProjectAgentSettingsPath(cwd);\n const userSettings: SubagentSettings =\n scope === 'project' ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);\n const projectSettings: SubagentSettings =\n scope === 'user' ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);\n\n const legacyDefaultModel = resolveSubagentDefaultModel(\n userSettings,\n projectSettings,\n userSettingsPath,\n projectSettingsPath,\n );\n const teamPackage = resolveActiveTeamPackageConfig();\n const packageModels = teamModelSpecs(teamPackage?.config);\n const packagePrimaryModel = packageModels?.[0];\n const defaultModel =\n packagePrimaryModel && teamPackage\n ? {\n type: 'packages.team.defaultModel' as const,\n scope: 'package' as const,\n path: teamPackage.path,\n model: packagePrimaryModel,\n }\n : legacyDefaultModel;\n const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);\n const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);\n\n const resolve = (agents: AgentConfig[]): AgentConfig[] =>\n applyCustomAgentOverrides(\n applySubagentDefaults(agents, defaultModel, defaultThinking, defaultExtensions),\n userSettings,\n projectSettings,\n userSettingsPath,\n projectSettingsPath,\n );\n\n const plugin = resolve(pluginAgentDirs().flatMap((directory) => loadAgentsFromDir(directory, 'plugin')));\n const user =\n scope === 'project' ? [] : resolve(userAgentDirs().flatMap((directory) => loadAgentsFromDir(directory, 'user')));\n const project =\n scope === 'user'\n ? []\n : resolve(\n resolveNearestProjectAgentDirs(cwd).readDirs.flatMap((directory) =>\n loadAgentsFromDir(directory, 'project'),\n ),\n );\n\n return {\n // A disabled agent is dropped last, so that an override which re-enables\n // one has already been applied by the time this runs.\n agents: mergeAgentsForScope(scope, user, project, plugin).filter((agent) => agent.disabled !== true),\n projectAgentsDir: resolveNearestProjectAgentDirs(cwd).preferredDir,\n modelScope: projectSettings.modelScope ?? userSettings.modelScope,\n };\n }\n}\n\n/**\n * Copy the parts a caller could mutate in place.\n *\n * The agent objects themselves are treated as immutable by every consumer, so\n * only the array is copied; deep-cloning every config on each keystroke would\n * cost more than the walk this cache replaced.\n */\nfunction cloneResult(result: AgentDiscoveryResult): AgentDiscoveryResult {\n return { ...result, agents: [...result.agents] };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"loader.mjs","names":[],"sources":["../../../../src/adapters/agents/loader.ts"],"sourcesContent":["/**\n * Reading agent definitions off disk into `AgentConfig` values.\n *\n * One markdown file is one agent: YAML frontmatter declares how the child run\n * should be shaped, and the body is its system prompt. This module owns the walk\n * that finds those files and the field-by-field interpretation of what it finds.\n * Precedence between sources, settings defaults, and builtin overrides are\n * applied by discovery afterwards, never here.\n *\n * DESIGN PATTERNS:\n * - A malformed value fails loudly, a malformed file is skipped quietly. A file\n * missing `name` or `description` is simply not an agent and must not stop the\n * sweep, but a `timeoutMs` of \"soon\" is an authoring mistake that has to\n * surface rather than be silently dropped\n * - Frontmatter keys this module does not interpret are copied into\n * `extraFields` verbatim, so a later writer can round-trip a file it did not\n * fully understand\n * - The directory walk prunes at project boundaries, so an agent directory that\n * happens to contain a checked-out sub-repository does not absorb its agents\n *\n * ASYNC-ONLY:\n * This package has no synchronous run mode, so the `async` frontmatter field is\n * not interpreted. It falls through into `extraFields` like any other key,\n * which keeps a definition written for the predecessor readable here.\n *\n * AVOID:\n * - Applying settings defaults or builtin overrides in this module\n * - Adding a field here without adding it to `LOADER_KNOWN_FIELDS`, which would\n * leak it into `extraFields` and get it written back twice\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { getProjectConfigDir } from '../filesystem/configDir';\nimport type { SystemPromptMode, ToolBudgetConfig } from '../../types';\nimport { parseFrontmatter, parseFrontmatterList } from './frontmatter';\nimport { parseMemoryFrontmatter } from './memory';\nimport { setAgentFrontmatterFields } from './settings';\nimport type { AgentConfig, AgentDefaultContext, AgentSource } from './types';\n\n// ============================================================================\n// Builtin defaults\n// ============================================================================\n\n/**\n * The agents this package ships.\n *\n * Named here because a definition on disk with one of these names is an\n * override of a builtin rather than a new agent, and because their defaults\n * differ from a custom agent's.\n */\nexport const BUILTIN_AGENT_NAMES = [\n 'advisor',\n 'context-builder',\n 'delegate',\n 'oracle',\n 'planner',\n 'researcher',\n 'reviewer',\n 'scout',\n 'worker',\n] as const;\n\n/**\n * The one builtin that extends the host's context instead of replacing it.\n *\n * `delegate` exists to carry out work on the operator's behalf in the same\n * project, so it inherits the host system prompt and project context. Every\n * other agent starts clean, which is what makes its output reproducible.\n */\nconst CONTEXT_INHERITING_BUILTIN = 'delegate';\n\nexport function defaultSystemPromptMode(name: string): SystemPromptMode {\n return name === CONTEXT_INHERITING_BUILTIN ? 'append' : 'replace';\n}\n\nexport function defaultInheritProjectContext(name: string): boolean {\n return name === CONTEXT_INHERITING_BUILTIN;\n}\n\nexport function defaultInheritSkills(): boolean {\n return false;\n}\n\n// ============================================================================\n// Frontmatter vocabulary\n// ============================================================================\n\n/**\n * Every frontmatter key this module interprets.\n *\n * Anything absent from this set is preserved in `extraFields` instead. `async`\n * is deliberately absent: this package is async-only, so the key is data to\n * carry, not a setting to honour.\n */\nexport const LOADER_KNOWN_FIELDS: ReadonlySet<string> = new Set([\n 'name',\n 'package',\n 'description',\n 'tools',\n 'model',\n 'fallbackModels',\n 'thinking',\n 'systemPromptMode',\n 'inheritProjectContext',\n 'inheritSkills',\n 'defaultContext',\n 'timeoutMs',\n 'skill',\n 'skills',\n 'skillPath',\n 'extensions',\n 'subagentOnlyExtensions',\n 'output',\n 'defaultReads',\n 'defaultProgress',\n 'interactive',\n 'maxSubagentDepth',\n 'completionGuard',\n 'toolBudget',\n 'memory',\n]);\n\nconst TRUE_LITERAL = 'true';\nconst FALSE_LITERAL = 'false';\n\n/** Frontmatter carries strings, so a boolean field is a tri-state until parsed. */\nfunction parseOptionalBoolean(raw: string | undefined): boolean | undefined {\n if (raw === TRUE_LITERAL) return true;\n if (raw === FALSE_LITERAL) return false;\n return undefined;\n}\n\n// ============================================================================\n// Identity\n// ============================================================================\n\n/** A package-qualified agent name: dot-separated lowercase segments. */\nconst IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]*(?:\\.[a-z0-9][a-z0-9-]*)*$/;\nconst PACKAGE_NAME_SEPARATOR = '.';\n\n/**\n * Reduce an authored package name to the canonical form used in runtime names.\n *\n * Sanitizing rather than rejecting outright keeps a human-written `My Tools`\n * usable, while the pattern check afterwards still rejects anything that\n * sanitizes down to nothing meaningful.\n */\nfunction normalizePackageName(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n return trimmed\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9.-]/g, '')\n .replace(/-+/g, '-')\n .replace(/\\.+/g, '.')\n .replace(/(?:^[-.]+|[-.]+$)/g, '');\n}\n\n/** Validate a `package` frontmatter value. An absent or empty value is not an error. */\nexport function parsePackageName(value: unknown, label = 'package'): { packageName?: string; error?: string } {\n if (value === undefined || value === false || value === '') return { packageName: undefined };\n if (typeof value !== 'string') return { error: `${label} must be a string or false when provided.` };\n const packageName = normalizePackageName(value);\n if (!packageName || !IDENTIFIER_PATTERN.test(packageName)) {\n return { error: `${label} is invalid after sanitization.` };\n }\n return { packageName };\n}\n\n/** The name an agent is addressed by, qualified when it belongs to a package. */\nexport function buildRuntimeName(localName: string, packageName?: string): string {\n const trimmedPackage = packageName?.trim();\n return trimmedPackage ? `${trimmedPackage}${PACKAGE_NAME_SEPARATOR}${localName}` : localName;\n}\n\n// ============================================================================\n// Tool lists\n// ============================================================================\n\n/** Marks a tool that is reached through an MCP server rather than the host. */\nconst MCP_TOOL_PREFIX = 'mcp:';\n\n/**\n * Split an authored `tools` list into host tools and MCP tools.\n *\n * The two are separate fields on `AgentConfig` because they are enforced by\n * different layers, but a single flat list is what an author wants to write.\n */\nexport function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {\n const mcpDirectTools: string[] = [];\n const tools: string[] = [];\n for (const tool of rawTools ?? []) {\n if (tool.startsWith(MCP_TOOL_PREFIX)) {\n mcpDirectTools.push(tool.slice(MCP_TOOL_PREFIX.length));\n } else {\n tools.push(tool);\n }\n }\n return {\n // An empty `tools:` means \"no tools\", which is not the same as an absent key.\n ...(rawTools !== undefined ? { tools } : {}),\n ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),\n };\n}\n\n// ============================================================================\n// Directory walk\n// ============================================================================\n\nconst DISCOVERY_PRUNED_DIR_NAMES: ReadonlySet<string> = new Set(['.git', 'node_modules']);\nconst GIT_DIR_NAME = '.git';\nconst LEGACY_AGENTS_DIR_NAME = '.agents';\nconst LEGACY_SKILLS_DIR_NAME = 'skills';\nconst AGENT_FILE_EXTENSION = '.md';\nconst CHAIN_FILE_EXTENSION = '.chain.md';\n\nfunction isDirectory(target: string): boolean {\n try {\n return fs.statSync(target).isDirectory();\n } catch {\n // A missing or unreadable path is simply not a directory for our purposes.\n return false;\n }\n}\n\n/** A directory that owns its own agent configuration is a separate project. */\nfunction isDiscoveryNestedProjectRoot(dir: string): boolean {\n return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, LEGACY_AGENTS_DIR_NAME));\n}\n\n/**\n * Decide whether the walk should skip a subdirectory.\n *\n * A nested repository or nested project root is pruned because its agents\n * belong to that project, not to the one being scanned. The root itself is\n * exempt: scanning a project's own agent directory must not prune it.\n */\nfunction shouldPruneDiscoveryDir(rootDir: string, dir: string, dirName: string): boolean {\n if (DISCOVERY_PRUNED_DIR_NAMES.has(dirName)) return true;\n if (fs.existsSync(path.join(dir, GIT_DIR_NAME))) return true;\n return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir);\n}\n\n/**\n * Collect matching files under `dir`, sorted so discovery order is stable.\n *\n * Symlinked entries are followed as files, because staging an agent by symlink\n * is a supported way to share one definition across projects.\n */\nexport function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean, rootDir = dir): string[] {\n const files: string[] = [];\n if (!fs.existsSync(dir)) return files;\n\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));\n } catch {\n // An unreadable directory yields nothing rather than failing the whole sweep.\n return files;\n }\n\n for (const entry of entries) {\n const filePath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) {\n files.push(...listFilesRecursive(filePath, predicate, rootDir));\n }\n continue;\n }\n if (!entry.isFile() && !entry.isSymbolicLink()) continue;\n if (!predicate(entry.name)) continue;\n files.push(filePath);\n }\n return files;\n}\n\n/**\n * Recognise a file that lives under a legacy `.agents/skills/` directory.\n *\n * Those markdown files are skill documents, not agent definitions, and they\n * predate the split. They would otherwise be loaded as nameless agents.\n */\nfunction isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {\n const relative = path.relative(rootDir, filePath);\n const parts = relative.split(path.sep).map((part) => part.toLowerCase());\n if (path.basename(rootDir).toLowerCase() === LEGACY_AGENTS_DIR_NAME) {\n parts.unshift(LEGACY_AGENTS_DIR_NAME);\n }\n return parts.some((part, index) => part === LEGACY_AGENTS_DIR_NAME && parts[index + 1] === LEGACY_SKILLS_DIR_NAME);\n}\n\n// ============================================================================\n// Structured field parsing\n// ============================================================================\n\nfunction parseSystemPromptMode(raw: string | undefined, localName: string): SystemPromptMode {\n if (raw === 'replace' || raw === 'append') return raw;\n return defaultSystemPromptMode(localName);\n}\n\nfunction parseDefaultContext(raw: string | undefined): AgentDefaultContext | undefined {\n return raw === 'fork' || raw === 'fresh' ? raw : undefined;\n}\n\nfunction parseTimeoutMs(raw: string | undefined, localName: string): number | undefined {\n if (raw === undefined) return undefined;\n const parsed = Number(raw);\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new Error(`Agent '${localName}' has invalid timeoutMs frontmatter; expected a positive integer.`);\n }\n return parsed;\n}\n\nfunction parseToolBudget(raw: string | undefined, localName: string): ToolBudgetConfig | undefined {\n if (raw === undefined || !raw.trim()) return undefined;\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error(`Agent '${localName}' has invalid toolBudget frontmatter; expected a JSON object.`);\n }\n // Field-level validation lives with the enforcement layer in the runs domain.\n return parsed as ToolBudgetConfig;\n}\n\n// ============================================================================\n// Loading\n// ============================================================================\n\n/** True for an agent definition file, false for a chain or anything else. */\nfunction isAgentFileName(fileName: string): boolean {\n return fileName.endsWith(AGENT_FILE_EXTENSION) && !fileName.endsWith(CHAIN_FILE_EXTENSION);\n}\n\n/**\n * Load every agent definition under `dir`.\n *\n * The returned configs carry only what the file itself declared. Settings\n * defaults, builtin overrides, and cross-source precedence are the caller's job.\n */\nexport function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {\n const agents: AgentConfig[] = [];\n\n for (const filePath of listFilesRecursive(dir, isAgentFileName)) {\n if (isLegacyAgentSkillPath(dir, filePath)) continue;\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf-8');\n } catch {\n // The file can be removed or replaced mid-sweep; skip it, do not abort.\n continue;\n }\n\n const { frontmatter, body } = parseFrontmatter(content);\n // Without both of these the file is documentation, not an agent.\n if (!frontmatter.name || !frontmatter.description) continue;\n\n const localName = frontmatter.name;\n const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);\n // An unusable package name would produce an unaddressable runtime name.\n if (parsedPackage.error) continue;\n\n const rawTools = parseFrontmatterList(frontmatter.tools);\n const { tools, mcpDirectTools } = splitToolList(rawTools);\n const skills = parseFrontmatterList(frontmatter.skill || frontmatter.skills);\n const skillPath = parseFrontmatterList(frontmatter.skillPath);\n const fallbackModels = parseFrontmatterList(frontmatter.fallbackModels);\n const defaultReads = parseFrontmatterList(frontmatter.defaultReads);\n const maxSubagentDepth = Number(frontmatter.maxSubagentDepth);\n\n const extraFields: Record<string, string> = {};\n for (const [key, value] of Object.entries(frontmatter)) {\n if (!LOADER_KNOWN_FIELDS.has(key)) extraFields[key] = value;\n }\n\n const agent: AgentConfig = {\n name: buildRuntimeName(localName, parsedPackage.packageName),\n localName,\n packageName: parsedPackage.packageName,\n description: frontmatter.description,\n tools: rawTools !== undefined ? (tools ?? []) : undefined,\n mcpDirectTools: mcpDirectTools && mcpDirectTools.length > 0 ? mcpDirectTools : undefined,\n model: frontmatter.model,\n fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,\n thinking: frontmatter.thinking === FALSE_LITERAL ? false : frontmatter.thinking,\n systemPromptMode: parseSystemPromptMode(frontmatter.systemPromptMode, localName),\n inheritProjectContext:\n parseOptionalBoolean(frontmatter.inheritProjectContext) ?? defaultInheritProjectContext(localName),\n inheritSkills: parseOptionalBoolean(frontmatter.inheritSkills) ?? defaultInheritSkills(),\n defaultContext: parseDefaultContext(frontmatter.defaultContext),\n defaultTimeoutMs: parseTimeoutMs(frontmatter.timeoutMs, localName),\n systemPrompt: body,\n source,\n filePath,\n skills: skills && skills.length > 0 ? skills : undefined,\n skillPath: skillPath && skillPath.length > 0 ? skillPath : undefined,\n extensions: parseFrontmatterList(frontmatter.extensions),\n subagentOnlyExtensions: parseFrontmatterList(frontmatter.subagentOnlyExtensions),\n output: frontmatter.output,\n defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,\n defaultProgress: frontmatter.defaultProgress === TRUE_LITERAL,\n interactive: frontmatter.interactive === TRUE_LITERAL,\n maxSubagentDepth: Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0 ? maxSubagentDepth : undefined,\n completionGuard: parseOptionalBoolean(frontmatter.completionGuard),\n toolBudget: parseToolBudget(frontmatter.toolBudget, localName),\n memory: parseMemoryFrontmatter(frontmatter.memory),\n extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,\n };\n // Record what the file actually declared, so a later settings pass can tell\n // an author's explicit value from one this loader defaulted in.\n setAgentFrontmatterFields(agent, new Set(Object.keys(frontmatter)));\n agents.push(agent);\n }\n\n return agents;\n}\n\n// ============================================================================\n// Plugin directories\n// ============================================================================\n\n/** Path-delimited list of read-only agent directories staged by the host. */\nexport const EXTRA_AGENT_DIRS_ENV = 'PI_SUBAGENT_EXTRA_AGENT_DIRS';\n\n/**\n * Read-only agent directories contributed by plugins.\n *\n * Read from the environment on every call rather than captured once, because a\n * host can stage a plugin directory after this module is first imported.\n */\nexport function pluginAgentDirs(): string[] {\n return (process.env[EXTRA_AGENT_DIRS_ENV] ?? '')\n .split(path.delimiter)\n .map((directory) => directory.trim())\n .filter((directory) => directory.length > 0);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"loader.mjs","names":["fs","path"],"sources":["../../../../src/adapters/agents/loader.ts"],"sourcesContent":["/**\n * Reading agent definitions off disk into `AgentConfig` values.\n *\n * One markdown file is one agent: YAML frontmatter declares how the child run\n * should be shaped, and the body is its system prompt. This module owns the walk\n * that finds those files and the field-by-field interpretation of what it finds.\n * Precedence between sources, settings defaults, and builtin overrides are\n * applied by discovery afterwards, never here.\n *\n * DESIGN PATTERNS:\n * - A malformed value fails loudly, a malformed file is skipped quietly. A file\n * missing `name` or `description` is simply not an agent and must not stop the\n * sweep, but a `timeoutMs` of \"soon\" is an authoring mistake that has to\n * surface rather than be silently dropped\n * - Frontmatter keys this module does not interpret are copied into\n * `extraFields` verbatim, so a later writer can round-trip a file it did not\n * fully understand\n * - The directory walk prunes at project boundaries, so an agent directory that\n * happens to contain a checked-out sub-repository does not absorb its agents\n *\n * ASYNC-ONLY:\n * This package has no synchronous run mode, so the `async` frontmatter field is\n * not interpreted. It falls through into `extraFields` like any other key,\n * which keeps a definition written for the predecessor readable here.\n *\n * AVOID:\n * - Applying settings defaults or builtin overrides in this module\n * - Adding a field here without adding it to `LOADER_KNOWN_FIELDS`, which would\n * leak it into `extraFields` and get it written back twice\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { getProjectConfigDir } from '../filesystem/configDir';\nimport type { SystemPromptMode, ToolBudgetConfig } from '../../types';\nimport { parseFrontmatter, parseFrontmatterList } from './frontmatter';\nimport { parseMemoryFrontmatter } from './memory';\nimport { setAgentFrontmatterFields } from './settings';\nimport type { AgentConfig, AgentDefaultContext, AgentSource } from './types';\n\n// ============================================================================\n// Builtin defaults\n// ============================================================================\n\n/**\n * The agents this package ships.\n *\n * Named here because a definition on disk with one of these names is an\n * override of a builtin rather than a new agent, and because their defaults\n * differ from a custom agent's.\n */\nexport const BUILTIN_AGENT_NAMES = [\n 'advisor',\n 'context-builder',\n 'delegate',\n 'oracle',\n 'planner',\n 'researcher',\n 'reviewer',\n 'scout',\n 'worker',\n] as const;\n\n/**\n * The one builtin that extends the host's context instead of replacing it.\n *\n * `delegate` exists to carry out work on the operator's behalf in the same\n * project, so it inherits the host system prompt and project context. Every\n * other agent starts clean, which is what makes its output reproducible.\n */\nconst CONTEXT_INHERITING_BUILTIN = 'delegate';\n\nexport function defaultSystemPromptMode(name: string): SystemPromptMode {\n return name === CONTEXT_INHERITING_BUILTIN ? 'append' : 'replace';\n}\n\nexport function defaultInheritProjectContext(name: string): boolean {\n return name === CONTEXT_INHERITING_BUILTIN;\n}\n\nexport function defaultInheritSkills(): boolean {\n return false;\n}\n\n// ============================================================================\n// Frontmatter vocabulary\n// ============================================================================\n\n/**\n * Every frontmatter key this module interprets.\n *\n * Anything absent from this set is preserved in `extraFields` instead. `async`\n * is deliberately absent: this package is async-only, so the key is data to\n * carry, not a setting to honour.\n */\nexport const LOADER_KNOWN_FIELDS: ReadonlySet<string> = new Set([\n 'name',\n 'package',\n 'description',\n 'tools',\n 'model',\n 'fallbackModels',\n 'thinking',\n 'systemPromptMode',\n 'inheritProjectContext',\n 'inheritSkills',\n 'defaultContext',\n 'timeoutMs',\n 'skill',\n 'skills',\n 'skillPath',\n 'extensions',\n 'subagentOnlyExtensions',\n 'output',\n 'defaultReads',\n 'defaultProgress',\n 'interactive',\n 'maxSubagentDepth',\n 'completionGuard',\n 'toolBudget',\n 'memory',\n]);\n\nconst TRUE_LITERAL = 'true';\nconst FALSE_LITERAL = 'false';\n\n/** Frontmatter carries strings, so a boolean field is a tri-state until parsed. */\nfunction parseOptionalBoolean(raw: string | undefined): boolean | undefined {\n if (raw === TRUE_LITERAL) return true;\n if (raw === FALSE_LITERAL) return false;\n return undefined;\n}\n\n// ============================================================================\n// Identity\n// ============================================================================\n\n/** A package-qualified agent name: dot-separated lowercase segments. */\nconst IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]*(?:\\.[a-z0-9][a-z0-9-]*)*$/;\nconst PACKAGE_NAME_SEPARATOR = '.';\n\n/**\n * Reduce an authored package name to the canonical form used in runtime names.\n *\n * Sanitizing rather than rejecting outright keeps a human-written `My Tools`\n * usable, while the pattern check afterwards still rejects anything that\n * sanitizes down to nothing meaningful.\n */\nfunction normalizePackageName(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n return trimmed\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9.-]/g, '')\n .replace(/-+/g, '-')\n .replace(/\\.+/g, '.')\n .replace(/(?:^[-.]+|[-.]+$)/g, '');\n}\n\n/** Validate a `package` frontmatter value. An absent or empty value is not an error. */\nexport function parsePackageName(value: unknown, label = 'package'): { packageName?: string; error?: string } {\n if (value === undefined || value === false || value === '') return { packageName: undefined };\n if (typeof value !== 'string') return { error: `${label} must be a string or false when provided.` };\n const packageName = normalizePackageName(value);\n if (!packageName || !IDENTIFIER_PATTERN.test(packageName)) {\n return { error: `${label} is invalid after sanitization.` };\n }\n return { packageName };\n}\n\n/** The name an agent is addressed by, qualified when it belongs to a package. */\nexport function buildRuntimeName(localName: string, packageName?: string): string {\n const trimmedPackage = packageName?.trim();\n return trimmedPackage ? `${trimmedPackage}${PACKAGE_NAME_SEPARATOR}${localName}` : localName;\n}\n\n// ============================================================================\n// Tool lists\n// ============================================================================\n\n/** Marks a tool that is reached through an MCP server rather than the host. */\nconst MCP_TOOL_PREFIX = 'mcp:';\n\n/**\n * Split an authored `tools` list into host tools and MCP tools.\n *\n * The two are separate fields on `AgentConfig` because they are enforced by\n * different layers, but a single flat list is what an author wants to write.\n */\nexport function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {\n const mcpDirectTools: string[] = [];\n const tools: string[] = [];\n for (const tool of rawTools ?? []) {\n if (tool.startsWith(MCP_TOOL_PREFIX)) {\n mcpDirectTools.push(tool.slice(MCP_TOOL_PREFIX.length));\n } else {\n tools.push(tool);\n }\n }\n return {\n // An empty `tools:` means \"no tools\", which is not the same as an absent key.\n ...(rawTools !== undefined ? { tools } : {}),\n ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),\n };\n}\n\n// ============================================================================\n// Directory walk\n// ============================================================================\n\nconst DISCOVERY_PRUNED_DIR_NAMES: ReadonlySet<string> = new Set(['.git', 'node_modules']);\nconst GIT_DIR_NAME = '.git';\nconst LEGACY_AGENTS_DIR_NAME = '.agents';\nconst LEGACY_SKILLS_DIR_NAME = 'skills';\nconst AGENT_FILE_EXTENSION = '.md';\nconst CHAIN_FILE_EXTENSION = '.chain.md';\n\nfunction isDirectory(target: string): boolean {\n try {\n return fs.statSync(target).isDirectory();\n } catch {\n // A missing or unreadable path is simply not a directory for our purposes.\n return false;\n }\n}\n\n/** A directory that owns its own agent configuration is a separate project. */\nfunction isDiscoveryNestedProjectRoot(dir: string): boolean {\n return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, LEGACY_AGENTS_DIR_NAME));\n}\n\n/**\n * Decide whether the walk should skip a subdirectory.\n *\n * A nested repository or nested project root is pruned because its agents\n * belong to that project, not to the one being scanned. The root itself is\n * exempt: scanning a project's own agent directory must not prune it.\n */\nfunction shouldPruneDiscoveryDir(rootDir: string, dir: string, dirName: string): boolean {\n if (DISCOVERY_PRUNED_DIR_NAMES.has(dirName)) return true;\n if (fs.existsSync(path.join(dir, GIT_DIR_NAME))) return true;\n return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir);\n}\n\n/**\n * Collect matching files under `dir`, sorted so discovery order is stable.\n *\n * Symlinked entries are followed as files, because staging an agent by symlink\n * is a supported way to share one definition across projects.\n */\nexport function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean, rootDir = dir): string[] {\n const files: string[] = [];\n if (!fs.existsSync(dir)) return files;\n\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));\n } catch {\n // An unreadable directory yields nothing rather than failing the whole sweep.\n return files;\n }\n\n for (const entry of entries) {\n const filePath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) {\n files.push(...listFilesRecursive(filePath, predicate, rootDir));\n }\n continue;\n }\n if (!entry.isFile() && !entry.isSymbolicLink()) continue;\n if (!predicate(entry.name)) continue;\n files.push(filePath);\n }\n return files;\n}\n\n/**\n * Recognise a file that lives under a legacy `.agents/skills/` directory.\n *\n * Those markdown files are skill documents, not agent definitions, and they\n * predate the split. They would otherwise be loaded as nameless agents.\n */\nfunction isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {\n const relative = path.relative(rootDir, filePath);\n const parts = relative.split(path.sep).map((part) => part.toLowerCase());\n if (path.basename(rootDir).toLowerCase() === LEGACY_AGENTS_DIR_NAME) {\n parts.unshift(LEGACY_AGENTS_DIR_NAME);\n }\n return parts.some((part, index) => part === LEGACY_AGENTS_DIR_NAME && parts[index + 1] === LEGACY_SKILLS_DIR_NAME);\n}\n\n// ============================================================================\n// Structured field parsing\n// ============================================================================\n\nfunction parseSystemPromptMode(raw: string | undefined, localName: string): SystemPromptMode {\n if (raw === 'replace' || raw === 'append') return raw;\n return defaultSystemPromptMode(localName);\n}\n\nfunction parseDefaultContext(raw: string | undefined): AgentDefaultContext | undefined {\n return raw === 'fork' || raw === 'fresh' ? raw : undefined;\n}\n\nfunction parseTimeoutMs(raw: string | undefined, localName: string): number | undefined {\n if (raw === undefined) return undefined;\n const parsed = Number(raw);\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new Error(`Agent '${localName}' has invalid timeoutMs frontmatter; expected a positive integer.`);\n }\n return parsed;\n}\n\nfunction parseToolBudget(raw: string | undefined, localName: string): ToolBudgetConfig | undefined {\n if (raw === undefined || !raw.trim()) return undefined;\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error(`Agent '${localName}' has invalid toolBudget frontmatter; expected a JSON object.`);\n }\n // Field-level validation lives with the enforcement layer in the runs domain.\n return parsed as ToolBudgetConfig;\n}\n\n// ============================================================================\n// Loading\n// ============================================================================\n\n/** True for an agent definition file, false for a chain or anything else. */\nfunction isAgentFileName(fileName: string): boolean {\n return fileName.endsWith(AGENT_FILE_EXTENSION) && !fileName.endsWith(CHAIN_FILE_EXTENSION);\n}\n\n/**\n * Load every agent definition under `dir`.\n *\n * The returned configs carry only what the file itself declared. Settings\n * defaults, builtin overrides, and cross-source precedence are the caller's job.\n */\nexport function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {\n const agents: AgentConfig[] = [];\n\n for (const filePath of listFilesRecursive(dir, isAgentFileName)) {\n if (isLegacyAgentSkillPath(dir, filePath)) continue;\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf-8');\n } catch {\n // The file can be removed or replaced mid-sweep; skip it, do not abort.\n continue;\n }\n\n const { frontmatter, body } = parseFrontmatter(content);\n // Without both of these the file is documentation, not an agent.\n if (!frontmatter.name || !frontmatter.description) continue;\n\n const localName = frontmatter.name;\n const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);\n // An unusable package name would produce an unaddressable runtime name.\n if (parsedPackage.error) continue;\n\n const rawTools = parseFrontmatterList(frontmatter.tools);\n const { tools, mcpDirectTools } = splitToolList(rawTools);\n const skills = parseFrontmatterList(frontmatter.skill || frontmatter.skills);\n const skillPath = parseFrontmatterList(frontmatter.skillPath);\n const fallbackModels = parseFrontmatterList(frontmatter.fallbackModels);\n const defaultReads = parseFrontmatterList(frontmatter.defaultReads);\n const maxSubagentDepth = Number(frontmatter.maxSubagentDepth);\n\n const extraFields: Record<string, string> = {};\n for (const [key, value] of Object.entries(frontmatter)) {\n if (!LOADER_KNOWN_FIELDS.has(key)) extraFields[key] = value;\n }\n\n const agent: AgentConfig = {\n name: buildRuntimeName(localName, parsedPackage.packageName),\n localName,\n packageName: parsedPackage.packageName,\n description: frontmatter.description,\n tools: rawTools !== undefined ? (tools ?? []) : undefined,\n mcpDirectTools: mcpDirectTools && mcpDirectTools.length > 0 ? mcpDirectTools : undefined,\n model: frontmatter.model,\n fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,\n thinking: frontmatter.thinking === FALSE_LITERAL ? false : frontmatter.thinking,\n systemPromptMode: parseSystemPromptMode(frontmatter.systemPromptMode, localName),\n inheritProjectContext:\n parseOptionalBoolean(frontmatter.inheritProjectContext) ?? defaultInheritProjectContext(localName),\n inheritSkills: parseOptionalBoolean(frontmatter.inheritSkills) ?? defaultInheritSkills(),\n defaultContext: parseDefaultContext(frontmatter.defaultContext),\n defaultTimeoutMs: parseTimeoutMs(frontmatter.timeoutMs, localName),\n systemPrompt: body,\n source,\n filePath,\n skills: skills && skills.length > 0 ? skills : undefined,\n skillPath: skillPath && skillPath.length > 0 ? skillPath : undefined,\n extensions: parseFrontmatterList(frontmatter.extensions),\n subagentOnlyExtensions: parseFrontmatterList(frontmatter.subagentOnlyExtensions),\n output: frontmatter.output,\n defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,\n defaultProgress: frontmatter.defaultProgress === TRUE_LITERAL,\n interactive: frontmatter.interactive === TRUE_LITERAL,\n maxSubagentDepth: Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0 ? maxSubagentDepth : undefined,\n completionGuard: parseOptionalBoolean(frontmatter.completionGuard),\n toolBudget: parseToolBudget(frontmatter.toolBudget, localName),\n memory: parseMemoryFrontmatter(frontmatter.memory),\n extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,\n };\n // Record what the file actually declared, so a later settings pass can tell\n // an author's explicit value from one this loader defaulted in.\n setAgentFrontmatterFields(agent, new Set(Object.keys(frontmatter)));\n agents.push(agent);\n }\n\n return agents;\n}\n\n// ============================================================================\n// Plugin directories\n// ============================================================================\n\n/** Path-delimited list of read-only agent directories staged by the host. */\nexport const EXTRA_AGENT_DIRS_ENV = 'PI_SUBAGENT_EXTRA_AGENT_DIRS';\n\n/**\n * Read-only agent directories contributed by plugins.\n *\n * Read from the environment on every call rather than captured once, because a\n * host can stage a plugin directory after this module is first imported.\n */\nexport function pluginAgentDirs(): string[] {\n return (process.env[EXTRA_AGENT_DIRS_ENV] ?? '')\n .split(path.delimiter)\n .map((directory) => directory.trim())\n .filter((directory) => directory.length > 0);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"projectRoot.mjs","names":[],"sources":["../../../../src/adapters/agents/projectRoot.ts"],"sourcesContent":["/**\n * Locating the project root that agent configuration hangs off.\n *\n * A project root is any ancestor of the working directory that carries a config\n * directory or a legacy `.agents` directory. There can be several: a package\n * inside a monorepo and the monorepo itself both qualify. Which one wins is a\n * setting, because both answers are legitimate. Teams that keep one shared set\n * of agents at the repository root want `git-root`, teams that give each package\n * its own want `nearest`, and neither can be inferred from the tree alone.\n *\n * DESIGN PATTERNS:\n * - Candidates are collected once, nearest first, then a policy picks from them.\n * Separating collection from policy is what lets `git-root` be answered\n * without a second walk\n * - Path derivation goes through `shared/configDir.ts`, so a rebranded build\n * that renames its config directory needs no change here\n *\n * AVOID:\n * - Hardcoding the config directory name; it is discovered, not assumed\n * - Treating the nearest candidate as the project root without reading the\n * setting first\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { getAgentDir, getProjectConfigDir, readSettingsFileStrict } from '../filesystem/configDir';\n\nconst SETTINGS_FILE_NAME = 'settings.json';\nconst GIT_DIR_NAME = '.git';\n/**\n * Bare `.agents`. Two roles, one name: under a project root it is the legacy\n * location that predates the config directory, under the home directory it is\n * the current one. Both are read.\n */\nconst DOT_AGENTS_DIR_NAME = '.agents';\n/** Agents inside a config directory, project-level or user-level. */\nconst AGENTS_DIR_NAME = 'agents';\nconst SUBAGENTS_SETTINGS_KEY = 'subagents';\nconst PROJECT_ROOT_RESOLUTION_KEY = 'projectRootResolution';\n\n/**\n * Which candidate root wins when several ancestors qualify.\n *\n * `nearest` stops at the first one found walking up; `git-root` prefers the\n * candidate that is also the enclosing git repository root.\n */\nexport type ProjectRootResolution = 'nearest' | 'git-root';\n\n/** Stable cache identity for equivalent absolute, relative, and symlinked cwd spellings. */\nexport function canonicalizeDiscoveryCwd(cwd: string): string {\n const resolved = path.resolve(cwd);\n try {\n return fs.realpathSync.native(resolved);\n } catch {\n // Missing or inaccessible paths still need a deterministic cache identity.\n return resolved;\n }\n}\n\n/** Whether a path exists and is a directory. Any failure reads as \"no\". */\nexport function isDirectory(dirPath: string): boolean {\n try {\n return fs.statSync(dirPath).isDirectory();\n } catch {\n // A missing path, a broken symlink, or a permission error all mean \"not usable\".\n return false;\n }\n}\n\n/** True when `dir` carries either marker that makes it a project root. */\nexport function isProjectRootCandidate(dir: string): boolean {\n return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, DOT_AGENTS_DIR_NAME));\n}\n\n/** Every project root above `cwd`, nearest first. */\nexport function findProjectRootCandidates(cwd: string): string[] {\n const roots: string[] = [];\n let currentDir = cwd;\n while (true) {\n if (isProjectRootCandidate(currentDir)) roots.push(currentDir);\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) return roots;\n currentDir = parentDir;\n }\n}\n\n/**\n * Nearest ancestor containing `.git`.\n *\n * Existence is enough: a worktree or submodule has a `.git` file rather than a\n * directory, and both are real repository roots.\n */\nexport function findNearestGitRoot(cwd: string): string | null {\n let currentDir = cwd;\n while (true) {\n if (fs.existsSync(path.join(currentDir, GIT_DIR_NAME))) return currentDir;\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) return null;\n currentDir = parentDir;\n }\n}\n\n/**\n * Read a candidate's declared resolution policy.\n *\n * Deliberately throws on an unrecognised value rather than falling back: a typo\n * here silently changes which agents the whole session sees.\n */\nexport function readProjectRootResolution(projectRoot: string): ProjectRootResolution | undefined {\n const settingsPath = path.join(getProjectConfigDir(projectRoot), SETTINGS_FILE_NAME);\n if (!fs.existsSync(settingsPath)) return undefined;\n\n const settings = readSettingsFileStrict(settingsPath);\n const subagents = settings[SUBAGENTS_SETTINGS_KEY];\n if (!subagents || typeof subagents !== 'object' || Array.isArray(subagents)) return undefined;\n\n const value = (subagents as Record<string, unknown>)[PROJECT_ROOT_RESOLUTION_KEY];\n if (value === undefined) return undefined;\n if (value === 'nearest' || value === 'git-root') return value;\n throw new Error(\n `Subagent settings in '${settingsPath}' have invalid '${PROJECT_ROOT_RESOLUTION_KEY}'; expected 'nearest' or 'git-root'.`,\n );\n}\n\n/** The closest project root above `cwd`, ignoring any resolution policy. */\nexport function findNearestProjectRoot(cwd: string): string | null {\n return findProjectRootCandidates(cwd)[0] ?? null;\n}\n\n/**\n * The project root to use, honouring the resolution policy.\n *\n * The policy is read from the nearest candidate first so a package can opt out\n * of its monorepo's shared agents, and from the git root second so a repository\n * can claim its packages without editing each one.\n */\nexport function findConfiguredProjectRoot(cwd: string): string | null {\n const candidates = findProjectRootCandidates(cwd);\n const nearestRoot = candidates[0];\n if (!nearestRoot) return null;\n\n const nearestMode = readProjectRootResolution(nearestRoot);\n if (nearestMode === 'nearest') return nearestRoot;\n\n const gitRoot = findNearestGitRoot(cwd);\n const gitProjectRoot = gitRoot\n ? candidates.find((candidate) => path.resolve(candidate) === path.resolve(gitRoot))\n : undefined;\n if (gitProjectRoot && (nearestMode === 'git-root' || readProjectRootResolution(gitProjectRoot) === 'git-root')) {\n return gitProjectRoot;\n }\n\n return nearestRoot;\n}\n\n/**\n * User-level agent directories, in load order.\n *\n * Two locations, not one: agents used to live in the config directory and now\n * also live in `~/.agents`, and both are still read so an existing setup keeps\n * working after the move. Returned unfiltered because the caller reads each\n * directory anyway and a missing one costs nothing.\n */\nexport function userAgentDirs(): string[] {\n return [path.join(getAgentDir(), AGENTS_DIR_NAME), path.join(os.homedir(), DOT_AGENTS_DIR_NAME)];\n}\n\n/** The user-level settings file. Always resolvable, unlike the project one. */\nexport function getUserAgentSettingsPath(): string {\n return path.join(getAgentDir(), SETTINGS_FILE_NAME);\n}\n\n/** The project settings file, or null when `cwd` sits under no project root. */\nexport function getProjectAgentSettingsPath(cwd: string): string | null {\n const projectRoot = findConfiguredProjectRoot(cwd);\n return projectRoot ? path.join(getProjectConfigDir(projectRoot), SETTINGS_FILE_NAME) : null;\n}\n\n/**\n * Project agent directories for `cwd`.\n *\n * `readDirs` holds only directories that exist, so discovery never stats twice,\n * and lists the legacy location first so a project mid-migration still resolves.\n * `preferredDir` is where a newly created agent should be written, and is\n * returned whether or not it exists yet.\n */\nexport function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {\n const projectRoot = findConfiguredProjectRoot(cwd);\n if (!projectRoot) return { readDirs: [], preferredDir: null };\n\n const legacyDir = path.join(projectRoot, DOT_AGENTS_DIR_NAME);\n const preferredDir = path.join(getProjectConfigDir(projectRoot), AGENTS_DIR_NAME);\n const readDirs: string[] = [];\n if (isDirectory(legacyDir)) readDirs.push(legacyDir);\n if (isDirectory(preferredDir)) readDirs.push(preferredDir);\n\n return { readDirs, preferredDir };\n}\n"],"mappings":"mNA0BkG,EAAA,EAElG,MAAM,EAAqB,gBAOrB,EAAsB,UAEtB,EAAkB,SAElB,EAA8B,wBAWpC,SAAgB,EAAyB,EAAqB,CAC5D,IAAM,EAAW,EAAK,QAAQ,CAAG,EACjC,GAAI,CACF,OAAO,EAAG,aAAa,OAAO,CAAQ,CACxC,MAAQ,CAEN,OAAO,CACT,CACF,CAGA,SAAgB,EAAY,EAA0B,CACpD,GAAI,CACF,OAAO,EAAG,SAAS,CAAO,CAAC,CAAC,YAAY,CAC1C,MAAQ,CAEN,MAAO,EACT,CACF,CAGA,SAAgB,EAAuB,EAAsB,CAC3D,OAAO,EAAY,EAAoB,CAAG,CAAC,GAAK,EAAY,EAAK,KAAK,EAAK,CAAmB,CAAC,CACjG,CAGA,SAAgB,EAA0B,EAAuB,CAC/D,IAAM,EAAkB,CAAC,EACrB,EAAa,EACjB,OAAa,CACP,EAAuB,CAAU,GAAG,EAAM,KAAK,CAAU,EAE7D,IAAM,EAAY,EAAK,QAAQ,CAAU,EACzC,GAAI,IAAc,EAAY,OAAO,EACrC,EAAa,CACf,CACF,CAQA,SAAgB,EAAmB,EAA4B,CAC7D,IAAI,EAAa,EACjB,OAAa,CACX,GAAI,EAAG,WAAW,EAAK,KAAK,EAAY,MAAY,CAAC,EAAG,OAAO,EAE/D,IAAM,EAAY,EAAK,QAAQ,CAAU,EACzC,GAAI,IAAc,EAAY,OAAO,KACrC,EAAa,CACf,CACF,CAQA,SAAgB,EAA0B,EAAwD,CAChG,IAAM,EAAe,EAAK,KAAK,EAAoB,CAAW,EAAG,CAAkB,EACnF,GAAI,CAAC,EAAG,WAAW,CAAY,EAAG,OAGlC,IAAM,EADW,EAAuB,CACf,CAAC,CAAC,UAC3B,GAAI,CAAC,GAAa,OAAO,GAAc,UAAY,MAAM,QAAQ,CAAS,EAAG,OAE7E,IAAM,EAAS,EAAsC,GACjD,OAAU,IAAA,GACd,IAAI,IAAU,WAAa,IAAU,WAAY,OAAO,EACxD,MAAU,MACR,yBAAyB,EAAa,kBAAkB,EAA4B,qCACtF,CAHwD,CAI1D,CAcA,SAAgB,EAA0B,EAA4B,CACpE,IAAM,EAAa,EAA0B,CAAG,EAC1C,EAAc,EAAW,GAC/B,GAAI,CAAC,EAAa,OAAO,KAEzB,IAAM,EAAc,EAA0B,CAAW,EACzD,GAAI,IAAgB,UAAW,OAAO,EAEtC,IAAM,EAAU,EAAmB,CAAG,EAChC,EAAiB,EACnB,EAAW,KAAM,GAAc,EAAK,QAAQ,CAAS,IAAM,EAAK,QAAQ,CAAO,CAAC,EAChF,IAAA,GAKJ,OAJI,IAAmB,IAAgB,YAAc,EAA0B,CAAc,IAAM,YAC1F,EAGF,CACT,CAUA,SAAgB,GAA0B,CACxC,MAAO,CAAC,EAAK,KAAK,EAAY,EAAG,CAAe,EAAG,EAAK,KAAK,EAAG,QAAQ,EAAG,CAAmB,CAAC,CACjG,CAGA,SAAgB,GAAmC,CACjD,OAAO,EAAK,KAAK,EAAY,EAAG,CAAkB,CACpD,CAGA,SAAgB,EAA4B,EAA4B,CACtE,IAAM,EAAc,EAA0B,CAAG,EACjD,OAAO,EAAc,EAAK,KAAK,EAAoB,CAAW,EAAG,CAAkB,EAAI,IACzF,CAUA,SAAgB,EAA+B,EAAkE,CAC/G,IAAM,EAAc,EAA0B,CAAG,EACjD,GAAI,CAAC,EAAa,MAAO,CAAE,SAAU,CAAC,EAAG,aAAc,IAAK,EAE5D,IAAM,EAAY,EAAK,KAAK,EAAa,CAAmB,EACtD,EAAe,EAAK,KAAK,EAAoB,CAAW,EAAG,CAAe,EAC1E,EAAqB,CAAC,EAI5B,OAHI,EAAY,CAAS,GAAG,EAAS,KAAK,CAAS,EAC/C,EAAY,CAAY,GAAG,EAAS,KAAK,CAAY,EAElD,CAAE,WAAU,cAAa,CAClC"}
1
+ {"version":3,"file":"projectRoot.mjs","names":["path","fs","os"],"sources":["../../../../src/adapters/agents/projectRoot.ts"],"sourcesContent":["/**\n * Locating the project root that agent configuration hangs off.\n *\n * A project root is any ancestor of the working directory that carries a config\n * directory or a legacy `.agents` directory. There can be several: a package\n * inside a monorepo and the monorepo itself both qualify. Which one wins is a\n * setting, because both answers are legitimate. Teams that keep one shared set\n * of agents at the repository root want `git-root`, teams that give each package\n * its own want `nearest`, and neither can be inferred from the tree alone.\n *\n * DESIGN PATTERNS:\n * - Candidates are collected once, nearest first, then a policy picks from them.\n * Separating collection from policy is what lets `git-root` be answered\n * without a second walk\n * - Path derivation goes through `shared/configDir.ts`, so a rebranded build\n * that renames its config directory needs no change here\n *\n * AVOID:\n * - Hardcoding the config directory name; it is discovered, not assumed\n * - Treating the nearest candidate as the project root without reading the\n * setting first\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { getAgentDir, getProjectConfigDir, readSettingsFileStrict } from '../filesystem/configDir';\n\nconst SETTINGS_FILE_NAME = 'settings.json';\nconst GIT_DIR_NAME = '.git';\n/**\n * Bare `.agents`. Two roles, one name: under a project root it is the legacy\n * location that predates the config directory, under the home directory it is\n * the current one. Both are read.\n */\nconst DOT_AGENTS_DIR_NAME = '.agents';\n/** Agents inside a config directory, project-level or user-level. */\nconst AGENTS_DIR_NAME = 'agents';\nconst SUBAGENTS_SETTINGS_KEY = 'subagents';\nconst PROJECT_ROOT_RESOLUTION_KEY = 'projectRootResolution';\n\n/**\n * Which candidate root wins when several ancestors qualify.\n *\n * `nearest` stops at the first one found walking up; `git-root` prefers the\n * candidate that is also the enclosing git repository root.\n */\nexport type ProjectRootResolution = 'nearest' | 'git-root';\n\n/** Stable cache identity for equivalent absolute, relative, and symlinked cwd spellings. */\nexport function canonicalizeDiscoveryCwd(cwd: string): string {\n const resolved = path.resolve(cwd);\n try {\n return fs.realpathSync.native(resolved);\n } catch {\n // Missing or inaccessible paths still need a deterministic cache identity.\n return resolved;\n }\n}\n\n/** Whether a path exists and is a directory. Any failure reads as \"no\". */\nexport function isDirectory(dirPath: string): boolean {\n try {\n return fs.statSync(dirPath).isDirectory();\n } catch {\n // A missing path, a broken symlink, or a permission error all mean \"not usable\".\n return false;\n }\n}\n\n/** True when `dir` carries either marker that makes it a project root. */\nexport function isProjectRootCandidate(dir: string): boolean {\n return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, DOT_AGENTS_DIR_NAME));\n}\n\n/** Every project root above `cwd`, nearest first. */\nexport function findProjectRootCandidates(cwd: string): string[] {\n const roots: string[] = [];\n let currentDir = cwd;\n while (true) {\n if (isProjectRootCandidate(currentDir)) roots.push(currentDir);\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) return roots;\n currentDir = parentDir;\n }\n}\n\n/**\n * Nearest ancestor containing `.git`.\n *\n * Existence is enough: a worktree or submodule has a `.git` file rather than a\n * directory, and both are real repository roots.\n */\nexport function findNearestGitRoot(cwd: string): string | null {\n let currentDir = cwd;\n while (true) {\n if (fs.existsSync(path.join(currentDir, GIT_DIR_NAME))) return currentDir;\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) return null;\n currentDir = parentDir;\n }\n}\n\n/**\n * Read a candidate's declared resolution policy.\n *\n * Deliberately throws on an unrecognised value rather than falling back: a typo\n * here silently changes which agents the whole session sees.\n */\nexport function readProjectRootResolution(projectRoot: string): ProjectRootResolution | undefined {\n const settingsPath = path.join(getProjectConfigDir(projectRoot), SETTINGS_FILE_NAME);\n if (!fs.existsSync(settingsPath)) return undefined;\n\n const settings = readSettingsFileStrict(settingsPath);\n const subagents = settings[SUBAGENTS_SETTINGS_KEY];\n if (!subagents || typeof subagents !== 'object' || Array.isArray(subagents)) return undefined;\n\n const value = (subagents as Record<string, unknown>)[PROJECT_ROOT_RESOLUTION_KEY];\n if (value === undefined) return undefined;\n if (value === 'nearest' || value === 'git-root') return value;\n throw new Error(\n `Subagent settings in '${settingsPath}' have invalid '${PROJECT_ROOT_RESOLUTION_KEY}'; expected 'nearest' or 'git-root'.`,\n );\n}\n\n/** The closest project root above `cwd`, ignoring any resolution policy. */\nexport function findNearestProjectRoot(cwd: string): string | null {\n return findProjectRootCandidates(cwd)[0] ?? null;\n}\n\n/**\n * The project root to use, honouring the resolution policy.\n *\n * The policy is read from the nearest candidate first so a package can opt out\n * of its monorepo's shared agents, and from the git root second so a repository\n * can claim its packages without editing each one.\n */\nexport function findConfiguredProjectRoot(cwd: string): string | null {\n const candidates = findProjectRootCandidates(cwd);\n const nearestRoot = candidates[0];\n if (!nearestRoot) return null;\n\n const nearestMode = readProjectRootResolution(nearestRoot);\n if (nearestMode === 'nearest') return nearestRoot;\n\n const gitRoot = findNearestGitRoot(cwd);\n const gitProjectRoot = gitRoot\n ? candidates.find((candidate) => path.resolve(candidate) === path.resolve(gitRoot))\n : undefined;\n if (gitProjectRoot && (nearestMode === 'git-root' || readProjectRootResolution(gitProjectRoot) === 'git-root')) {\n return gitProjectRoot;\n }\n\n return nearestRoot;\n}\n\n/**\n * User-level agent directories, in load order.\n *\n * Two locations, not one: agents used to live in the config directory and now\n * also live in `~/.agents`, and both are still read so an existing setup keeps\n * working after the move. Returned unfiltered because the caller reads each\n * directory anyway and a missing one costs nothing.\n */\nexport function userAgentDirs(): string[] {\n return [path.join(getAgentDir(), AGENTS_DIR_NAME), path.join(os.homedir(), DOT_AGENTS_DIR_NAME)];\n}\n\n/** The user-level settings file. Always resolvable, unlike the project one. */\nexport function getUserAgentSettingsPath(): string {\n return path.join(getAgentDir(), SETTINGS_FILE_NAME);\n}\n\n/** The project settings file, or null when `cwd` sits under no project root. */\nexport function getProjectAgentSettingsPath(cwd: string): string | null {\n const projectRoot = findConfiguredProjectRoot(cwd);\n return projectRoot ? path.join(getProjectConfigDir(projectRoot), SETTINGS_FILE_NAME) : null;\n}\n\n/**\n * Project agent directories for `cwd`.\n *\n * `readDirs` holds only directories that exist, so discovery never stats twice,\n * and lists the legacy location first so a project mid-migration still resolves.\n * `preferredDir` is where a newly created agent should be written, and is\n * returned whether or not it exists yet.\n */\nexport function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {\n const projectRoot = findConfiguredProjectRoot(cwd);\n if (!projectRoot) return { readDirs: [], preferredDir: null };\n\n const legacyDir = path.join(projectRoot, DOT_AGENTS_DIR_NAME);\n const preferredDir = path.join(getProjectConfigDir(projectRoot), AGENTS_DIR_NAME);\n const readDirs: string[] = [];\n if (isDirectory(legacyDir)) readDirs.push(legacyDir);\n if (isDirectory(preferredDir)) readDirs.push(preferredDir);\n\n return { readDirs, preferredDir };\n}\n"],"mappings":"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"}