aiden-runtime 4.5.0 → 4.6.1

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 (50) hide show
  1. package/README.md +17 -2
  2. package/dist/cli/v4/aidenCLI.js +207 -100
  3. package/dist/cli/v4/chatSession.js +120 -0
  4. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
  5. package/dist/cli/v4/commands/fanout.js +42 -59
  6. package/dist/cli/v4/commands/help.js +8 -0
  7. package/dist/cli/v4/commands/index.js +21 -1
  8. package/dist/cli/v4/commands/mcp.js +80 -54
  9. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  10. package/dist/cli/v4/commands/recovery.js +122 -0
  11. package/dist/cli/v4/commands/runs.js +22 -2
  12. package/dist/cli/v4/commands/spawnPause.js +93 -0
  13. package/dist/cli/v4/commands/walkthrough.js +140 -0
  14. package/dist/cli/v4/daemonAgentBuilder.js +4 -1
  15. package/dist/cli/v4/defaultSoul.js +1 -1
  16. package/dist/cli/v4/onboarding/disclaimer.js +162 -0
  17. package/dist/cli/v4/onboarding/loading.js +208 -0
  18. package/dist/cli/v4/onboarding/providerPicker.js +126 -0
  19. package/dist/cli/v4/onboarding/successScreen.js +68 -0
  20. package/dist/cli/v4/repl/firstRunHint.js +107 -0
  21. package/dist/cli/v4/setupWizard.js +201 -31
  22. package/dist/core/v4/aidenAgent.js +219 -1
  23. package/dist/core/v4/daemon/bootstrap.js +47 -0
  24. package/dist/core/v4/daemon/db/migrations.js +66 -0
  25. package/dist/core/v4/daemon/runStore.js +33 -3
  26. package/dist/core/v4/providerFallback.js +35 -2
  27. package/dist/core/v4/providers/modelFetch.js +179 -0
  28. package/dist/core/v4/providers/probe.js +275 -0
  29. package/dist/core/v4/runtimeToggles.js +30 -3
  30. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  31. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  32. package/dist/core/v4/subagent/childBuilder.js +391 -0
  33. package/dist/core/v4/subagent/fanout.js +75 -51
  34. package/dist/core/v4/subagent/spawnPause.js +191 -0
  35. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  36. package/dist/core/v4/toolRegistry.js +19 -3
  37. package/dist/core/v4/ui/banner.js +133 -0
  38. package/dist/core/v4/ui/theme.js +164 -0
  39. package/dist/core/version.js +1 -1
  40. package/dist/moat/plannerGuard.js +29 -0
  41. package/dist/providers/v4/anthropicAdapter.js +31 -3
  42. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  43. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  44. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  45. package/dist/tools/v4/index.js +17 -3
  46. package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
  47. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  48. package/dist/tools/v4/subagent/subagentFanout.js +53 -1
  49. package/dist/tools/v4/ui/_uiSmokeTool.js +60 -0
  50. package/package.json +7 -3
package/README.md CHANGED
@@ -10,14 +10,15 @@
10
10
 
11
11
  Autonomous AI Engine — local-first, Windows-native, yours to own
12
12
 
13
- 74 skills · 59 tools · 19 providers · 9 channels · AGPL-3.0
13
+ 74 skills · 60 tools · 19 providers · 9 channels · AGPL-3.0
14
14
 
15
15
  Windows · Linux · WSL · macOS (API Mode)
