aiden-runtime 4.1.5 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. package/package.json +13 -3
@@ -97,6 +97,13 @@ const sessionManager_1 = require("../../core/v4/sessionManager");
97
97
  const toolRegistry_1 = require("../../core/v4/toolRegistry");
98
98
  const skillLoader_1 = require("../../core/v4/skillLoader");
99
99
  const index_1 = require("../../tools/v4/index");
100
+ // v4.6 Phase 1 — spawn_sub_agent: always-on runStore + LLM-callable tool.
101
+ const node_crypto_1 = require("node:crypto");
102
+ const node_os_1 = __importDefault(require("node:os"));
103
+ const daemonConfig_1 = require("../../core/v4/daemon/daemonConfig");
104
+ const connection_1 = require("../../core/v4/daemon/db/connection");
105
+ const runStore_1 = require("../../core/v4/daemon/runStore");
106
+ const spawnSubAgentTool_1 = require("../../tools/v4/subagent/spawnSubAgentTool");
100
107
  const skillCommands_1 = require("../../core/v4/skillCommands");
101
108
  const aidenAgent_1 = require("../../core/v4/aidenAgent");
102
109
  const promptBuilder_1 = require("../../core/v4/promptBuilder");
@@ -207,7 +214,35 @@ function buildAgentFallbackSlots(primaryAdapter, primaryProviderId, primaryModel
207
214
  // same Groq account at slots 0 and 1).
208
215
  return [primarySlot, ...defaults];
209
216
  }
217
+ /**
218
+ * Commander 5.x option-value collector. Use with `.option('--flag <v>',
219
+ * '...', collectArray, [])` so repeated `--flag a --flag b` produces
220
+ * `['a','b']` instead of `'b'`. Commander 5 does NOT support the
221
+ * `<x...>` variadic syntax on options (only positional args).
222
+ */
223
+ function collectArray(value, previous) {
224
+ if (!Array.isArray(previous))
225
+ return [value];
226
+ return previous.concat([value]);
227
+ }
210
228
  async function main(argv, opts = {}) {
229
+ // v4.5 Phase 1 — daemon foundation bootstrap is deferred until the
230
+ // REPL action handler fires (see the default `.action()` below).
231
+ // Subcommands like `trigger add`, `trigger list`, `config get`,
232
+ // `--version`, `--help`, `doctor`, etc. do NOT need the daemon
233
+ // foundation. Booting it for every CLI invocation:
234
+ // 1. Wastes work (HTTP server up + tear down for a one-shot
235
+ // DB-write subcommand).
236
+ // 2. Creates phantom "crash recovery" warnings: the daemon-
237
+ // foundation install/teardown cycle for each subcommand
238
+ // doesn't shut down gracefully (process.exit happens after
239
+ // the action), so the NEXT invocation's evaluateBootState
240
+ // sees a stale daemon_instances row with a missed heartbeat
241
+ // and writes a crash_reports row.
242
+ // 3. Adds latency on cold paths (CLI feels slow when AIDEN_DAEMON=1).
243
+ // The interactive REPL action calls `bootstrapDaemon()` explicitly,
244
+ // which is the only entry point that genuinely benefits from the
245
+ // foundation (long-running session = useful daemon).
211
246
  const program = new commander_1.Command();
212
247
  program
213
248
  .name('aiden')
@@ -234,6 +269,36 @@ async function main(argv, opts = {}) {
234
269
  process.stderr.write(`Run 'aiden --help' for available commands.\n`);
235
270
  process.exit(2);
236
271
  }
272
+ // v4.5 Phase 1 — daemon foundation bootstrap. Lazy-imported so
273
+ // the daemon module's side-effect cost stays at zero when
274
+ // AIDEN_DAEMON is off (better-sqlite3 native binding, chokidar,
275
+ // express, etc. all stay unloaded). Only fires for the REPL
276
+ // path — subcommands (trigger add/list/show/remove/enable/
277
+ // disable/test, config, --version, --help, doctor, …) do NOT
278
+ // touch the daemon foundation.
279
+ // v4.5 Phase 7c — two-phase bootstrap. The daemon FOUNDATION
280
+ // (file watchers, webhook routes, email triggers, cron
281
+ // emitter, dispatcher with placeholder runner, HTTP server)
282
+ // comes up HERE — before buildAgentRuntime, before the setup
283
+ // wizard. This restores the pre-7b guarantee that daemon
284
+ // foundation boots regardless of REPL configuration state
285
+ // (matters for systemd/launchd units booted on fresh machines
286
+ // and for any environment without a provider configured yet).
287
+ //
288
+ // The REAL agent runner gets installed later inside
289
+ // runInteractiveChat() once buildAgentRuntime returns — see
290
+ // `installDaemonAgentBuilder` call there.
291
+ try {
292
+ if (process.env.AIDEN_DAEMON === '1') {
293
+ const { bootstrapDaemonFoundation } = await Promise.resolve().then(() => __importStar(require('../../core/v4/daemon/bootstrap')));
294
+ bootstrapDaemonFoundation();
295
+ }
296
+ }
297
+ catch (e) {
298
+ // Fail-loud but non-fatal — daemon foundation init failure
299
+ // must not block the user from opening a REPL.
300
+ console.error('[daemon] foundation bootstrap failed: ' + (e instanceof Error ? e.message : String(e)));
301
+ }
237
302
  // Tier-3.1: surface --no-ui as an env var so downstream modules
238
303
  // (which import uiBuild.ts) see the flag without threading it
239
304
  // through every call site.
@@ -318,6 +383,140 @@ async function main(argv, opts = {}) {
318
383
  }
319
384
  await runSkillsSubcommand(action, arg, opts);
320
385
  });