16
16
  ```
17
17
 
18
18
  <p align="center">
19
- <a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/badge/version-v4.5.0-f97316?style=for-the-badge" alt="v4.5.0" /></a>
19
+ <a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/badge/version-v4.6.0-f97316?style=for-the-badge" alt="v4.6.0" /></a>
20
20
  <a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/v/aiden-runtime?color=f97316&label=npm&style=for-the-badge" alt="npm" /></a>
21
+ <a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/dm/aiden-runtime?color=f97316&label=downloads&style=for-the-badge" alt="npm downloads" /></a>
21
22
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-orange?style=for-the-badge" alt="License: AGPL-3.0" /></a>
22
23
  <a href="https://discord.gg/gMZ3hUnQTm"><img src="https://img.shields.io/badge/chat-discord-7289da?logo=discord&logoColor=white&style=for-the-badge" alt="Discord" /></a>
23
24
  <a href="https://github.com/taracodlabs/aiden/stargazers"><img src="https://img.shields.io/github/stars/taracodlabs/aiden?style=for-the-badge&color=f9d71c" alt="Stars" /></a>
@@ -33,6 +34,20 @@ Windows · Linux · WSL · macOS (API Mode)
33
34
 
34
35
  ---
35
36
 
37
+ ## What's new in v4.6
38
+
39
+ Aiden now spawns workers and learns from itself.
40
+
41
+ - **Sub-agents.** `spawn_sub_agent` runs a focused child with an isolated context + intersected toolset; `subagent_fanout` runs N children in parallel (ensemble or partition) with merge strategies (`all` / `vote` / `pick-best` / `combine`) and provider rotation across configured fallback slots.
42
+ - **Operator kill-switch.** `/spawn-pause on|off|status` blocks new sub-agent spawning while in-flight children continue. Marker file at `~/.aiden/spawn.paused` so the state survives restart and is shared across REPL, daemon, and MCP runtimes. Optional reason field captured in the typed `SUBAGENT_SPAWN_PAUSED` error envelope.
43
+ - **Self-improvement loop foundation.** TCE classifications + recoveries persist to two new SQLite tables (`failure_signatures`, `recovery_reports`); `/recovery list|show|clear` surfaces recurring failure patterns across sessions.
44
+ - **REPL parent-run lineage.** Each REPL turn writes its own `runs` row; sub-agent children link back via `spawned_from_run_id`. `aiden runs list` hides children by default and shows a `(N children, M OK)` badge per parent; `--include-children` flips to flat view.
45
+ - **PlannerGuard opt-in.** The keyword-based per-turn tool narrower is OFF by default in v4.6 (modern models pick well from the full catalog). Enable via `/planner-guard on` or `AIDEN_PLANNER_GUARD=1` for smaller local models.
46
+
47
+ Phase 2 also fixed an MCP-mode `subagent_fanout` regression that had silently broken in the v4.5 refactor.
48
+
49
+ ---
50
+
36
51
  ## What's new in v4.5
37
52
 
38
53
  Aiden now wakes up by itself.
@@ -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");
@@ -502,6 +509,7 @@ async function main(argv, opts = {}) {
502
509
  .option('--source <src>', 'list: filter by trigger source (file/webhook/email/schedule/manual)')
503
510
  .option('--status <s>', 'list: filter by status (queued/running/completed/failed/cancelled/interrupted)')
504
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)')
505
513
  .action(async (action, posArgs, cmdObj) => {
506
514
  const { runRunsSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/runs')));
507
515
  const code = await runRunsSubcommand(action, posArgs ?? [], cmdObj, {
@@ -623,6 +631,60 @@ async function main(argv, opts = {}) {
623
631
  async function buildAgentRuntime(cliOpts, opts) {
624
632
  const paths = opts.pathsOverride ?? (0, paths_1.resolveAidenPaths)();
625
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
+ };
626
688
  // Phase 16c.2: load `paths.envFile` (the aiden-managed `.env` that
627
689
  // `setupWizard.ts::upsertEnvVar` writes to) into `process.env` BEFORE
628
690
  // any provider resolution. The bug: setup wrote keys to this file but
@@ -747,7 +809,28 @@ async function buildAgentRuntime(cliOpts, opts) {
747
809
  process.stdout.write(`\n${(0, providerDetection_1.summarizeDetection)(detection)}\n`);
748
810
  process.stdout.write('config.yaml is empty — let\'s pick a provider that matches.\n');
749
811
  }
750
- process.stdout.write('Launching setup wizard…\n\n');
812
+ // ONB1-WIRE — disclaimer + loading screens land BEFORE the wizard.
813
+ // Only inside this TTY-guarded branch (the non-TTY branch at the
814
+ // outer else already bails into explore mode). The detection
815
+ // summary above gives the user context for what was found; the
816
+ // disclaimer then asks for explicit consent before we walk them
817
+ // through setup. Declining exits 0 — the user chose not to
818
+ // continue, that's not an error.
819
+ //
820
+ // Lazy-required so the test harness paths that stub the wizard
821
+ // don't pay the load cost.
822
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
823
+ const { showDisclaimer } = require('./onboarding/disclaimer');
824
+ const disc = await showDisclaimer();
825
+ if (!disc.ok) {
826
+ // User typed 'n' / 'no'. The disclaimer already printed the
827
+ // friendly goodbye line; just exit cleanly.
828
+ process.exit(0);
829
+ }
830
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
831
+ const { runLoadingSequence, defaultLoadingSteps } = require('./onboarding/loading');
832
+ await runLoadingSequence(defaultLoadingSteps(paths));
833
+ process.stdout.write('\n');
751
834
  const result = await (0, setupWizard_1.runSetupWizard)({ paths });
752
835
  // Phase 30.2.1: three exit states.
753
836
  if (result.status === 'exited') {
@@ -1238,7 +1321,11 @@ async function buildAgentRuntime(cliOpts, opts) {
1238
1321
  // ── Build agent with all moat layers attached ────────────────────────
1239
1322
  const agent = new aidenAgent_1.AidenAgent({
1240
1323
  provider: adapter,
1241
- tools: toolRegistry.getSchemas(),
1324
+ // v4.6 Phase 1 — 'repl' context filter excludes tools tagged
1325
+ // daemon-only (none today) and INCLUDES tools tagged repl-only
1326
+ // (e.g. spawn_sub_agent, registered after this line). Tools with
1327
+ // no `contexts` field default to visible in both contexts.
1328
+ tools: toolRegistry.getSchemas(undefined, 'repl'),
1242
1329
  toolExecutor,
1243
1330
  maxTurns: config.getValue('agent.max_turns', 90),
1244
1331
  auxiliaryClient,
@@ -1426,6 +1513,21 @@ async function buildAgentRuntime(cliOpts, opts) {
1426
1513
  // Wire the gateway singleton's logger BEFORE registering its processor
1427
1514
  // so register / unregister channel events are scoped correctly.
1428
1515
  gateway_1.gateway.attachLogger(bootLogger.child('gateway'));
1516
+ // v4.6 Phase 3A — startup probe for the spawn-pause kill-switch.
1517
+ // The state was initialised early (line ~740) before tool wiring.
1518
+ // Now that bootLogger exists, emit a visible warning so an
1519
+ // operator who forgot they paused in a prior session learns
1520
+ // immediately rather than puzzling at silent rejected fanouts.
1521
+ if (spawnPauseBootStatus) {
1522
+ const s = spawnPauseBootStatus;
1523
+ const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
1524
+ bootLogger.warn(`spawn_sub_agent / subagent_fanout are PAUSED${reasonSuffix}. ` +
1525
+ 'Run /spawn-pause off to resume.', {
1526
+ pausedAt: s.pausedAt ?? null,
1527
+ pausedBy: s.pausedBy ?? null,
1528
+ durationMs: s.durationMs ?? null,
1529
+ });
1530
+ }
1429
1531
  // ── Phase v4.1-subagent.1 — replace subagent_fanout stub with wired version
1430
1532
  //
1431
1533
  // tools/v4/index.ts registers a stub at boot so the schema is visible
@@ -1442,10 +1544,45 @@ async function buildAgentRuntime(cliOpts, opts) {
1442
1544
  // shared subsystems (registry, skillLoader, paths, memoryManager,
1443
1545
  // promptBuilder, promptBuilderOptions) are read-only and pass by
1444
1546
  // reference.
1547
+ // v4.6 Phase 2Q-A — `runFanout` routes each child through
1548
+ // `spawnSubAgent`. `spawnDeps` mirrors the deps the
1549
+ // `makeSpawnSubAgentTool` factory accepts (see registration below
1550
+ // this block). The legacy per-call `runChild` closure that lived
1551
+ // here pre-2R has been deleted; the primitive owns child
1552
+ // construction now.
1445
1553
  toolRegistry.register((0, index_1.makeSubagentFanoutTool)({
1446
1554
  logger: bootLogger.child('subagent'),
1447
1555
  resolveActiveModel: () => ({ providerId, modelId }),
1448
1556
  aggregatorAdapter: adapter,
1557
+ spawnDeps: {
1558
+ toolRegistry,
1559
+ parentToolContext: {
1560
+ cwd: process.cwd(),
1561
+ paths,
1562
+ sessions: sessionManager,
1563
+ memory: memoryManager,
1564
+ memoryGuard,
1565
+ ssrfProtection,
1566
+ tirithScanner,
1567
+ skillLoader,
1568
+ },
1569
+ parentProvider: adapter,
1570
+ parentProviderId: providerId,
1571
+ parentModelId: modelId,
1572
+ resolveVerifiedFlag,
1573
+ resolveToolset,
1574
+ resolveMutates,
1575
+ runStore: replRunStore,
1576
+ instanceId: replInstanceId,
1577
+ logger: bootLogger.child('subagent'),
1578
+ },
1579
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the shared
1580
+ // `replParentRunRef` mutated by `ChatSession.runAgentTurn` so
1581
+ // fanout children get `spawned_from_run_id` populated. Returns
1582
+ // undefined between turns (ref cleared post-turn), matching the
1583
+ // pre-2Q-B behaviour for slash-command-triggered spawns.
1584
+ resolveParentRunId: () => replParentRunRef.runId ?? undefined,
1585
+ resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
1449
1586
  resolveProviders: () => {
1450
1587
  // When the parent uses FallbackAdapter, expose every key-present
1451
1588
  // slot's (providerId, modelId) so rotation can spread children
@@ -1466,110 +1603,68 @@ async function buildAgentRuntime(cliOpts, opts) {
1466
1603
  }
1467
1604
  return [{ providerId, modelId }];
1468
1605
  },
1469
- runChild: async (childOpts) => {
1470
- // Per-child context: paths / skillLoader / memoryManager / processes
1471
- // are SAFE to share (read-only or per-call by design). The approval
1472
- // engine is intentionally OMITTED — N children competing for one
1473
- // stdin REPL would deadlock.
1474
- const childCtx = {
1475
- cwd: process.cwd(),
1476
- paths,
1477
- sessions: sessionManager,
1478
- memory: memoryManager,
1479
- skillLoader,
1480
- // approvalEngine, ssrfProtection, tirithScanner, memoryGuard:
1481
- // SSRF + Tirith would be safe to share but adding them now
1482
- // expands the per-child surface; keep lean for v4.1-subagent.1
1483
- // and revisit when fanout actually exercises network or shell
1484
- // tools (gated by ALLOW_DESTRUCTIVE).
1485
- };
1486
- // Filter the tool surface. Default-safe: read-only tools only.
1487
- // AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 mirrors the MCP env from
1488
- // v4.1-mcp — predictable, env-driven.
1489
- const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
1490
- process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
1491
- const childToolNames = [];
1492
- for (const name of toolRegistry.list()) {
1493
- const h = toolRegistry.get(name);
1494
- if (!h)
1495
- continue;
1496
- if (h.mutates && !allowDestructive)
1497
- continue;
1498
- // Avoid recursive fanout this phase — children cannot spawn
1499
- // their own children. Recursion was capped at depth 1 by
1500
- // default in prior multi-agent systems for the same reason;
1501
- // v3 starved nested spawns.
1502
- if (name === 'subagent_fanout')
1503
- continue;
1504
- childToolNames.push(name);
1505
- }
1506
- const childExecutor = toolRegistry.buildExecutor(childCtx);
1507
- const childTools = childToolNames
1508
- .map((n) => toolRegistry.get(n)?.schema)
1509
- .filter((s) => !!s);
1510
- // Provider isolation: clone the FallbackAdapter so per-child
1511
- // rate-limit state doesn't pollute the parent or siblings.
1512
- // Non-Fallback adapters are stateless by spec (providers/v4/
1513
- // types.ts:190) so direct reuse is safe.
1514
- const childProvider = adapter instanceof providerFallback_1.FallbackAdapter
1515
- ? adapter.clone()
1516
- : adapter;
1517
- // Build per-child AidenAgent. Skip the moat layers (PlannerGuard,
1518
- // HonestyEnforcement, SkillTeacher, SkillEnforcementTracker) —
1519
- // they're parent-loop concerns and add cost without value at the
1520
- // child scale. Skip promptBuilder too: children get a SHORT
1521
- // system prompt (brief identity + role) instead of the parent's
1522
- // full SOUL.md + 72-skills inventory + memory snapshot. The
1523
- // tradeoff is deliberate — children answer the GOAL, not "be
1524
- // Aiden". With the full prompt, trivial queries take 30s+ for
1525
- // children to generate verbose self-introductions; the lean
1526
- // child prompt brings n=2 trivial fanouts under 12s. Parent
1527
- // should pass any context children genuinely need via the
1528
- // `query` / `tasks[].context` argument.
1529
- const child = new aidenAgent_1.AidenAgent({
1530
- provider: childProvider,
1531
- tools: childTools,
1532
- toolExecutor: childExecutor,
1533
- maxTurns: childOpts.maxIterations,
1534
- providerId: childOpts.provider.providerId,
1535
- modelId: childOpts.provider.modelId,
1536
- // No promptBuilder — childSystemPrompt prepended manually below.
1537
- // No fallback strategy — child failures bubble up to the
1538
- // orchestrator, which surfaces them in the result envelope.
1539
- });
1540
- // Honour the abort signal — if the parent aborts mid-call (or the
1541
- // per-child timeout fires), short-circuit before dispatching to
1542
- // the provider. AidenAgent doesn't take an AbortSignal directly;
1543
- // the AbortController plumbing through fetch is the
1544
- // v4.1-subagent.2 / v4.2 hardening pass. Pre-check here for the
1545
- // synchronous path.
1546
- if (childOpts.signal.aborted) {
1547
- throw new Error('aborted before dispatch');
1548
- }
1549
- // Brief, role-aware system prompt — drops 5KB+ of Aiden identity
1550
- // boilerplate that would otherwise inflate every child to 30s+
1551
- // wall-clock for a trivial query. The parent agent retains the
1552
- // full prompt when it's the orchestrator; children answer the
1553
- // goal directly.
1554
- const roleLine = childOpts.role
1555
- ? `Role: ${childOpts.role}. `
1556
- : '';
1557
- const childSystemPrompt = `You are one of ${childOpts.index >= 0 ? 'N' : '?'} parallel subagents. ` +
1558
- `${roleLine}Answer the user's request concisely. Use available tools when ` +
1559
- `the answer requires real-world information you don't have memorized.`;
1560
- const history = [
1561
- { role: 'system', content: childSystemPrompt },
1562
- { role: 'user', content: childOpts.prompt },
1563
- ];
1564
- const result = await child.runConversation(history);
1565
- return result.finalContent;
1566
- },
1567
1606
  }));