386
+ // v4.5 Phase 2 — file watcher trigger management.
387
+ program
388
+ .command('trigger <action> [args...]')
389
+ .description('Manage daemon triggers. Actions: add, list, show, remove, enable, disable, test.')
390
+ // Commander 5.x stores option values directly on the Command
391
+ // instance, so `--name` would clobber Command.prototype.name().
392
+ // Use `--label` instead; we map to the internal `name` arg below.
393
+ .option('--label <label>', 'Trigger label/name (for add).')
394
+ // Commander 5.x does NOT support `<x...>` variadic syntax on
395
+ // OPTIONS (only on positional args) — repeating overwrites
396
+ // instead of appending. Use an explicit collector so multiple
397
+ // --path / --include / --exclude / --event flags accumulate.
398
+ .option('--path <path>', 'Path to watch (for add file). Repeatable.', collectArray, [])
399
+ .option('--include <glob>', 'Include glob pattern. Repeatable.', collectArray, [])
400
+ .option('--exclude <glob>', 'Exclude glob pattern. Repeatable.', collectArray, [])
401
+ .option('--event <type>', 'Event type: add|change|unlink. Repeatable.', collectArray, [])
402
+ .option('--debounce-ms <n>', 'Per-path debounce in ms (default 750).', (v) => Number.parseInt(v, 10))
403
+ .option('--settle-ms <n>', 'Stable-stat settle in ms (default 1000).', (v) => Number.parseInt(v, 10))
404
+ .option('--max-settle-ms <n>', 'Max settle time before giving up (default 30000).', (v) => Number.parseInt(v, 10))
405
+ .option('--max-queue-depth <n>', 'Max per-watcher queue depth (default 100).', (v) => Number.parseInt(v, 10))
406
+ .option('--no-ignore-temp', 'Disable default ignore for editor temps + .git + node_modules.')
407
+ .option('--content-hash', 'Compute sha256 per change (opt-in; slower).')
408
+ .option('--reconcile <policy>', 'skip_existing | process_new_since_last_seen | full_rescan (default skip_existing).')
409
+ .option('--polling', 'Force polling mode (network FS / WSL bind mount).')
410
+ .option('--prompt-template <text>', 'Phase 5 — agent prompt template.')
411
+ .option('--disabled', 'Create trigger in disabled state.')
412
+ // v4.5 Phase 3 — webhook-specific options.
413
+ .option('--hmac <format>', 'Webhook HMAC format: github | gitlab | generic (default generic).')
414
+ .option('--secret <s>', 'Webhook secret. Auto-generated when omitted.')
415
+ .option('--rate-limit <n>', 'Webhook requests/min (default 30).', (v) => Number.parseInt(v, 10))
416
+ .option('--max-body-bytes <n>', 'Webhook max body cap (default 1048576).', (v) => Number.parseInt(v, 10))
417
+ .option('--idempotency-ttl-ms <n>', 'Webhook idempotency TTL (default 3600000).', (v) => Number.parseInt(v, 10))
418
+ .option('--deliver-only', 'Phase 3 stub: accept + log; Phase 5 will dispatch via channel.')
419
+ // v4.5 Phase 4a — email IMAP options.
420
+ .option('--host <host>', 'IMAP host (for add email).')
421
+ .option('--port <n>', 'IMAP port (default 993).', (v) => Number.parseInt(v, 10))
422
+ .option('--user <addr>', 'IMAP user / email address (for add email).')
423
+ .option('--password <pwd>', 'IMAP password (for add email).')
424
+ .option('--no-tls', 'Disable TLS (for add email; default is TLS on).')
425
+ .option('--mailbox <name>', 'IMAP mailbox (default INBOX).')
426
+ .option('--poll-ms <n>', 'Email poll interval ms (default 15000).', (v) => Number.parseInt(v, 10))
427
+ .option('--allow-sender <addr-or-glob>', 'Allowed sender pattern (repeatable, REQUIRED for email).', collectArray, [])
428
+ .option('--allow-subject <regex>', 'Allowed subject regex (repeatable).', collectArray, [])
429
+ .option('--attachment-policy <policy>', 'skip | inline-text | save-to-tmp (default skip).')
430
+ .option('--no-validate', 'Skip IMAP pre-flight connectivity check.')
431
+ .action(async (action, posArgs, cmd) => {
432
+ const { runTriggerSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/trigger')));
433
+ const cliOpts = cmd.opts();
434
+ const argv = {
435
+ name: cliOpts.label,
436
+ paths: cliOpts.path,
437
+ include: cliOpts.include,
438
+ exclude: cliOpts.exclude,
439
+ events: cliOpts.event,
440
+ debounceMs: cliOpts.debounceMs,
441
+ settleMs: cliOpts.settleMs,
442
+ maxSettleMs: cliOpts.maxSettleMs,
443
+ maxQueueDepth: cliOpts.maxQueueDepth,
444
+ noIgnoreTemp: cliOpts.ignoreTemp === false,
445
+ contentHash: cliOpts.contentHash === true,
446
+ reconcile: cliOpts.reconcile,
447
+ polling: cliOpts.polling === true,
448
+ promptTemplate: cliOpts.promptTemplate,
449
+ disabled: cliOpts.disabled === true,
450
+ // v4.5 Phase 3 — webhook options.
451
+ hmac: cliOpts.hmac,
452
+ secret: cliOpts.secret,
453
+ rateLimit: cliOpts.rateLimit,
454
+ maxBodyBytes: cliOpts.maxBodyBytes,
455
+ idempotencyTtlMs: cliOpts.idempotencyTtlMs,
456
+ deliverOnly: cliOpts.deliverOnly === true,
457
+ // v4.5 Phase 4a — email options.
458
+ host: cliOpts.host,
459
+ port: cliOpts.port,
460
+ user: cliOpts.user,
461
+ password: cliOpts.password,
462
+ noTls: cliOpts.tls === false,
463
+ mailbox: cliOpts.mailbox,
464
+ pollMs: cliOpts.pollMs,
465
+ allowSenders: cliOpts.allowSender ?? [],
466
+ allowSubjects: cliOpts.allowSubject ?? [],
467
+ attachmentPolicy: cliOpts.attachmentPolicy,
468
+ noValidate: cliOpts.validate === false,
469
+ };
470
+ const code = await runTriggerSubcommand(action, posArgs ?? [], argv, {
471
+ writeOut: opts.writeOut,
472
+ });
473
+ process.exit(code);
474
+ });
475
+ // v4.5 Phase 4b — daemon supervisor commands.
476
+ program
477
+ .command('daemon <action> [args...]')
478
+ .description('Manage the v4.5 daemon. Actions: install, uninstall, start, stop, restart, status, logs.')
479
+ .action(async (action, posArgs) => {
480
+ const { runDaemonSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/daemon')));
481
+ const code = await runDaemonSubcommand(action, posArgs ?? [], {
482
+ writeOut: opts.writeOut,
483
+ });
484
+ process.exit(code);
485
+ });
486
+ // v4.5 Phase 6 — `aiden cron` top-level surface (mirrors slash command).
487
+ program
488
+ .command('cron <action> [args...]')
489
+ .description('Scheduled jobs. Actions: add, list, show, remove, enable, disable, run, logs.')
490
+ .option('--label <name>', 'job label (alphanumeric/dash/underscore)')
491
+ .option('--schedule <expr>', 'cron expr ("0 9 * * *") / interval ("every 5m") / ISO timestamp')
492
+ .option('--command <cmd>', 'shell command to run')
493
+ .option('--timezone <tz>', 'IANA timezone (default UTC)')
494
+ .option('--misfire-policy <policy>', 'skip_stale | run_once_if_late | catch_up_with_limit | manual_review')
495
+ .option('--prompt-template <tpl>', 'render template instead of running raw command (daemon mode)')
496
+ .option('--deliver-only', 'daemon skips the agent loop on fire')
497
+ .action(async (action, posArgs, cmdObj) => {
498
+ const { runCronSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/cron')));
499
+ const code = await runCronSubcommand(action, posArgs ?? [], cmdObj, {
500
+ writeOut: opts.writeOut,
501
+ });
502
+ process.exit(code);
503
+ });
504
+ // v4.5 Phase 6 — `aiden runs` surface (daemon run history).
505
+ program
506
+ .command('runs <action> [args...]')
507
+ .description('Daemon runs. Actions: list, show <id>, interrupt <id>, stats.')
508
+ .option('--limit <n>', 'list: max rows (default 50)', (v) => Number.parseInt(v, 10))
509
+ .option('--source <src>', 'list: filter by trigger source (file/webhook/email/schedule/manual)')
510
+ .option('--status <s>', 'list: filter by status (queued/running/completed/failed/cancelled/interrupted)')
511
+ .option('--trigger <prefix>', 'list: sessionId prefix (e.g. "trigger:file:<id>:")')
512
+ .option('--include-children', 'list: include sub-agent children (default: top-level only, with per-parent badge)')
513
+ .action(async (action, posArgs, cmdObj) => {
514
+ const { runRunsSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/runs')));
515
+ const code = await runRunsSubcommand(action, posArgs ?? [], cmdObj, {
516
+ writeOut: opts.writeOut,
517
+ });
518
+ process.exit(code);
519
+ });
321
520
  program
322
521
  .command('mcp <action>')
323
522
  .description('MCP server mode (Phase v4.1-mcp). Actions: serve, status, tools.')
@@ -432,6 +631,60 @@ async function main(argv, opts = {}) {
432
631
  async function buildAgentRuntime(cliOpts, opts) {
433
632
  const paths = opts.pathsOverride ?? (0, paths_1.resolveAidenPaths)();
434
633
  await (0, paths_1.ensureAidenDirsExist)(paths);
634
+ // ── v4.6 Phase 1 — always-on runStore for spawn_sub_agent ──────────────
635
+ //
636
+ // The spawn_sub_agent primitive persists each child run to the runs
637
+ // table via spawned_from_run_id FK. The REPL needs a runStore handle
638
+ // regardless of whether AIDEN_DAEMON=1 or not. SQLite WAL mode
639
+ // (enabled in openDaemonDb) allows REPL + daemon to coexist on the
640
+ // same file without lock contention. The connection.ts module caches
641
+ // per-path, so when daemon foundation has already opened the DB this
642
+ // call returns the same handle. Migration runner is idempotent on
643
+ // already-current schemas.
644
+ const replInstanceId = `repl-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
645
+ const replDb = (0, connection_1.openDaemonDb)((0, daemonConfig_1.daemonDbPath)(paths.root));
646
+ // Seed the REPL's daemon_instances row so the FK on runs.instance_id
647
+ // is satisfied. Idempotent under INSERT OR IGNORE — multiple REPL
648
+ // launches reusing the same instance_id (rare with random UUID
649
+ // suffix) silently no-op.
650
+ replDb.prepare(`INSERT OR IGNORE INTO daemon_instances
651
+ (instance_id, pid, hostname, started_at, last_heartbeat, version)
652
+ VALUES (?, ?, ?, ?, ?, ?)`).run(replInstanceId, process.pid, node_os_1.default.hostname(), Date.now(), Date.now(), version_1.VERSION);
653
+ const replRunStore = (0, runStore_1.createRunStore)({ db: replDb });
654
+ // v4.6 Phase 3A — operator kill-switch for sub-agent spawning.
655
+ // Initialised as early as possible so any subsequent tool wiring
656
+ // sees the singleton. The marker file lives at
657
+ // `<paths.root>/spawn.paused` and is shared across REPL + daemon
658
+ // + MCP for cross-process coordination. The startup probe (warn
659
+ // operator that pause is active from a prior session) fires
660
+ // later, once `bootLogger` is available — see the
661
+ // `spawnPauseBootStatus` block below.
662
+ const { initSpawnPause } = await Promise.resolve().then(() => __importStar(require('../../core/v4/subagent/spawnPause')));
663
+ const spawnPauseState = initSpawnPause({ aidenHome: paths.root });
664
+ // v4.6 Phase 3b — self-improvement loop. Initialise the durable
665
+ // failure-ledger / recovery-report store against the same
666
+ // daemon.db handle the runStore uses. WAL coexistence: REPL +
667
+ // daemon + MCP all share the same connection-cached handle, so
668
+ // any writes from one runtime are visible to the others. The
669
+ // TCE write-through path inside the agent loop reads through the
670
+ // module-level singleton; initialising here makes spawnSubAgent
671
+ // and daemon-fired turns observe the same persistence.
672
+ const { initRecoveryStore } = await Promise.resolve().then(() => __importStar(require('../../core/v4/selfimprovement/recoveryStore')));
673
+ initRecoveryStore({ db: replDb });
674
+ const spawnPauseBootStatus = spawnPauseState.isPaused()
675
+ ? spawnPauseState.status()
676
+ : null;
677
+ // v4.6 Phase 2Q-B — mutable holder for the REPL's current parent
678
+ // run id. ChatSession's `runAgentTurn` writes a row before each
679
+ // turn dispatches and stores the id here (and clears it on
680
+ // completion). The spawn / fanout tool factories below read it
681
+ // through `resolveParentRunId` / `resolveParentSessionId`
682
+ // callbacks so any child spawned mid-turn is linked back to the
683
+ // live REPL parent row via `runs.spawned_from_run_id`.
684
+ const replParentRunRef = {
685
+ runId: null,
686
+ sessionId: null,
687
+ };
435
688
  // Phase 16c.2: load `paths.envFile` (the aiden-managed `.env` that
436
689
  // `setupWizard.ts::upsertEnvVar` writes to) into `process.env` BEFORE
437
690
  // any provider resolution. The bug: setup wrote keys to this file but
@@ -475,6 +728,21 @@ async function buildAgentRuntime(cliOpts, opts) {
475
728
  }
476
729
  const config = new config_1.ConfigManager(paths);
477
730
  await config.load();
731
+ // v4.5 Phase 8a — initialise the runtimeToggles singleton with a
732
+ // ConfigManager seam so /sandbox /tce /browser-depth flips persist
733
+ // to config.yaml. Core modules (sandboxConfig, turnState,
734
+ // browserState) consult this singleton; precedence is
735
+ // env > config.yaml > default per Q-P8a-1(a).
736
+ {
737
+ const { initRuntimeToggles } = await Promise.resolve().then(() => __importStar(require('../../core/v4/runtimeToggles')));
738
+ initRuntimeToggles({
739
+ configRead: (key) => config.getValue(key),
740
+ configWriteAndSave: async (key, value) => {
741
+ config.set(key, value);
742
+ await config.save();
743
+ },
744
+ });
745
+ }
478
746
  // Phase 30.2 — fresh-user UX. Detection extends the old
479
747
  // `isFreshInstall`-only gate so we cover three new failure modes:
480
748
  // 1. fresh user with no env / no OAuth / no config → wizard fires
@@ -504,42 +772,63 @@ async function buildAgentRuntime(cliOpts, opts) {
504
772
  // consumed when building the adapter.
505
773
  let exploreMode = false;
506
774
  if (wizardNeeded) {
507
- if (!detection.hasAnyProvider) {
508
- // Truly empty: no env, no OAuth, no Ollama, no inline config.
509
- process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
510
- }
511
- else if (configuredProviderBroken) {
512
- // Config points at a provider we can't credential-resolve.
513
- process.stdout.write(`\nConfigured provider '${detection.configProvider}' has no usable credentials ` +
514
- `at ${node_path_1.default.join(paths.root, 'auth', `${detection.configProvider}.json`)}.\n`);
515
- }
516
- else {
517
- // Detected something (env / oauth / ollama) but config.yaml is
518
- // missing or empty — DEFAULT_CONFIG would route to anthropic and
519
- // the resolver would fail. Surface the detection so the user
520
- // sees what we found, then walk them through proper setup.
521
- process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
522
- process.stdout.write('config.yaml is empty — let\'s pick a provider that matches.\n');
523
- }
524
- process.stdout.write('Launching setup wizard…\n\n');
525
- const result = await (0, setupWizard_1.runSetupWizard)({ paths });
526
- // Phase 30.2.1: three exit states.
527
- if (result.status === 'exited') {
528
- // Recovery option [5] — clean exit, no REPL.
529
- process.exit(0);
530
- }
531
- if (result.status === 'skipped') {
532
- // Recovery option [4] "explore mode" OR Ctrl+C cancellation.
533
- // Boot continues into the REPL with a NullAdapter; chat is
534
- // intercepted by ChatSession, slash commands work normally.
535
- // Flagged here and consumed below where the adapter is built.
775
+ // v4.5 Phase 7c — TTY guard. The wizard uses inquirer prompts
776
+ // which block on stdin. When stdin is NOT a TTY (systemd unit
777
+ // start, launchd run, piped invocation, CI), there's no user
778
+ // to answer, so the wizard would hang forever — which in turn
779
+ // blocks the rest of buildAgentRuntime AND any post-build
780
+ // daemon wiring. Bail into exploreMode instead so the REPL
781
+ // boots with a NullAdapter; the daemon foundation (started at
782
+ // top of REPL action handler) keeps serving trigger rails with
783
+ // the placeholder runner. The operator runs `aiden` interactively
784
+ // once to finish configuration; from then on the real-agent
785
+ // runner installs on every subsequent boot.
786
+ if (!process.stdin.isTTY) {
787
+ process.stderr.write('[setup] stdin is not a TTY skipping interactive wizard. ' +
788
+ 'Booting in explore mode with NullAdapter. ' +
789
+ 'Run `aiden` from a terminal once to configure a provider.\n');
536
790
  exploreMode = true;
791
+ // Skip the wizard block entirely; fall through to adapter
792
+ // build with exploreMode=true so a NullAdapter is used.
537
793
  }
538
- // 'configured' (or 'skipped' — we still want the env/.env reload
539
- // for slash commands like /providers that read fresh state)
540
- // re-load both so the resolver sees what the wizard wrote.
541
- (0, envSources_1.loadAidenEnvFile)(paths.envFile);
542
- await config.load();
794
+ else {
795
+ if (!detection.hasAnyProvider) {
796
+ // Truly empty: no env, no OAuth, no Ollama, no inline config.
797
+ process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
798
+ }
799
+ else if (configuredProviderBroken) {
800
+ // Config points at a provider we can't credential-resolve.
801
+ process.stdout.write(`\nConfigured provider '${detection.configProvider}' has no usable credentials ` +
802
+ `at ${node_path_1.default.join(paths.root, 'auth', `${detection.configProvider}.json`)}.\n`);
803
+ }
804
+ else {
805
+ // Detected something (env / oauth / ollama) but config.yaml is
806
+ // missing or empty — DEFAULT_CONFIG would route to anthropic and
807
+ // the resolver would fail. Surface the detection so the user
808
+ // sees what we found, then walk them through proper setup.
809
+ process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
810
+ process.stdout.write('config.yaml is empty — let\'s pick a provider that matches.\n');
811
+ }
812
+ process.stdout.write('Launching setup wizard…\n\n');
813
+ const result = await (0, setupWizard_1.runSetupWizard)({ paths });
814
+ // Phase 30.2.1: three exit states.
815
+ if (result.status === 'exited') {
816
+ // Recovery option [5] — clean exit, no REPL.
817
+ process.exit(0);
818
+ }
819
+ if (result.status === 'skipped') {
820
+ // Recovery option [4] "explore mode" OR Ctrl+C cancellation.
821
+ // Boot continues into the REPL with a NullAdapter; chat is
822
+ // intercepted by ChatSession, slash commands work normally.
823
+ // Flagged here and consumed below where the adapter is built.
824
+ exploreMode = true;
825
+ }
826
+ // 'configured' (or 'skipped' — we still want the env/.env reload
827
+ // for slash commands like /providers that read fresh state) →
828
+ // re-load both so the resolver sees what the wizard wrote.
829
+ (0, envSources_1.loadAidenEnvFile)(paths.envFile);
830
+ await config.load();
831
+ } // end of TTY branch
543
832
  }
544
833
  // Phase v4.1.2-bug1: boot model selection now consults the priority-
545
834
  // list auto-picker (cli/v4/providerBootSelector.ts) instead of
@@ -726,35 +1015,28 @@ async function buildAgentRuntime(cliOpts, opts) {
726
1015
  else
727
1016
  display.dim(ln.text);
728
1017
  }
1018
+ // v4.5 TUI polish — blank line so the plugin boot card breathes
1019
+ // before the AIDEN banner stamps in below.
1020
+ display.write('\n');
729
1021
  // Phase 16g: surface the SOUL.md upgrade notice once on boot (only
730
1022
  // when set — for users with edited SOUL.md that would have been
731
1023
  // silently overwritten by the upgrade).
732
1024
  if (soulNotice) {
733
1025
  display.dim(`[soul] ${soulNotice}`);
734
1026
  }
735
- // Phase 20 Task 5: non-blocking npm update check. Fires on a separate
736
- // microtask so REPL boot is unaffected. The cache hit path is sub-ms;
737
- // the cache miss path is bounded by REGISTRY_TIMEOUT_MS (4 s) and runs
738
- // after the prompt is already up. Honors AIDEN_NO_UPDATE_CHECK=1.
1027
+ // v4.5 update system the old setImmediate dim/warn one-liner
1028
+ // here was superseded by the interactive boot prompt rendered
1029
+ // later inside `chatSession.run()::maybeShowBootUpdatePrompt`.
1030
+ // Single surface for update notification: the boxed prompt.
1031
+ // Pre-warm the registry probe so the cache is fresh by the time
1032
+ // the prompt asks — same non-blocking pattern, same opt-out
1033
+ // semantics, no user-visible output from this call.
739
1034
  setImmediate(async () => {
740
1035
  try {
741
- const { checkForUpdate, formatUpdateLine } = await Promise.resolve().then(() => __importStar(require('../../core/v4/update/checkUpdate')));
1036
+ const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require('../../core/v4/update/checkUpdate')));
742
1037
  // eslint-disable-next-line @typescript-eslint/no-var-requires
743
1038
  const pkg = require('../../package.json');
744
- const status = await checkForUpdate({
745
- paths,
746
- installedVersion: pkg.version,
747
- });
748
- const line = formatUpdateLine(status);
749
- if (line) {
750
- // Phase 20 Task 6: louder surfacing on first-ever boot when the
751
- // installed package is already behind. Subsequent boots stay
752
- // low-key (dim) — users have seen the line before.
753
- if (status.firstRun && status.updateAvailable)
754
- display.warn(line);
755
- else
756
- display.dim(line);
757
- }
1039
+ await checkForUpdate({ paths, installedVersion: pkg.version });
758
1040
  }
759
1041
  catch {
760
1042
  /* silent — update check is best-effort */
@@ -902,6 +1184,11 @@ async function buildAgentRuntime(cliOpts, opts) {
902
1184
  const recallSessionHealth = new subsystemHealth_1.SubsystemHealthTracker('recall-session');
903
1185
  subsystemHealthRegistry.register('recall-session', () => recallSessionHealth.snapshot());
904
1186
  const skillTeacher = new skillTeacher_1.SkillTeacher(skillLoader, skillManageProxy, skillTeacherTier, undefined, (name) => toolRegistry.get(name), skillTeacherHealth);
1187
+ // v4.1.6 Polish 2 — late-wire the SkillTeacher reference so the
1188
+ // post-render `handleSkillProposal` flow can persist accepted
1189
+ // proposals. CliCallbacks was constructed earlier (line ~906) —
1190
+ // before SkillTeacher existed — so we set it now.
1191
+ callbacks.setSkillTeacher(skillTeacher);
905
1192
  // ── Tool executor with full Phase 9 + 10 context ─────────────────────
906
1193
  const toolExecutor = toolRegistry.buildExecutor({
907
1194
  cwd: process.cwd(),
@@ -921,6 +1208,14 @@ async function buildAgentRuntime(cliOpts, opts) {
921
1208
  return typeof v === 'boolean' ? v : undefined;
922
1209
  };
923
1210
  const resolveToolset = (name) => toolRegistry.get(name)?.toolset;
1211
+ // v4.2 Phase 4 — checkpoint/restore mutability resolver. The agent's
1212
+ // Phase 4 hook calls this before dispatching each tool to decide
1213
+ // whether to flag the live checkpoint as having mutated state. Same
1214
+ // registry source as resolveToolset; closure captures the live
1215
+ // registry reference so newly-registered tools are seen. Unknown
1216
+ // tools return undefined → agent treats them as non-mutating (no
1217
+ // checkpoint flag); plugin authors must declare `mutates` honestly.
1218
+ const resolveMutates = (name) => toolRegistry.get(name)?.mutates;
924
1219
  // ── Phase 16b.4: assemble system-prompt context ─────────────────────
925
1220
  // PromptBuilder needs SOUL.md (read at build time from `paths.soulMd`),
926
1221
  // a frozen MemorySnapshot (loaded once at boot — same lifecycle as
@@ -1005,7 +1300,11 @@ async function buildAgentRuntime(cliOpts, opts) {
1005
1300
  // ── Build agent with all moat layers attached ────────────────────────
1006
1301
  const agent = new aidenAgent_1.AidenAgent({
1007
1302
  provider: adapter,
1008
- tools: toolRegistry.getSchemas(),
1303
+ // v4.6 Phase 1 — 'repl' context filter excludes tools tagged
1304
+ // daemon-only (none today) and INCLUDES tools tagged repl-only
1305
+ // (e.g. spawn_sub_agent, registered after this line). Tools with
1306
+ // no `contexts` field default to visible in both contexts.
1307
+ tools: toolRegistry.getSchemas(undefined, 'repl'),
1009
1308
  toolExecutor,
1010
1309
  maxTurns: config.getValue('agent.max_turns', 90),
1011
1310
  auxiliaryClient,
@@ -1034,6 +1333,22 @@ async function buildAgentRuntime(cliOpts, opts) {
1034
1333
  skillOutcomeTracker.onTool(call, phase, result);
1035
1334
  }
1036
1335
  catch { /* telemetry must not break the turn */ }
1336
+ // v4.5 Phase 8b — pre-tool-call contextual suggestion. Fires
1337
+ // when the call would benefit from a v4.4/v4.5 subsystem the
1338
+ // user currently has off. Engine handles dismissal + budget;
1339
+ // we just render the tip and tell the engine it was shown.
1340
+ if (phase === 'before') {
1341
+ try {
1342
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1343
+ const { getSuggestionEngine } = require('../../core/v4/suggestionEngine');
1344
+ const tip = getSuggestionEngine().checkToolCall(call);
1345
+ if (tip) {
1346
+ display.dim(tip.message);
1347
+ getSuggestionEngine().recordFired(tip.slot);
1348
+ }
1349
+ }
1350
+ catch { /* never let a suggestion crash a tool call */ }
1351
+ }
1037
1352
  callbacks.onToolCall?.(call, phase, result);
1038
1353
  },
1039
1354
  onCompression: callbacks.onCompression,
@@ -1042,6 +1357,7 @@ async function buildAgentRuntime(cliOpts, opts) {
1042
1357
  skillTeacherCallbacks: { promptUser: callbacks.promptSkillProposal },
1043
1358
  resolveVerifiedFlag,
1044
1359
  resolveToolset,
1360
+ resolveMutates,
1045
1361
  providerId,
1046
1362
  modelId,
1047
1363
  // Phase 16b.4: wire PromptBuilder so SOUL.md actually reaches the LLM.
@@ -1113,6 +1429,28 @@ async function buildAgentRuntime(cliOpts, opts) {
1113
1429
  memoryManager.onMutation((file) => {
1114
1430
  agent.markMemoryDirty(file === 'user' ? 'user' : 'memory');
1115
1431
  });
1432
+ // v4.5 Phase 7b — daemon agent builder. Captures the references
1433
+ // above so the dispatcher can construct a fresh AidenAgent per
1434
+ // daemon-claimed trigger. Strategy B (closure capture) — REPL
1435
+ // construction stays untouched; we just expose the builder on
1436
+ // the returned AgentRuntime so runInteractiveChat can pass it to
1437
+ // bootstrapDaemon().
1438
+ const { buildDaemonAgentBuilder } = await Promise.resolve().then(() => __importStar(require('./daemonAgentBuilder')));
1439
+ const daemonAgentBuilder = buildDaemonAgentBuilder({
1440
+ paths,
1441
+ resolver,
1442
+ fallbackAdapter: adapter,
1443
+ toolRegistry,
1444
+ toolExecutor,
1445
+ auxiliaryClient,
1446
+ promptBuilder,
1447
+ promptBuilderOptions,
1448
+ memoryManager,
1449
+ resolveVerifiedFlag,
1450
+ resolveToolset,
1451
+ resolveMutates,
1452
+ maxTurns: config.getValue('agent.max_turns', 90),
1453
+ });
1116
1454
  // Phase v4.1.2 alive-core: SOUL.md file watcher. Best-effort —
1117
1455
  // some filesystems (network mounts, certain WSL configs) don't
1118
1456
  // support fs.watch reliably. We try to attach; if it fails, the
@@ -1154,6 +1492,21 @@ async function buildAgentRuntime(cliOpts, opts) {
1154
1492
  // Wire the gateway singleton's logger BEFORE registering its processor
1155
1493
  // so register / unregister channel events are scoped correctly.
1156
1494
  gateway_1.gateway.attachLogger(bootLogger.child('gateway'));
1495
+ // v4.6 Phase 3A — startup probe for the spawn-pause kill-switch.
1496
+ // The state was initialised early (line ~740) before tool wiring.
1497
+ // Now that bootLogger exists, emit a visible warning so an
1498
+ // operator who forgot they paused in a prior session learns
1499
+ // immediately rather than puzzling at silent rejected fanouts.
1500
+ if (spawnPauseBootStatus) {
1501
+ const s = spawnPauseBootStatus;
1502
+ const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
1503
+ bootLogger.warn(`spawn_sub_agent / subagent_fanout are PAUSED${reasonSuffix}. ` +
1504
+ 'Run /spawn-pause off to resume.', {
1505
+ pausedAt: s.pausedAt ?? null,
1506
+ pausedBy: s.pausedBy ?? null,
1507
+ durationMs: s.durationMs ?? null,
1508
+ });
1509
+ }
1157
1510
  // ── Phase v4.1-subagent.1 — replace subagent_fanout stub with wired version
1158
1511
  //
1159
1512
  // tools/v4/index.ts registers a stub at boot so the schema is visible
@@ -1170,10 +1523,45 @@ async function buildAgentRuntime(cliOpts, opts) {
1170
1523
  // shared subsystems (registry, skillLoader, paths, memoryManager,
1171
1524
  // promptBuilder, promptBuilderOptions) are read-only and pass by
1172
1525
  // reference.
1526
+ // v4.6 Phase 2Q-A — `runFanout` routes each child through
1527
+ // `spawnSubAgent`. `spawnDeps` mirrors the deps the
1528
+ // `makeSpawnSubAgentTool` factory accepts (see registration below
1529
+ // this block). The legacy per-call `runChild` closure that lived
1530
+ // here pre-2R has been deleted; the primitive owns child
1531
+ // construction now.
1173
1532
  toolRegistry.register((0, index_1.makeSubagentFanoutTool)({
1174
1533
  logger: bootLogger.child('subagent'),
1175
1534
  resolveActiveModel: () => ({ providerId, modelId }),
1176
1535
  aggregatorAdapter: adapter,
1536
+ spawnDeps: {
1537
+ toolRegistry,
1538
+ parentToolContext: {
1539
+ cwd: process.cwd(),
1540
+ paths,
1541
+ sessions: sessionManager,
1542
+ memory: memoryManager,
1543
+ memoryGuard,
1544
+ ssrfProtection,
1545
+ tirithScanner,
1546
+ skillLoader,
1547
+ },
1548
+ parentProvider: adapter,
1549
+ parentProviderId: providerId,
1550
+ parentModelId: modelId,
1551
+ resolveVerifiedFlag,
1552
+ resolveToolset,
1553
+ resolveMutates,
1554
+ runStore: replRunStore,
1555
+ instanceId: replInstanceId,
1556
+ logger: bootLogger.child('subagent'),
1557
+ },
1558
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the shared
1559
+ // `replParentRunRef` mutated by `ChatSession.runAgentTurn` so
1560
+ // fanout children get `spawned_from_run_id` populated. Returns
1561
+ // undefined between turns (ref cleared post-turn), matching the
1562
+ // pre-2Q-B behaviour for slash-command-triggered spawns.
1563
+ resolveParentRunId: () => replParentRunRef.runId ?? undefined,
1564
+ resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
1177
1565
  resolveProviders: () => {
1178
1566
  // When the parent uses FallbackAdapter, expose every key-present
1179
1567
  // slot's (providerId, modelId) so rotation can spread children
@@ -1194,110 +1582,68 @@ async function buildAgentRuntime(cliOpts, opts) {
1194
1582
  }
1195
1583
  return [{ providerId, modelId }];
1196
1584
  },
1197
- runChild: async (childOpts) => {
1198
- // Per-child context: paths / skillLoader / memoryManager / processes
1199
- // are SAFE to share (read-only or per-call by design). The approval
1200
- // engine is intentionally OMITTED — N children competing for one
1201
- // stdin REPL would deadlock.
1202
- const childCtx = {
1203
- cwd: process.cwd(),
1204
- paths,
1205
- sessions: sessionManager,
1206
- memory: memoryManager,
1207
- skillLoader,
1208
- // approvalEngine, ssrfProtection, tirithScanner, memoryGuard:
1209
- // SSRF + Tirith would be safe to share but adding them now
1210
- // expands the per-child surface; keep lean for v4.1-subagent.1
1211
- // and revisit when fanout actually exercises network or shell
1212
- // tools (gated by ALLOW_DESTRUCTIVE).
1213
- };
1214
- // Filter the tool surface. Default-safe: read-only tools only.
1215
- // AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 mirrors the MCP env from
1216
- // v4.1-mcp — predictable, env-driven.
1217
- const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
1218
- process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
1219
- const childToolNames = [];
1220
- for (const name of toolRegistry.list()) {
1221
- const h = toolRegistry.get(name);
1222
- if (!h)
1223
- continue;
1224
- if (h.mutates && !allowDestructive)
1225
- continue;
1226
- // Avoid recursive fanout this phase — children cannot spawn
1227
- // their own children. Recursion was capped at depth 1 by
1228
- // default in prior multi-agent systems for the same reason;
1229
- // v3 starved nested spawns.
1230
- if (name === 'subagent_fanout')
1231
- continue;
1232
- childToolNames.push(name);
1233
- }
1234
- const childExecutor = toolRegistry.buildExecutor(childCtx);
1235
- const childTools = childToolNames
1236
- .map((n) => toolRegistry.get(n)?.schema)
1237
- .filter((s) => !!s);
1238
- // Provider isolation: clone the FallbackAdapter so per-child
1239
- // rate-limit state doesn't pollute the parent or siblings.
1240
- // Non-Fallback adapters are stateless by spec (providers/v4/
1241
- // types.ts:190) so direct reuse is safe.
1242
- const childProvider = adapter instanceof providerFallback_1.FallbackAdapter
1243
- ? adapter.clone()
1244
- : adapter;
1245
- // Build per-child AidenAgent. Skip the moat layers (PlannerGuard,
1246
- // HonestyEnforcement, SkillTeacher, SkillEnforcementTracker) —
1247
- // they're parent-loop concerns and add cost without value at the
1248
- // child scale. Skip promptBuilder too: children get a SHORT
1249
- // system prompt (brief identity + role) instead of the parent's
1250
- // full SOUL.md + 72-skills inventory + memory snapshot. The
1251
- // tradeoff is deliberate — children answer the GOAL, not "be
1252
- // Aiden". With the full prompt, trivial queries take 30s+ for
1253
- // children to generate verbose self-introductions; the lean
1254
- // child prompt brings n=2 trivial fanouts under 12s. Parent
1255
- // should pass any context children genuinely need via the
1256
- // `query` / `tasks[].context` argument.
1257
- const child = new aidenAgent_1.AidenAgent({
1258
- provider: childProvider,
1259
- tools: childTools,
1260
- toolExecutor: childExecutor,
1261
- maxTurns: childOpts.maxIterations,
1262
- providerId: childOpts.provider.providerId,
1263
- modelId: childOpts.provider.modelId,
1264
- // No promptBuilder — childSystemPrompt prepended manually below.
1265
- // No fallback strategy — child failures bubble up to the
1266
- // orchestrator, which surfaces them in the result envelope.
1267
- });
1268
- // Honour the abort signal — if the parent aborts mid-call (or the
1269
- // per-child timeout fires), short-circuit before dispatching to
1270
- // the provider. AidenAgent doesn't take an AbortSignal directly;
1271
- // the AbortController plumbing through fetch is the
1272
- // v4.1-subagent.2 / v4.2 hardening pass. Pre-check here for the
1273
- // synchronous path.
1274
- if (childOpts.signal.aborted) {
1275
- throw new Error('aborted before dispatch');
1276
- }
1277
- // Brief, role-aware system prompt — drops 5KB+ of Aiden identity
1278
- // boilerplate that would otherwise inflate every child to 30s+
1279
- // wall-clock for a trivial query. The parent agent retains the
1280
- // full prompt when it's the orchestrator; children answer the
1281
- // goal directly.
1282
- const roleLine = childOpts.role
1283
- ? `Role: ${childOpts.role}. `
1284
- : '';
1285
- const childSystemPrompt = `You are one of ${childOpts.index >= 0 ? 'N' : '?'} parallel subagents. ` +
1286
- `${roleLine}Answer the user's request concisely. Use available tools when ` +
1287
- `the answer requires real-world information you don't have memorized.`;
1288
- const history = [
1289
- { role: 'system', content: childSystemPrompt },
1290
- { role: 'user', content: childOpts.prompt },
1291
- ];
1292
- const result = await child.runConversation(history);
1293
- return result.finalContent;
1294
- },
1295
1585
  }));
1296
1586
  bootLogger.child('subagent').info('subagent_fanout: wired (replaces stub)', {
1297
1587
  providerId,
1298
1588
  modelId,
1299
1589
  fallback: adapter instanceof providerFallback_1.FallbackAdapter ? 'FallbackAdapter' : 'direct',
1300
1590
  });
1591
+ // ── v4.6 Phase 1 — register spawn_sub_agent (REPL only) ────────────────
1592
+ //
1593
+ // The new single-child synchronous primitive. Coexists with
1594
+ // subagent_fanout (Q9 — additive in Phase 1; Phase 2 will refactor
1595
+ // fanout to call this primitive N times).
1596
+ //
1597
+ // Wired here, AFTER `agent` and `toolExecutor` are in scope, because
1598
+ // the child builder needs the parent agent's reference (to read
1599
+ // `getCurrentSignal()` at dispatch time per the agent-instance signal
1600
+ // pattern) and the parent's tool context for ssrf/tirith/memory/etc.
1601
+ //
1602
+ // Deliberately NOT registered in cli/v4/daemonAgentBuilder.ts —
1603
+ // daemon-fired agents don't expose spawn_sub_agent in their tool
1604
+ // catalog (Q6 lock).
1605
+ toolRegistry.register((0, spawnSubAgentTool_1.makeSpawnSubAgentTool)({
1606
+ parentAgent: agent,
1607
+ toolRegistry,
1608
+ parentToolContext: {
1609
+ cwd: process.cwd(),
1610
+ paths,
1611
+ sessions: sessionManager,
1612
+ memory: memoryManager,
1613
+ memoryGuard,
1614
+ // approvalEngine intentionally OMITTED — the child builder
1615
+ // constructs its own auto-deny ApprovalEngine. Listing it here
1616
+ // would be ignored (childBuilder overrides via spread), but
1617
+ // keeping it out makes the intent explicit.
1618
+ ssrfProtection,
1619
+ tirithScanner,
1620
+ skillLoader,
1621
+ },
1622
+ parentProvider: adapter,
1623
+ parentProviderId: providerId,
1624
+ parentModelId: modelId,
1625
+ resolveVerifiedFlag,
1626
+ resolveToolset,
1627
+ resolveMutates,
1628
+ runStore: replRunStore,
1629
+ instanceId: replInstanceId,
1630
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the same
1631
+ // shared `replParentRunRef` the fanout factory above uses;
1632
+ // ChatSession.runAgentTurn populates it before each turn
1633
+ // dispatches. Returns undefined between turns so spawns from
1634
+ // slash-command handlers stay top-level (consistent with
1635
+ // pre-2Q-B observable behaviour).
1636
+ resolveParentRunId: () => replParentRunRef.runId ?? undefined,
1637
+ resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
1638
+ // v4.6 Phase 1 observability — info-level traces for spec at
1639
+ // invocation, child-tools count, completion, and per-tool-call
1640
+ // run_events on the child's runs row.
1641
+ logger: bootLogger.child('subagent'),
1642
+ }));
1643
+ bootLogger.child('subagent').info('spawn_sub_agent: wired (REPL only)', {
1644
+ instanceId: replInstanceId,
1645
+ dbPath: (0, daemonConfig_1.daemonDbPath)(paths.root),
1646
+ });
1301
1647
  // ── Phase v4.1-2.1: gateway message processor ────────────────────
1302
1648
  //
1303
1649
  // Channel adapters call `gateway.routeMessage(...)` for every inbound
@@ -1489,10 +1835,40 @@ async function buildAgentRuntime(cliOpts, opts) {
1489
1835
  pluginLoader,
1490
1836
  exploreMode,
1491
1837
  channelManager,
1838
+ daemonAgentBuilder,
1839
+ // v4.6 Phase 2Q-B — REPL parent-run wiring.
1840
+ replRunStore,
1841
+ replInstanceId,
1842
+ replParentRunRef,
1492
1843
  };
1493
1844
  }
1494
1845
  async function runInteractiveChat(cliOpts, opts) {
1495
1846
  const runtime = await buildAgentRuntime(cliOpts, opts);
1847
+ // v4.5 Phase 7c — install the REAL agent runner now that the
1848
+ // REPL agent is built. The daemon foundation already came up
1849
+ // earlier (top of REPL action handler) with the Phase 5a
1850
+ // placeholder runner serving claims. This call atomically swaps
1851
+ // the dispatcher's runner from placeholder → real; the next
1852
+ // claim uses `runtime.daemonAgentBuilder` to construct a real
1853
+ // AidenAgent per fire.
1854
+ try {
1855
+ if (process.env.AIDEN_DAEMON === '1') {
1856
+ const { getDaemonHandle, installDaemonAgentBuilder } = await Promise.resolve().then(() => __importStar(require('../../core/v4/daemon/bootstrap')));
1857
+ const handle = getDaemonHandle();
1858
+ if (handle && handle.active) {
1859
+ const ok = installDaemonAgentBuilder(handle, runtime.daemonAgentBuilder, { provider: runtime.providerId, model: runtime.modelId });
1860
+ if (!ok) {
1861
+ console.warn('[daemon] real-runner install returned false — placeholder still active');
1862
+ }
1863
+ }
1864
+ }
1865
+ }
1866
+ catch (e) {
1867
+ // Fail-loud but non-fatal — install failure leaves the
1868
+ // placeholder runner active; the REPL still works and the
1869
+ // daemon's rails still serve triggers.
1870
+ console.error('[daemon] install agent builder failed: ' + (e instanceof Error ? e.message : String(e)));
1871
+ }
1496
1872
  const sessionOpts = {
1497
1873
  agent: runtime.agent,
1498
1874
  display: runtime.display,
@@ -1530,6 +1906,14 @@ async function runInteractiveChat(cliOpts, opts) {
1530
1906
  // when /quit fires the auto-summary path.
1531
1907
  memoryManager: runtime.memoryManager,
1532
1908
  memoryGuard: runtime.memoryGuard,
1909
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. ChatSession.runAgentTurn
1910
+ // writes a runs row per turn and stores its id into
1911
+ // `replParentRunRef`; the spawn / fanout factories above read the
1912
+ // ref via their `resolveParentRunId` / `resolveParentSessionId`
1913
+ // callbacks so children get `spawned_from_run_id` populated.
1914
+ replRunStore: runtime.replRunStore,
1915
+ replInstanceId: runtime.replInstanceId,
1916
+ replParentRunRef: runtime.replParentRunRef,
1533
1917
  };
1534
1918
  if (cliOpts.tui) {
1535
1919
  await (0, aidenTUI_1.runTuiMode)({