1568
1607
  bootLogger.child('subagent').info('subagent_fanout: wired (replaces stub)', {
1569
1608
  providerId,
1570
1609
  modelId,
1571
1610
  fallback: adapter instanceof providerFallback_1.FallbackAdapter ? 'FallbackAdapter' : 'direct',
1572
1611
  });
1612
+ // ── v4.6 Phase 1 — register spawn_sub_agent (REPL only) ────────────────
1613
+ //
1614
+ // The new single-child synchronous primitive. Coexists with
1615
+ // subagent_fanout (Q9 — additive in Phase 1; Phase 2 will refactor
1616
+ // fanout to call this primitive N times).
1617
+ //
1618
+ // Wired here, AFTER `agent` and `toolExecutor` are in scope, because
1619
+ // the child builder needs the parent agent's reference (to read
1620
+ // `getCurrentSignal()` at dispatch time per the agent-instance signal
1621
+ // pattern) and the parent's tool context for ssrf/tirith/memory/etc.
1622
+ //
1623
+ // Deliberately NOT registered in cli/v4/daemonAgentBuilder.ts —
1624
+ // daemon-fired agents don't expose spawn_sub_agent in their tool
1625
+ // catalog (Q6 lock).
1626
+ toolRegistry.register((0, spawnSubAgentTool_1.makeSpawnSubAgentTool)({
1627
+ parentAgent: agent,
1628
+ toolRegistry,
1629
+ parentToolContext: {
1630
+ cwd: process.cwd(),
1631
+ paths,
1632
+ sessions: sessionManager,
1633
+ memory: memoryManager,
1634
+ memoryGuard,
1635
+ // approvalEngine intentionally OMITTED — the child builder
1636
+ // constructs its own auto-deny ApprovalEngine. Listing it here
1637
+ // would be ignored (childBuilder overrides via spread), but
1638
+ // keeping it out makes the intent explicit.
1639
+ ssrfProtection,
1640
+ tirithScanner,
1641
+ skillLoader,
1642
+ },
1643
+ parentProvider: adapter,
1644
+ parentProviderId: providerId,
1645
+ parentModelId: modelId,
1646
+ resolveVerifiedFlag,
1647
+ resolveToolset,
1648
+ resolveMutates,
1649
+ runStore: replRunStore,
1650
+ instanceId: replInstanceId,
1651
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the same
1652
+ // shared `replParentRunRef` the fanout factory above uses;
1653
+ // ChatSession.runAgentTurn populates it before each turn
1654
+ // dispatches. Returns undefined between turns so spawns from
1655
+ // slash-command handlers stay top-level (consistent with
1656
+ // pre-2Q-B observable behaviour).
1657
+ resolveParentRunId: () => replParentRunRef.runId ?? undefined,
1658
+ resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
1659
+ // v4.6 Phase 1 observability — info-level traces for spec at
1660
+ // invocation, child-tools count, completion, and per-tool-call
1661
+ // run_events on the child's runs row.
1662
+ logger: bootLogger.child('subagent'),
1663
+ }));
1664
+ bootLogger.child('subagent').info('spawn_sub_agent: wired (REPL only)', {
1665
+ instanceId: replInstanceId,
1666
+ dbPath: (0, daemonConfig_1.daemonDbPath)(paths.root),
1667
+ });
1573
1668
  // ── Phase v4.1-2.1: gateway message processor ────────────────────
1574
1669
  //
1575
1670
  // Channel adapters call `gateway.routeMessage(...)` for every inbound
@@ -1762,6 +1857,10 @@ async function buildAgentRuntime(cliOpts, opts) {
1762
1857
  exploreMode,
1763
1858
  channelManager,
1764
1859
  daemonAgentBuilder,
1860
+ // v4.6 Phase 2Q-B — REPL parent-run wiring.
1861
+ replRunStore,
1862
+ replInstanceId,
1863
+ replParentRunRef,
1765
1864
  };
1766
1865
  }
1767
1866
  async function runInteractiveChat(cliOpts, opts) {
@@ -1828,6 +1927,14 @@ async function runInteractiveChat(cliOpts, opts) {
1828
1927
  // when /quit fires the auto-summary path.
1829
1928
  memoryManager: runtime.memoryManager,
1830
1929
  memoryGuard: runtime.memoryGuard,
1930
+ // v4.6 Phase 2Q-B — REPL parent-run wiring. ChatSession.runAgentTurn
1931
+ // writes a runs row per turn and stores its id into
1932
+ // `replParentRunRef`; the spawn / fanout factories above read the
1933
+ // ref via their `resolveParentRunId` / `resolveParentSessionId`
1934
+ // callbacks so children get `spawned_from_run_id` populated.
1935
+ replRunStore: runtime.replRunStore,
1936
+ replInstanceId: runtime.replInstanceId,
1937
+ replParentRunRef: runtime.replParentRunRef,
1831
1938
  };
1832
1939
  if (cliOpts.tui) {
1833
1940
  await (0, aidenTUI_1.runTuiMode)({
@@ -922,6 +922,43 @@ class ChatSession {
922
922
  const baseHistory = newHistory.length > 0
923
923
  ? [...this.history, ...newHistory, userMsg]
924
924
  : [...this.history, userMsg];
925
+ // v4.6 Phase 2Q-B — REPL parent-run row (best-effort).
926
+ //
927
+ // Insert a `runs` row tagged with this REPL session BEFORE the
928
+ // agent loop dispatches. Capture the row id into the shared
929
+ // `replParentRunRef` so any `spawn_sub_agent` / `subagent_fanout`
930
+ // child this turn produces can link back via
931
+ // `spawned_from_run_id`. The ref is cleared in the catch /
932
+ // success paths below regardless of outcome.
933
+ //
934
+ // Defensive: a runStore write failure (locked DB, schema drift,
935
+ // etc.) must NOT crash the REPL — every persistence call here is
936
+ // wrapped in try/catch and reduces to a logged warning. The
937
+ // user-facing turn still runs.
938
+ let replRunId = null;
939
+ const replRunStore = this.opts.replRunStore;
940
+ const replInstanceId = this.opts.replInstanceId;
941
+ const replParentRunRef = this.opts.replParentRunRef;
942
+ if (replRunStore && replInstanceId && this.sessionId) {
943
+ try {
944
+ replRunId = replRunStore.create({
945
+ sessionId: this.sessionId,
946
+ instanceId: replInstanceId,
947
+ status: 'running',
948
+ startedAt: turnStartedAt,
949
+ });
950
+ if (replParentRunRef) {
951
+ replParentRunRef.runId = replRunId;
952
+ replParentRunRef.sessionId = this.sessionId;
953
+ }
954
+ }
955
+ catch (err) {
956
+ // Logged once per turn; the user's chat is not interrupted.
957
+ // eslint-disable-next-line no-console
958
+ console.warn('[runs] failed to write REPL parent-run row:', err instanceof Error ? err.message : String(err));
959
+ replRunId = null;
960
+ }
961
+ }
925
962
  // Phase 16c: streaming gated on display.streaming config.
926
963
  // v4.1.4 Part 1.6: PRODUCTION DEFAULT FLIPPED FROM FALSE TO TRUE.
927
964
  // Streaming delivers the activity indicator, tool-row live tick,
@@ -1179,6 +1216,35 @@ class ChatSession {
1179
1216
  this.history = result.messages;
1180
1217
  this.totalUsage.inputTokens += result.totalUsage.inputTokens;
1181
1218
  this.totalUsage.outputTokens += result.totalUsage.outputTokens;
1219
+ // v4.6 Phase 2Q-B — finalize the REPL parent-run row on success.
1220
+ // `finishReason` from the agent loop maps directly into our DB
1221
+ // status: `stop` → completed; `interrupted` / `tool_loop` →
1222
+ // surface as 'interrupted' so it's visible in `runs list`;
1223
+ // `budget_exhausted` / `error` → failed. Wrapped in try/catch
1224
+ // so even a runStore write failure here can't crash the REPL.
1225
+ if (replRunStore && replRunId !== null) {
1226
+ try {
1227
+ const dbStatus = result.finishReason === 'stop' ? 'completed' :
1228
+ result.finishReason === 'interrupted' ? 'interrupted' :
1229
+ result.finishReason === 'tool_loop' ? 'interrupted' :
1230
+ 'failed';
1231
+ replRunStore.setStatus(replRunId, dbStatus, {
1232
+ finishReason: result.finishReason,
1233
+ completedAt: Date.now(),
1234
+ });
1235
+ }
1236
+ catch (err) {
1237
+ // eslint-disable-next-line no-console
1238
+ console.warn('[runs] failed to finalize REPL parent-run row:', err instanceof Error ? err.message : String(err));
1239
+ }
1240
+ }
1241
+ // Clear the shared ref so a subsequent turn (or stray
1242
+ // spawn/fanout dispatched between turns from a slash command
1243
+ // handler) doesn't see a stale parent id.
1244
+ if (replParentRunRef) {
1245
+ replParentRunRef.runId = null;
1246
+ replParentRunRef.sessionId = null;
1247
+ }
1182
1248
  // Phase 16d: surface inline confirmations for verified memory writes.
1183
1249
  // We MUST gate on verified=true (the post-write read flag from
1184
1250
  // MemoryGuard) — HonestyEnforcement uses the same flag to catch
@@ -1289,6 +1355,26 @@ class ChatSession {
1289
1355
  progressBar?.hide();
1290
1356
  if (streamingActive)
1291
1357
  this.opts.display.streamComplete();
1358
+ // v4.6 Phase 2Q-B — finalize REPL parent-run row on error.
1359
+ // Visible in `aiden runs list` as a failed top-level row so
1360
+ // operators can correlate a chat error with whatever children
1361
+ // it had already kicked off this turn.
1362
+ if (replRunStore && replRunId !== null) {
1363
+ try {
1364
+ replRunStore.setStatus(replRunId, 'failed', {
1365
+ finishReason: 'error',
1366
+ completedAt: Date.now(),
1367
+ });
1368
+ }
1369
+ catch (e2) {
1370
+ // eslint-disable-next-line no-console
1371
+ console.warn('[runs] failed to mark REPL parent-run failed:', e2 instanceof Error ? e2.message : String(e2));
1372
+ }
1373
+ }
1374
+ if (replParentRunRef) {
1375
+ replParentRunRef.runId = null;
1376
+ replParentRunRef.sessionId = null;
1377
+ }
1292
1378
  const msg = err?.message ?? String(err);
1293
1379
  // v4.1.3-prebump: classify the error so the suggestion below
1294
1380
  // points at the actual fix instead of the generic "/model or
@@ -1418,6 +1504,27 @@ class ChatSession {
1418
1504
  providerOk: !this.opts.unconfigured,
1419
1505
  version: version_1.VERSION,
1420
1506
  }) + '\n');
1507
+ // v4.6 Phase 3A — operator kill-switch indicator. Lands ABOVE
1508
+ // the blank-line + provider-source annotation so an operator
1509
+ // who paused in a prior session sees the state immediately on
1510
+ // boot, alongside the standard status pills. Single dim
1511
+ // warning line; no special chrome — the message itself is the
1512
+ // visual signal.
1513
+ try {
1514
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1515
+ const { getSpawnPause } = require('../../core/v4/subagent/spawnPause');
1516
+ const s = getSpawnPause().status();
1517
+ if (s.paused) {
1518
+ const reasonSuffix = s.reason ? ` · ${s.reason}` : '';
1519
+ const durationSuffix = typeof s.durationMs === 'number'
1520
+ ? ` · ${formatDuration(s.durationMs)}`
1521
+ : '';
1522
+ display.warn(`spawn-pause: ON${reasonSuffix}${durationSuffix} — use /spawn-pause off to resume`);
1523
+ }
1524
+ }
1525
+ catch {
1526
+ // Singleton not initialised (test stubs, etc.) — silently skip.
1527
+ }
1421
1528
  // v4.5 TUI polish — blank line so the status pills row doesn't
1422
1529
  // crowd the muted source annotation right beneath it.
1423
1530
  display.write('\n');
@@ -1493,6 +1600,19 @@ class ChatSession {
1493
1600
  await this.maybeShowBootUpdatePrompt();
1494
1601
  }
1495
1602
  catch { /* never let the update prompt crash boot */ }
1603
+ // ONB1 slice 9 — one-time first-run hint banner. Renders below
1604
+ // the boot card on the very first session after a successful
1605
+ // setup; dismissed when the user sends their first message or
1606
+ // runs /dismiss. Lazy-required so test-harness sessions that
1607
+ // omit `paths` don't pay the fs cost.
1608
+ try {
1609
+ if (this.opts.paths) {
1610
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1611
+ const { renderFirstRunHint } = require('./repl/firstRunHint');
1612
+ await renderFirstRunHint({ paths: this.opts.paths, out: process.stdout });
1613
+ }
1614
+ }
1615
+ catch { /* never let a missing marker crash boot */ }
1496
1616
  // Bottom prompt hint — final line of the boot card.
1497
1617
  display.write('\n');
1498
1618
  display.write(display.bottomPromptHint() + '\n');
@@ -31,12 +31,14 @@ const LABEL = {
31
31
  tce: 'TCE',
32
32
  browser_depth: 'Browser depth',
33
33
  suggestions: 'Suggestions',
34
+ planner_guard: 'Planner-Guard',
34
35
  };
35
36
  const CONFIG_DOTTED = {
36
37
  sandbox: 'runtime_toggles.sandbox',
37
38
  tce: 'runtime_toggles.tce',
38
39
  browser_depth: 'runtime_toggles.browser_depth',
39
40
  suggestions: 'runtime_toggles.suggestions',
41
+ planner_guard: 'runtime_toggles.planner_guard',
40
42
  };
41
43
  /**
42
44
  * Apply a toggle change. When `ctx.config` is wired, persists to