@bahulam/code 0.1.10 → 0.1.12

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 (52) hide show
  1. package/package.json +5 -2
  2. package/src/agents/loader.mjs +26 -4
  3. package/src/auth/bahulam-auth.mjs +2 -13
  4. package/src/auth/tarang-auth.mjs +313 -0
  5. package/src/commands/agent.mjs +7 -7
  6. package/src/commands/plugin-manage.mjs +449 -0
  7. package/src/commands/plugin.mjs +247 -0
  8. package/src/config/cli-args.mjs +16 -0
  9. package/src/config/env.mjs +2 -0
  10. package/src/config/hook-runner.mjs +8 -8
  11. package/src/config/memory-loader.mjs +7 -3
  12. package/src/config/settings-loader.mjs +5 -3
  13. package/src/core/attachments.mjs +2 -2
  14. package/src/core/background-tasks.mjs +186 -0
  15. package/src/core/headless.mjs +59 -3
  16. package/src/core/local-agent.mjs +10 -1
  17. package/src/core/local-store.mjs +10 -10
  18. package/src/core/paths.mjs +10 -96
  19. package/src/core/policy-resolver.mjs +1 -1
  20. package/src/core/project-context-loader.mjs +2 -2
  21. package/src/core/risk-tier.mjs +1 -0
  22. package/src/core/stream-client.mjs +148 -1
  23. package/src/core/system-prompt.mjs +31 -12
  24. package/src/core/tool-executor.mjs +457 -12
  25. package/src/local-service/agent-relay.mjs +139 -12
  26. package/src/local-service/server.mjs +345 -20
  27. package/src/orchestration/approval.mjs +30 -0
  28. package/src/orchestration/completion-triggers.mjs +40 -0
  29. package/src/orchestration/dispatch.mjs +118 -0
  30. package/src/orchestration/events.mjs +19 -0
  31. package/src/orchestration/graph.mjs +126 -0
  32. package/src/orchestration/node-runner.mjs +193 -0
  33. package/src/orchestration/runner.mjs +200 -0
  34. package/src/plugins/executor.mjs +121 -0
  35. package/src/plugins/loader.mjs +123 -123
  36. package/src/plugins/manifest.mjs +291 -0
  37. package/src/plugins/preflight.mjs +227 -0
  38. package/src/plugins/registry.mjs +233 -0
  39. package/src/plugins/state.mjs +290 -0
  40. package/src/terminal/agents.mjs +26 -4
  41. package/src/terminal/init.mjs +2 -2
  42. package/src/terminal/main.mjs +83 -4
  43. package/src/terminal/repl-explore.mjs +1 -1
  44. package/src/terminal/repl-render.mjs +67 -12
  45. package/src/terminal/repl-state.mjs +4 -2
  46. package/src/terminal/repl.mjs +621 -99
  47. package/src/tools/agent.mjs +6 -2
  48. package/src/tools/analyze-image.mjs +1 -1
  49. package/src/tools/project-overview.mjs +7 -7
  50. package/src/tools/registry.mjs +88 -4
  51. package/src/ui/slash-commands.mjs +19 -1
  52. package/src/ui/sub-agent.mjs +14 -8
@@ -64,6 +64,10 @@ import { SkillInstaller } from '../skills/installer.mjs';
64
64
  import { SkillsLoader } from '../skills/loader.mjs';
65
65
  import { openSkillsPicker, formatSkillsList } from './skills-picker.mjs';
66
66
  import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, syncAgentsToBackend } from '../agents/scaffold.mjs';
67
+ import { listLocalWorkflows } from '../agents/workflow_scaffold.mjs';
68
+ import { dispatch } from '../orchestration/dispatch.mjs';
69
+ import { registerJobCompletionDispatch } from '../orchestration/completion-triggers.mjs';
70
+ import { PluginRegistry } from '../plugins/registry.mjs';
67
71
  import { SessionManager } from '../core/session-manager.mjs';
68
72
  import { parseArgs } from '../config/cli-args.mjs';
69
73
  import { pickModelOverridesForm } from './repl-model-form.mjs';
@@ -114,7 +118,7 @@ import {
114
118
  flushPendingHead,
115
119
  isInlineOutcomeTool,
116
120
  pushSubAgentWindowLine,
117
- rebuildSubAgentWindow,
121
+ rebuildSubAgentWindowGroups,
118
122
  renderBlockBoundary,
119
123
  renderExploreRun,
120
124
  renderFileDiffEvent,
@@ -875,7 +879,7 @@ function handleAttachmentsCommand(rest = '', ctx) {
875
879
  }
876
880
 
877
881
  async function confirmVisionUpload(ctx, attachments, { skip = false } = {}) {
878
- if (skip || process.env.KEPLER_VISION_CONFIRM === '0' || process.env.KEPLER_VISION_CONFIRM === 'false') {
882
+ if (skip || process.env.BAHULAM_VISION_CONFIRM === '0' || process.env.BAHULAM_VISION_CONFIRM === 'false') {
879
883
  return true;
880
884
  }
881
885
  if (!ctx?._rl || !process.stdin.isTTY) return false;
@@ -965,6 +969,7 @@ async function handleAgentsCommand(rest = '', ctx) {
965
969
  force: Boolean(flags.force),
966
970
  });
967
971
  process.stderr.write(` ${c.green('✓')} ${c.dim('Created local agent:')} ${created.filePath}\n`);
972
+ process.stderr.write(` ${c.dim('Available now in this workspace:')} /run ${created.slug} "<task>"\n`);
968
973
  const shouldOpen = !flags['no-open'] && (Boolean(flags.open) || isVsCodeTerminal());
969
974
  if (shouldOpen) {
970
975
  const opened = openAgentFile(created.filePath, {
@@ -976,7 +981,7 @@ async function handleAgentsCommand(rest = '', ctx) {
976
981
  process.stderr.write(` ${c.dim(opened.reason)}\n`);
977
982
  }
978
983
  }
979
- process.stderr.write(` ${c.dim('Sync explicitly with:')} /agents sync ${created.slug}\n`);
984
+ process.stderr.write(` ${c.dim('Optional cloud/account sync:')} /agents sync ${created.slug}\n`);
980
985
  } catch (err) {
981
986
  process.stderr.write(` ${c.red(err.message || String(err))}\n`);
982
987
  }
@@ -1002,7 +1007,7 @@ async function handleAgentsCommand(rest = '', ctx) {
1002
1007
  process.stderr.write(` ${c.yellow('!')} ${c.dim(opened.reason)}\n`);
1003
1008
  process.stderr.write(` ${c.dim('Agent file:')} ${agent.source}\n`);
1004
1009
  }
1005
- process.stderr.write(` ${c.dim('Sync after editing:')} /agents sync ${agent.slug}\n`);
1010
+ process.stderr.write(` ${c.dim('Local changes are available immediately in this workspace. Optional cloud/account sync:')} /agents sync ${agent.slug}\n`);
1006
1011
  return;
1007
1012
  }
1008
1013
 
@@ -1024,7 +1029,7 @@ async function handleAgentsCommand(rest = '', ctx) {
1024
1029
  agents: selected,
1025
1030
  });
1026
1031
  const synced = result.synced ?? selected.length;
1027
- process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to Supabase.`)}\n`);
1032
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to the backend for account/cloud reuse.`)}\n`);
1028
1033
  } catch (err) {
1029
1034
  process.stderr.write(` ${c.red(err.message || String(err))}\n`);
1030
1035
  }
@@ -1044,6 +1049,43 @@ function printSkillsUsage() {
1044
1049
  process.stderr.write(` ${c.dim(' /skills update <name> [--project]')}\n`);
1045
1050
  }
1046
1051
 
1052
+ async function handlePluginsCommand(rest = '', ctx) {
1053
+ const argv = String(rest || '').trim().split(/\s+/).filter(Boolean);
1054
+ const action = (argv[0] || 'list').toLowerCase();
1055
+ const known = new Set(['install', 'validate', 'check', 'lint', 'list', 'ls', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'info', 'update', 'upgrade']);
1056
+
1057
+ // Bare `/plugins <name>` (no action verb) is treated as info, mirroring how
1058
+ // `/skills <name>` behaves. `/plugins` alone lists.
1059
+ if (!known.has(action)) {
1060
+ if (argv.length === 0) return handlePluginsCommand('list', ctx);
1061
+ return handlePluginsCommand(`info ${argv.join(' ')}`, ctx);
1062
+ }
1063
+
1064
+ const args = {
1065
+ action, pluginName: null, source: null,
1066
+ global: true, force: false, ref: null, json: false,
1067
+ };
1068
+ for (let i = 1; i < argv.length; i++) {
1069
+ const a = argv[i];
1070
+ if (a === '--project') args.global = false;
1071
+ else if (a === '--global') args.global = true;
1072
+ else if (a === '--force' || a === '-f') args.force = true;
1073
+ else if (a === '--json') args.json = true;
1074
+ else if (a === '--ref' || a === '--tag' || a === '--branch') args.ref = argv[++i];
1075
+ else if (!a.startsWith('-')) {
1076
+ if (action === 'install' && !args.source) args.source = a;
1077
+ else if (['validate', 'check', 'lint'].includes(action) && !args.source && !args.pluginName) {
1078
+ if (a.includes('/')) args.source = a; else args.pluginName = a;
1079
+ }
1080
+ else if (!args.pluginName) args.pluginName = a;
1081
+ }
1082
+ }
1083
+ try {
1084
+ const { handlePluginManagementCommand } = await import('../commands/plugin-manage.mjs');
1085
+ await handlePluginManagementCommand(args, { cwd: process.cwd(), throwOnError: true });
1086
+ } catch { /* already printed by the handler */ }
1087
+ }
1088
+
1047
1089
  async function handleSkillsCommand(rest = '', ctx) {
1048
1090
  const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
1049
1091
  const hasFlag = (flag) => parts.includes(flag);
@@ -1482,10 +1524,14 @@ function isDeniedStatusMessage(message = '') {
1482
1524
  }
1483
1525
 
1484
1526
  function toolCallId(data = {}, tool = 'tool') {
1485
- return data.call_id || data._callId || data.request_id || data.id ||
1527
+ return data.call_id || data._callId || data.tool_call_id || data.request_id || data.id ||
1486
1528
  `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1487
1529
  }
1488
1530
 
1531
+ function explicitToolCallId(data = {}) {
1532
+ return data.call_id || data._callId || data.tool_call_id || data.request_id || data.id || null;
1533
+ }
1534
+
1489
1535
  function isSubAgentToolEvent(data = {}) {
1490
1536
  return Boolean(data?.internal || data?.sub_agent);
1491
1537
  }
@@ -1495,7 +1541,7 @@ function shouldFoldSubAgentTool(data = {}) {
1495
1541
  }
1496
1542
 
1497
1543
  function foldedSubAgentName(data = {}) {
1498
- return data?.sub_agent || data?.agent || data?.type || 'sub-agent';
1544
+ return data?.sub_agent_label || data?.sub_agent || data?.agent || data?.type || 'sub-agent';
1499
1545
  }
1500
1546
 
1501
1547
  function subAgentStartingLine(agentType = 'sub-agent') {
@@ -1508,19 +1554,131 @@ function subAgentStartingLine(agentType = 'sub-agent') {
1508
1554
  return `→ starting ${normalized}`;
1509
1555
  }
1510
1556
 
1511
- function ensureFoldedSubAgentTools(agentType) {
1557
+ function subAgentRunId(data = {}) {
1558
+ return data?.run_id || data?.sub_agent_run_id || null;
1559
+ }
1560
+
1561
+ function normalizeSubAgentRunData(data = {}) {
1562
+ if (!data || typeof data !== 'object') return data;
1563
+ const runId = subAgentRunId(data);
1564
+ if (!runId) return data;
1565
+ const laneRun = session.activeSubAgentRuns?.get(runId);
1566
+ const patch = {};
1567
+ if (!data.run_id) patch.run_id = runId;
1568
+ if (laneRun?.type && !data.sub_agent) patch.sub_agent = laneRun.type;
1569
+ if (laneRun && !data.sub_agent_label) patch.sub_agent_label = subAgentLaneLabel(laneRun);
1570
+ return Object.keys(patch).length ? { ...data, ...patch } : data;
1571
+ }
1572
+
1573
+ function foldedSubAgentKey(data = {}) {
1574
+ return subAgentRunId(data) || foldedSubAgentName(data);
1575
+ }
1576
+
1577
+ function hasParallelSubAgentRuns() {
1578
+ return activeSubAgentLanes().length > 1;
1579
+ }
1580
+
1581
+ function hasReliableSubAgentAttribution(data = {}) {
1582
+ return !(hasParallelSubAgentRuns() && isSubAgentToolEvent(data) && !subAgentRunId(data));
1583
+ }
1584
+
1585
+ function ensureFoldedSubAgentTools(agentType, key = agentType, data = {}) {
1586
+ runtime.foldedSubAgentToolMap = runtime.foldedSubAgentToolMap || new Map();
1512
1587
  const current = runtime.foldedSubAgentTools;
1513
- if (current && current.agentType === agentType) return current;
1514
- if (current?.entries?.length) flushFoldedSubAgentTools();
1515
- runtime.foldedSubAgentTools = {
1516
- agentType,
1517
- entries: [],
1518
- startedAt: Date.now(),
1588
+ if (current && current.key === key) return current;
1589
+
1590
+ let fold = runtime.foldedSubAgentToolMap.get(key);
1591
+ if (!fold) {
1592
+ const runId = subAgentRunId(data);
1593
+ const laneRun = runId ? session.activeSubAgentRuns?.get(runId) : null;
1594
+ fold = {
1595
+ key,
1596
+ runId,
1597
+ agentType,
1598
+ label: laneRun?.label || agentType,
1599
+ query: laneRun?.query || data?.query || '',
1600
+ entries: [],
1601
+ startedAt: Date.now(),
1602
+ };
1603
+ runtime.foldedSubAgentToolMap.set(key, fold);
1604
+ } else {
1605
+ const runId = subAgentRunId(data);
1606
+ const laneRun = runId ? session.activeSubAgentRuns?.get(runId) : null;
1607
+ if (runId && !fold.runId) fold.runId = runId;
1608
+ if (laneRun) fold.label = subAgentLaneLabel(laneRun);
1609
+ if (laneRun?.query && !fold.query) fold.query = laneRun.query;
1610
+ }
1611
+ runtime.foldedSubAgentTools = fold;
1612
+ return fold;
1613
+ }
1614
+
1615
+ function createSubAgentLane(agentType, query, runId, data = {}) {
1616
+ session.activeSubAgentRuns = session.activeSubAgentRuns || new Map();
1617
+ const sameTypeOrdinals = [...session.activeSubAgentRuns.values()]
1618
+ .filter(run => run.type === agentType)
1619
+ .map(run => Number(run.ordinal || 1));
1620
+ const ordinal = sameTypeOrdinals.length ? Math.max(...sameTypeOrdinals) + 1 : 1;
1621
+ const lane = {
1622
+ type: agentType,
1623
+ ordinal,
1624
+ label: agentType,
1625
+ runId,
1626
+ query,
1627
+ tools: 0,
1628
+ // Backend signals the run starts as one of N siblings — label with
1629
+ // the ordinal from the first event (explore#1) so the open block and
1630
+ // the close line agree even for the first run of a batch.
1631
+ forceOrdinal: Number(data?.parallel_batch) > 1,
1519
1632
  };
1520
- return runtime.foldedSubAgentTools;
1633
+ session.activeSubAgentRuns.set(runId, lane);
1634
+ ensureFoldedSubAgentTools(agentType, runId, { type: agentType, query, run_id: runId });
1635
+ _syncSubAgentWindow();
1636
+ return lane;
1637
+ }
1638
+
1639
+ function activeSubAgentLanes() {
1640
+ return session.activeSubAgentRuns instanceof Map
1641
+ ? [...session.activeSubAgentRuns.values()]
1642
+ : [];
1643
+ }
1644
+
1645
+ function subAgentLaneLabel(lane, lanes = activeSubAgentLanes()) {
1646
+ const sameType = lanes.filter(item => item.type === lane.type).length;
1647
+ if (lane.forceOrdinal || sameType > 1 || Number(lane.ordinal || 1) > 1) {
1648
+ return `${lane.type}#${lane.ordinal || 1}`;
1649
+ }
1650
+ return lane.label || lane.type || 'sub-agent';
1651
+ }
1652
+
1653
+ function removeFoldedSubAgentTools(key) {
1654
+ const map = runtime.foldedSubAgentToolMap;
1655
+ if (key && map?.has(key)) {
1656
+ const fold = map.get(key);
1657
+ map.delete(key);
1658
+ if (runtime.foldedSubAgentTools?.key === key) {
1659
+ runtime.foldedSubAgentTools = map.values().next().value || null;
1660
+ }
1661
+ return fold;
1662
+ }
1663
+ if (!key && map?.size) {
1664
+ const folds = [...map.values()];
1665
+ map.clear();
1666
+ runtime.foldedSubAgentTools = null;
1667
+ return folds;
1668
+ }
1669
+ const fold = runtime.foldedSubAgentTools;
1670
+ runtime.foldedSubAgentTools = null;
1671
+ if (map?.clear) map.clear();
1672
+ return fold;
1521
1673
  }
1522
1674
 
1523
- function findFoldedToolEntry(fold, callId, tool) {
1675
+ function resetFoldedSubAgentTools() {
1676
+ runtime.foldedSubAgentTools = null;
1677
+ if (runtime.foldedSubAgentToolMap?.clear) runtime.foldedSubAgentToolMap.clear();
1678
+ else runtime.foldedSubAgentToolMap = new Map();
1679
+ }
1680
+
1681
+ function findFoldedToolEntry(fold, callId, tool, summary = null) {
1524
1682
  if (!fold) return null;
1525
1683
  if (callId) {
1526
1684
  const exact = fold.entries.find(entry => entry.callId === callId);
@@ -1530,21 +1688,37 @@ function findFoldedToolEntry(fold, callId, tool) {
1530
1688
  const entry = fold.entries[i];
1531
1689
  if (entry.tool === tool && !entry.result) return entry;
1532
1690
  }
1691
+ // The same underlying call arrives on two event streams with different
1692
+ // id namespaces (bridge tool_call call_id vs framework sub_agent_tool
1693
+ // tool_id). When an entry for this exact tool+summary exists — even one
1694
+ // already resolved — treat the second stream's event as the same call
1695
+ // instead of double-counting it (54 "tool uses" for 27 real calls).
1696
+ if (summary) {
1697
+ for (let i = fold.entries.length - 1; i >= 0; i--) {
1698
+ const entry = fold.entries[i];
1699
+ if (entry.tool === tool && entry.summary === summary) return entry;
1700
+ }
1701
+ }
1533
1702
  return null;
1534
1703
  }
1535
1704
 
1536
1705
  function foldSubAgentToolCall(data = {}) {
1706
+ // Under parallel runs, an event without a run_id cannot be attributed
1707
+ // to a lane — drop it from the fold rather than guess (it would render
1708
+ // under whichever agent's group happens to match by name).
1709
+ if (!hasReliableSubAgentAttribution(data)) return;
1537
1710
  const tool = data?.tool || 'unknown';
1538
1711
  const args = data?.args || {};
1539
- const callId = toolCallId(data, tool);
1712
+ const callId = explicitToolCallId(data);
1540
1713
  const agentType = foldedSubAgentName(data);
1541
- const fold = ensureFoldedSubAgentTools(agentType);
1542
- const existing = findFoldedToolEntry(fold, callId, tool);
1714
+ const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
1715
+ const displaySummary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
1716
+ const existing = findFoldedToolEntry(fold, callId, tool, displaySummary);
1543
1717
  const entry = existing || {
1544
1718
  callId,
1545
1719
  tool,
1546
1720
  args,
1547
- summary: toolDisplaySummary(tool, args, { cwd: safeCwd() }),
1721
+ summary: displaySummary,
1548
1722
  startedAt: Date.now(),
1549
1723
  result: null,
1550
1724
  durationMs: null,
@@ -1554,7 +1728,9 @@ function foldSubAgentToolCall(data = {}) {
1554
1728
  if (!existing) fold.entries.push(entry);
1555
1729
  recordCard({ id: callId, tool, args, startedAt: entry.startedAt });
1556
1730
  session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
1557
- updateSpinner(`${agentType} ${tool}`);
1731
+ // Parallel runs own the aggregate spinner text ("2 agents · explore#1 8
1732
+ // · explore#2 10") — don't clobber it with a single run's tool.
1733
+ if (!hasParallelSubAgentRuns()) updateSpinner(`${agentType} → ${tool}`);
1558
1734
  // Live sub-agent window: rebuild from the accumulating fold entries so
1559
1735
  // the same rich '• tool head — outcome' bullets that appear in the
1560
1736
  // final summary render live during the run. Users previously saw only
@@ -1564,13 +1740,37 @@ function foldSubAgentToolCall(data = {}) {
1564
1740
  _syncSubAgentWindow(fold);
1565
1741
  }
1566
1742
 
1743
+ function foldSubAgentToolProgress(data = {}) {
1744
+ const tool = data?.tool || 'unknown';
1745
+ const args = data?.args || {};
1746
+ const callId = explicitToolCallId(data);
1747
+ const agentType = foldedSubAgentName(data);
1748
+ const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
1749
+ const displaySummary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
1750
+ const existing = findFoldedToolEntry(fold, callId, tool, displaySummary);
1751
+ if (!existing) {
1752
+ fold.entries.push({
1753
+ callId,
1754
+ tool,
1755
+ args,
1756
+ summary: displaySummary,
1757
+ startedAt: Date.now(),
1758
+ result: null,
1759
+ durationMs: null,
1760
+ outcome: '',
1761
+ tone: 'dim',
1762
+ });
1763
+ }
1764
+ _syncSubAgentWindow(fold);
1765
+ }
1766
+
1567
1767
  function foldSubAgentToolResult(data = {}) {
1568
1768
  const tool = data?.tool || data?._tool || 'unknown';
1569
1769
  const args = data?.args || {};
1570
- const callId = toolCallId(data, tool);
1770
+ const callId = explicitToolCallId(data);
1571
1771
  const agentType = foldedSubAgentName(data);
1572
- const fold = ensureFoldedSubAgentTools(agentType);
1573
- let entry = findFoldedToolEntry(fold, callId, tool);
1772
+ const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
1773
+ let entry = findFoldedToolEntry(fold, callId, tool, toolDisplaySummary(tool, args, { cwd: safeCwd() }));
1574
1774
  if (!entry) {
1575
1775
  entry = {
1576
1776
  callId,
@@ -1594,7 +1794,7 @@ function foldSubAgentToolResult(data = {}) {
1594
1794
  entry.tone = summary.tone || 'dim';
1595
1795
  if (data._blocked) session.blockedOps++;
1596
1796
  recordCard({ id: callId, tool, args: entry.args, result: data, durationMs, startedAt: entry.startedAt });
1597
- updateSpinner(`${agentType} → ${tool}`);
1797
+ if (!hasParallelSubAgentRuns()) updateSpinner(`${agentType} → ${tool}`);
1598
1798
  // Same live-window sync on result — updates the '• tool' line into
1599
1799
  // '• tool — outcome' as each result lands, without waiting for the
1600
1800
  // whole sub-agent to complete.
@@ -1607,15 +1807,63 @@ function foldSubAgentToolResult(data = {}) {
1607
1807
  * Uses the same foldedToolLine formatter as the completion summary for
1608
1808
  * consistency.
1609
1809
  */
1610
- function _syncSubAgentWindow(fold) {
1611
- if (!fold || !Array.isArray(fold.entries)) return;
1810
+ function _syncSubAgentWindow() {
1612
1811
  const cols = process.stderr.columns || 120;
1613
1812
  // Reserve column budget for the ' ' indent the window applies in
1614
1813
  // presentStatus so we don't wrap.
1615
- const lines = fold.entries.slice(-7).map(entry =>
1616
- foldedToolLine(entry, '', Math.max(40, cols - 4)).trimStart()
1814
+ const maxToolRows = 6;
1815
+ const lanes = activeSubAgentLanes();
1816
+ const visibleLanes = lanes.length ? lanes : (
1817
+ runtime.foldedSubAgentToolMap?.size
1818
+ ? [...runtime.foldedSubAgentToolMap.values()].map(fold => ({
1819
+ type: fold.agentType,
1820
+ label: fold.label,
1821
+ runId: fold.runId || fold.key,
1822
+ ordinal: 1,
1823
+ }))
1824
+ : []
1617
1825
  );
1618
- rebuildSubAgentWindow(lines);
1826
+ if (!visibleLanes.length) return;
1827
+ const multi = visibleLanes.length > 1;
1828
+ const groups = [];
1829
+ for (const lane of visibleLanes) {
1830
+ // With multiple lanes, a lane shows ONLY its own run's fold — never
1831
+ // the label/name-keyed or "current" fallbacks, which would leak
1832
+ // unattributed or sibling entries into the wrong agent's group.
1833
+ const fold = runtime.foldedSubAgentToolMap?.get(lane.runId)
1834
+ || (multi ? null : (runtime.foldedSubAgentToolMap?.get(lane.label) || runtime.foldedSubAgentTools));
1835
+ const entries = Array.isArray(fold?.entries) ? fold.entries : [];
1836
+ const label = subAgentLaneLabel(lane, visibleLanes);
1837
+ if (multi) {
1838
+ // One rotating row per agent under the aggregate spinner: the
1839
+ // latest tool, refreshed as calls arrive. Counts live in the
1840
+ // spinner summary ("2 agents · explore#1 31 · explore#2 14").
1841
+ const last = entries[entries.length - 1];
1842
+ const body = last
1843
+ ? foldedToolLine(last, '', Math.max(40, cols - 16)).trimStart()
1844
+ : subAgentStartingLine(lane.type);
1845
+ groups.push({
1846
+ key: lane.runId || label,
1847
+ runId: lane.runId,
1848
+ label,
1849
+ header: '',
1850
+ lines: [fitAnsiLine(`${paint.brand.data(label)} ${body}`, cols)],
1851
+ });
1852
+ continue;
1853
+ }
1854
+ const lines = entries.slice(-maxToolRows).map(entry =>
1855
+ foldedToolLine(entry, '', Math.max(40, cols - 4)).trimStart()
1856
+ );
1857
+ if (!lines.length) lines.push(subAgentStartingLine(lane.type));
1858
+ groups.push({
1859
+ key: lane.runId || lane.label || lane.type,
1860
+ runId: lane.runId,
1861
+ label,
1862
+ header: '',
1863
+ lines,
1864
+ });
1865
+ }
1866
+ rebuildSubAgentWindowGroups(groups);
1619
1867
  }
1620
1868
 
1621
1869
  function foldedOutcome(entry) {
@@ -1637,11 +1885,8 @@ function foldedToolLine(entry, indent, columns) {
1637
1885
  return fitAnsiLine(line, Math.max(32, columns));
1638
1886
  }
1639
1887
 
1640
- function flushFoldedSubAgentTools() {
1641
- const fold = runtime.foldedSubAgentTools;
1642
- if (!fold) return;
1888
+ function flushOneFoldedSubAgentTools(fold) {
1643
1889
  const entries = Array.isArray(fold.entries) ? fold.entries : [];
1644
- runtime.foldedSubAgentTools = null;
1645
1890
  if (!entries.length) return;
1646
1891
 
1647
1892
  renderBlockBoundary('tool', { compactSame: true });
@@ -1674,6 +1919,16 @@ function flushFoldedSubAgentTools() {
1674
1919
  runtime.lastRenderedBlock = 'tool';
1675
1920
  }
1676
1921
 
1922
+ function flushFoldedSubAgentTools(key = null) {
1923
+ const fold = removeFoldedSubAgentTools(key);
1924
+ if (!fold) return;
1925
+ if (Array.isArray(fold)) {
1926
+ for (const item of fold) flushOneFoldedSubAgentTools(item);
1927
+ return;
1928
+ }
1929
+ flushOneFoldedSubAgentTools(fold);
1930
+ }
1931
+
1677
1932
  function renderEvent(event) {
1678
1933
  const { type, data } = event;
1679
1934
 
@@ -1869,10 +2124,11 @@ function renderEvent(event) {
1869
2124
 
1870
2125
  case 'tool_call':
1871
2126
  case 'tool_request': {
2127
+ const eventData = normalizeSubAgentRunData(data);
1872
2128
  if (watchState.active) {
1873
- watchState.addEntry('tool', { label: data?.tool, detail: data?.args?.file_path || data?.args?.path || data?.args?.pattern || data?.args?.query || '' });
2129
+ watchState.addEntry('tool', { label: eventData?.tool, detail: eventData?.args?.file_path || eventData?.args?.path || eventData?.args?.pattern || eventData?.args?.query || '' });
1874
2130
  }
1875
- const isInternal = Boolean(data?.internal || data?.sub_agent);
2131
+ const isInternal = Boolean(eventData?.internal || eventData?.sub_agent);
1876
2132
  if (isInternal) {
1877
2133
  session.subAgentToolCalls++;
1878
2134
  session.totalSubAgentToolCalls++;
@@ -1881,13 +2137,16 @@ function renderEvent(event) {
1881
2137
  session.totalPrimaryToolCalls++;
1882
2138
  }
1883
2139
  session.totalToolCalls++;
1884
- if (shouldFoldSubAgentTool(data)) {
1885
- foldSubAgentToolCall(data);
2140
+ if (!hasReliableSubAgentAttribution(eventData)) {
2141
+ break;
2142
+ }
2143
+ if (shouldFoldSubAgentTool(eventData)) {
2144
+ foldSubAgentToolCall(eventData);
1886
2145
  break;
1887
2146
  }
1888
2147
  stopSpinner();
1889
2148
  flushContent();
1890
- renderToolCall(data);
2149
+ renderToolCall(eventData);
1891
2150
  break;
1892
2151
  }
1893
2152
 
@@ -1929,16 +2188,20 @@ function renderEvent(event) {
1929
2188
 
1930
2189
  case 'tool_result':
1931
2190
  case 'tool_done': {
2191
+ const eventData = normalizeSubAgentRunData(data);
1932
2192
  if (watchState.active) {
1933
- const success = data?.success !== false;
1934
- watchState.addEntry('done', { label: data?.tool, detail: success ? '✓' : '✗' });
2193
+ const success = eventData?.success !== false;
2194
+ watchState.addEntry('done', { label: eventData?.tool, detail: success ? '✓' : '✗' });
2195
+ }
2196
+ if (!hasReliableSubAgentAttribution(eventData)) {
2197
+ break;
1935
2198
  }
1936
- if (shouldFoldSubAgentTool(data)) {
1937
- foldSubAgentToolResult(data);
2199
+ if (shouldFoldSubAgentTool(eventData)) {
2200
+ foldSubAgentToolResult(eventData);
1938
2201
  break;
1939
2202
  }
1940
2203
  stopSpinner();
1941
- renderToolResult(data, type);
2204
+ renderToolResult(eventData, type);
1942
2205
  break;
1943
2206
  }
1944
2207
 
@@ -2052,32 +2315,69 @@ function renderEvent(event) {
2052
2315
  if (watchState.active) {
2053
2316
  watchState.addEntry('spawn', { type: data?.type, label: (data?.query || '').slice(0, 60) });
2054
2317
  }
2055
- stopSpinner();
2056
- clearPendingHead();
2057
- flushFoldedSubAgentTools();
2058
2318
  const agentType = data?.type || 'sub-agent';
2059
2319
  const query = data?.query || '';
2320
+ // Parallel sub-agents: track each run in its own display lane. Two
2321
+ // concurrent runs of the same type are only distinguishable by the
2322
+ // backend-issued run_id; label lanes explore#1 / explore#2 when
2323
+ // more than one run is active. Solo runs render exactly as before.
2324
+ session.activeSubAgentRuns = session.activeSubAgentRuns || new Map();
2325
+ const hadActiveRuns = session.activeSubAgentRuns.size > 0;
2326
+ if (!hadActiveRuns) {
2327
+ stopSpinner();
2328
+ clearPendingHead();
2329
+ flushFoldedSubAgentTools();
2330
+ }
2331
+ const runId = data?.run_id || `${agentType}:${Date.now().toString(36)}`;
2332
+ const lane = createSubAgentLane(agentType, query, runId, data);
2333
+ const parallel = session.activeSubAgentRuns.size > 1 || lane.forceOrdinal;
2334
+ const label = subAgentLaneLabel(lane);
2060
2335
  renderBlockBoundary('subagent');
2061
- process.stderr.write(renderSubAgentOpen({ type: agentType, query }).replace(/^\n/, '') + '\n');
2336
+ process.stderr.write(renderSubAgentOpen({ id: runId, type: label, query, parentDepth: parallel ? 0 : undefined }).replace(/^\n/, '') + '\n');
2062
2337
  runtime.lastRenderedBlock = 'subagent';
2063
2338
  session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
2064
2339
  session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
2065
2340
  // Fixed-height live tool window under the spinner (queue mode):
2066
2341
  // inner tool calls stream here instead of flooding the transcript.
2067
2342
  setSubAgentWindowActive(true);
2068
- // Phase per sub-agent run: the status line counts elapsed time and
2069
- // tool calls live ("plan agent · 4 calls · 32s") for the whole run.
2070
- startSpinner(`${agentType} agent`, { phase: `sub:${agentType}:${Date.now()}` });
2071
- pushSubAgentWindowLine(subAgentStartingLine(agentType));
2343
+ if (hadActiveRuns) {
2344
+ updateSpinner(`${session.activeSubAgentRuns.size} agents running`);
2345
+ } else {
2346
+ // Phase per sub-agent run: the status line counts elapsed time and
2347
+ // tool calls live ("plan agent · 4 calls · 32s") for the whole run.
2348
+ // First run of a signaled batch starts the spinner with its
2349
+ // ordinal label so the display matches the open block.
2350
+ startSpinner(`${label} agent`, { phase: `sub:${agentType}:${Date.now()}` });
2351
+ }
2352
+ _syncSubAgentWindow();
2072
2353
  break;
2073
2354
  }
2074
2355
 
2075
2356
  case 'sub_agent_tool': {
2076
2357
  // The regular tool_call event renders the card, indented by the
2077
2358
  // sub-agent stack depth. Just update the spinner text here.
2359
+ const eventData = normalizeSubAgentRunData(data);
2078
2360
  const agentType = data?.type || 'sub-agent';
2079
- const tool = data?.tool || '';
2361
+ const tool = eventData?.tool || '';
2080
2362
  if (!tool) break;
2363
+ // Lane attribution for parallel runs: prefix window/spinner lines
2364
+ // with the run's label so interleaved streams stay readable.
2365
+ const eventRunId = subAgentRunId(eventData);
2366
+ if (!eventRunId && hasParallelSubAgentRuns()) {
2367
+ break;
2368
+ }
2369
+ const laneRun = session.activeSubAgentRuns?.get(eventRunId);
2370
+ if (laneRun) laneRun.tools++;
2371
+ foldSubAgentToolProgress(eventData);
2372
+ const laneParallel = (session.activeSubAgentRuns?.size || 0) > 1;
2373
+ if (laneParallel) {
2374
+ bumpSpinnerProgress();
2375
+ const activeLanes = activeSubAgentLanes();
2376
+ const lanes = activeLanes
2377
+ .map(r => `${subAgentLaneLabel(r, activeLanes)} ${r.tools}`).join(' · ');
2378
+ updateSpinner(`${session.activeSubAgentRuns.size} agents · ${lanes}`);
2379
+ break;
2380
+ }
2081
2381
  // Feed the live window from THIS event — it always fires (55/55 in
2082
2382
  // observed runs), unlike the inner tool_call render path which
2083
2383
  // diverts for explore-category tools and folded verbosity modes.
@@ -2092,7 +2392,7 @@ function renderEvent(event) {
2092
2392
  // + window; the fallback line is only for the "otherwise blind"
2093
2393
  // case that surfaced in earlier local terminal reports.
2094
2394
  if (!rqueue.isActive()) {
2095
- const label = data?.label || '';
2395
+ const label = eventData?.label || '';
2096
2396
  const hint = label ? ` · ${label}` : '';
2097
2397
  const key = `${agentType}:${tool}:${label}`;
2098
2398
  if (session._lastSubAgentInlineKey !== key) {
@@ -2111,6 +2411,15 @@ function renderEvent(event) {
2111
2411
  break;
2112
2412
  }
2113
2413
 
2414
+ case 'sub_agent_tool_result': {
2415
+ const eventData = normalizeSubAgentRunData(data);
2416
+ if (!hasReliableSubAgentAttribution(eventData)) {
2417
+ break;
2418
+ }
2419
+ foldSubAgentToolResult(eventData);
2420
+ break;
2421
+ }
2422
+
2114
2423
  case 'sub_agent_complete': {
2115
2424
  if (watchState.active) {
2116
2425
  const agentType = data?.type || 'sub-agent';
@@ -2118,11 +2427,42 @@ function renderEvent(event) {
2118
2427
  const durationS = data?.duration_s || 0;
2119
2428
  watchState.addEntry('done', { type: agentType, detail: `${toolCalls} tools · ${durationS.toFixed(1)}s`, status: 'done' });
2120
2429
  }
2121
- setSubAgentWindowActive(false);
2122
- stopSpinner();
2123
- clearPendingHead();
2124
- flushFoldedSubAgentTools();
2430
+ const eventData = normalizeSubAgentRunData(data);
2125
2431
  const agentType = data?.type || 'sub-agent';
2432
+ // Retire this run's display lane; while sibling runs are still
2433
+ // active keep the shared window/spinner alive for them.
2434
+ const eventRunId = subAgentRunId(eventData);
2435
+ const doneRun = session.activeSubAgentRuns?.get(eventRunId);
2436
+ const doneLabel = doneRun ? subAgentLaneLabel(doneRun) : agentType;
2437
+ const doneFold = eventRunId ? removeFoldedSubAgentTools(eventRunId) : null;
2438
+ if (doneFold) {
2439
+ doneFold.agentType = doneLabel;
2440
+ doneFold.label = doneLabel;
2441
+ }
2442
+ if (eventRunId) session.activeSubAgentRuns?.delete(eventRunId);
2443
+ else session.activeSubAgentRuns?.clear();
2444
+ const siblingsActive = (session.activeSubAgentRuns?.size || 0) > 0;
2445
+ // In verbose mode each sub-agent tool already rendered a full
2446
+ // transcript card live — flushing the fold would list every tool a
2447
+ // second time. The fold batch is the durable record ONLY when tools
2448
+ // were folded (default verbosity).
2449
+ const toolsRenderedLive = showSubAgentTools(getVerbosity());
2450
+ if (siblingsActive) {
2451
+ if (doneFold && !toolsRenderedLive) flushOneFoldedSubAgentTools(doneFold);
2452
+ _syncSubAgentWindow();
2453
+ } else {
2454
+ setSubAgentWindowActive(false);
2455
+ stopSpinner();
2456
+ clearPendingHead();
2457
+ session._lanesPreservedAtTurnEnd = false;
2458
+ if (toolsRenderedLive) {
2459
+ if (!doneFold) removeFoldedSubAgentTools(null);
2460
+ } else if (doneFold) {
2461
+ flushOneFoldedSubAgentTools(doneFold);
2462
+ } else {
2463
+ flushFoldedSubAgentTools();
2464
+ }
2465
+ }
2126
2466
  const usage = data?.usage || {};
2127
2467
  // Output tokens = generation size. Summing input+output across a
2128
2468
  // multi-iteration sub-agent double-counts the context re-shipped each
@@ -2135,7 +2475,8 @@ function renderEvent(event) {
2135
2475
  const summary = data?.result_summary
2136
2476
  || (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
2137
2477
  process.stderr.write(renderSubAgentClose({
2138
- type: agentType,
2478
+ id: eventRunId,
2479
+ type: doneLabel,
2139
2480
  success: data?.success !== false,
2140
2481
  summary,
2141
2482
  costUsd,
@@ -2386,8 +2727,24 @@ function renderEvent(event) {
2386
2727
  if (session.turns === 1 && session.user) telemetry.track('first_answer', {});
2387
2728
  stopSpinner();
2388
2729
  flushContent();
2389
- flushFoldedSubAgentTools();
2390
- resetSubAgents();
2730
+ // Background/parallel runs can outlive the turn. When lanes are
2731
+ // still active at turn end, preserve them (folds, stack, window) so
2732
+ // late events land correctly instead of corrupting fresh state —
2733
+ // but only for ONE turn boundary: if they're still around at the
2734
+ // next complete with no closure, force-clean to avoid stale lanes.
2735
+ const lanesLive = (session.activeSubAgentRuns?.size || 0) > 0;
2736
+ if (lanesLive && !session._lanesPreservedAtTurnEnd) {
2737
+ session._lanesPreservedAtTurnEnd = true;
2738
+ const n = session.activeSubAgentRuns.size;
2739
+ process.stderr.write(` ${c.dim(`${n} agent run${n === 1 ? '' : 's'} still active in background — progress continues below`)}\n`);
2740
+ } else {
2741
+ session._lanesPreservedAtTurnEnd = false;
2742
+ if (showSubAgentTools(getVerbosity())) resetFoldedSubAgentTools();
2743
+ else flushFoldedSubAgentTools();
2744
+ resetSubAgents();
2745
+ session.activeSubAgentRuns?.clear();
2746
+ setSubAgentWindowActive(false);
2747
+ }
2391
2748
  session.inSubAgent = false;
2392
2749
 
2393
2750
  const summary = data?.summary || '';
@@ -2801,57 +3158,160 @@ async function prepareDirectAgentRunContext(ctx, instruction = '') {
2801
3158
  return execContext;
2802
3159
  }
2803
3160
 
3161
+ function stripWrappingQuotes(value = '') {
3162
+ const text = String(value || '').trim();
3163
+ if (text.length >= 2) {
3164
+ const first = text[0];
3165
+ const last = text[text.length - 1];
3166
+ if ((first === '"' || first === "'") && first === last) {
3167
+ return text.slice(1, -1).trim();
3168
+ }
3169
+ }
3170
+ return text;
3171
+ }
3172
+
3173
+ function addRunTarget(map, agent, kind = 'agent') {
3174
+ const slug = String(agent?.slug || agent?.command || agent?.name || '').trim();
3175
+ if (!slug || map.has(`${kind}:${slug}`)) return;
3176
+ map.set(`${kind}:${slug}`, {
3177
+ slug,
3178
+ name: agent?.name || slug,
3179
+ description: agent?.description || '',
3180
+ scope: agent?.source_scope || agent?.source || kind,
3181
+ kind,
3182
+ });
3183
+ }
3184
+
3185
+ function printRunUsage(ctx) {
3186
+ process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
3187
+ process.stderr.write(` ${c.gray('Example: /run docker-analyzer Analyze all running Docker containers')}\n`);
3188
+
3189
+ const targets = new Map();
3190
+ for (const agent of listLocalAgents(safeCwd())) addRunTarget(targets, agent);
3191
+ for (const agent of BUILTIN_AGENTS) addRunTarget(targets, agent);
3192
+ for (const agent of ctx.toolExecutor?.listRunnables?.() || []) addRunTarget(targets, agent);
3193
+ for (const agent of pluginRegistry?.listAgents?.() || []) addRunTarget(targets, agent);
3194
+
3195
+ const agents = [...targets.values()].filter(item => item.kind === 'agent').slice(0, 12);
3196
+ if (agents.length) {
3197
+ process.stderr.write(`\n ${c.dim('Agents')}\n`);
3198
+ for (const agent of agents) {
3199
+ const desc = agent.description ? ` ${c.dim('- ' + agent.description)}` : '';
3200
+ const scope = agent.scope ? ` ${c.dim('[' + agent.scope + ']')}` : '';
3201
+ process.stderr.write(` ${c.brand(agent.slug)}${scope}${desc}\n`);
3202
+ }
3203
+ }
3204
+
3205
+ const workflows = listLocalWorkflows(safeCwd()).slice(0, 8);
3206
+ if (workflows.length) {
3207
+ process.stderr.write(`\n ${c.dim('Workflows')}\n`);
3208
+ for (const workflow of workflows) {
3209
+ const slug = workflow.slug || workflow.name;
3210
+ const desc = workflow.description ? ` ${c.dim('- ' + workflow.description)}` : '';
3211
+ process.stderr.write(` ${c.brand(slug)}${desc}\n`);
3212
+ }
3213
+ }
3214
+ }
3215
+
2804
3216
  async function handleRunCommand(rest = '', ctx) {
2805
3217
  const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
2806
3218
  const target = parts.shift();
2807
- const instruction = parts.join(' ');
3219
+ const instruction = stripWrappingQuotes(parts.join(' '));
2808
3220
 
2809
3221
  if (!target) {
2810
- process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
3222
+ printRunUsage(ctx);
2811
3223
  return;
2812
3224
  }
2813
3225
 
2814
3226
  const localAgent = listLocalAgents(safeCwd()).find(agent => localAgentMatches(agent, target));
2815
3227
  const builtinAgent = findBuiltinAgent(target);
2816
- const runnableAgent = localAgent || builtinAgent;
2817
- if (runnableAgent) {
2818
- try {
2819
- const execContext = await prepareDirectAgentRunContext(ctx, instruction || target);
2820
- return await runAgentDefinition(runnableAgent, instruction, ctx, session, renderEvent, {
2821
- cwd: execContext.cwd,
2822
- execContext,
2823
- });
2824
- } catch (err) {
2825
- process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
3228
+ const registeredAgent = ctx.toolExecutor?.listRunnables?.()
3229
+ ?.find(agent => localAgentMatches(agent, target));
3230
+ const pluginAgent = pluginRegistry?.listAgents?.()
3231
+ ?.find(agent => localAgentMatches(agent, target));
3232
+ const runnableAgent = localAgent || builtinAgent || registeredAgent || pluginAgent;
3233
+
3234
+ const creds = ctx.auth?.loadCredentials?.() || {};
3235
+ const dispatchCtx = {
3236
+ toolExecutor: ctx.toolExecutor,
3237
+ listRunnables: () => ctx.toolExecutor?.listRunnables?.() || [],
3238
+ listLocalWorkflows: () => listLocalWorkflows(safeCwd()),
3239
+ renderEvent,
3240
+ sessionSubstrate: makeSessionSubstrate(ctx),
3241
+ auth: { token: creds.token || null },
3242
+ credentials: {
3243
+ apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
3244
+ openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
3245
+ },
3246
+ cwd: safeCwd(),
3247
+ };
3248
+
3249
+ try {
3250
+ if (!runnableAgent) process.stderr.write(` ${c.dim(`Running workflow '${target}'...`)}\n`);
3251
+ const outcome = await dispatch({
3252
+ type: 'manual',
3253
+ source: 'repl:/run',
3254
+ target: runnableAgent ? { kind: 'agent', slug: target, agent: runnableAgent } : target,
3255
+ params: { instruction },
3256
+ channel: null,
3257
+ }, dispatchCtx);
3258
+
3259
+ if (!outcome.dispatched) {
3260
+ process.stderr.write(` ${c.red('✗')} ${outcome.reason}\n`);
3261
+ process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
2826
3262
  return;
2827
3263
  }
2828
- }
2829
3264
 
2830
- try {
2831
- process.stderr.write(` ${c.dim(`Running workflow '${target}'...`)}\n`);
2832
- const result = await ctx.toolExecutor.execute('workflow_run_multi', {
2833
- name: target,
2834
- instruction,
2835
- });
3265
+ const result = outcome.result || {};
3266
+ if (outcome.channel === 'server') {
3267
+ if (result?.success === false) {
3268
+ process.stderr.write(` ${c.red('✗')} ${result.output || `Workflow '${target}' failed.`}\n`);
3269
+ return;
3270
+ }
3271
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`Workflow '${target}' complete`)}\n`);
3272
+ const details = [];
3273
+ if (result?.run_id) details.push(`run ${result.run_id}`);
3274
+ if (result?.duration_s) details.push(`${result.duration_s}s`);
3275
+ if (result?.total_tokens) details.push(`${formatTokens(result.total_tokens)} tok`);
3276
+ if (result?.total_cost) details.push(formatCostValue(result.total_cost));
3277
+ if (details.length) process.stderr.write(` ${c.dim(details.join(' · '))}\n`);
3278
+
3279
+ // Push server workflow output to conversation history so the main
3280
+ // agent sees the outcome next turn (mirrors local graph run below).
3281
+ const serverOutput = result?.result || result?.output || '';
3282
+ if (serverOutput && result?.success !== false) {
3283
+ session.history.push(
3284
+ { role: 'user', content: `[${target}] ${instruction || 'run'}` },
3285
+ { role: 'assistant', content: String(serverOutput) },
3286
+ );
3287
+ } else if (result?.success === false && result?.output) {
3288
+ session.history.push(
3289
+ { role: 'user', content: `[${target}] ${instruction || 'run'}` },
3290
+ { role: 'assistant', content: `Workflow failed: ${String(result.output)}` },
3291
+ );
3292
+ }
2836
3293
 
2837
- if (result?.success === false) {
2838
- process.stderr.write(` ${c.red('✗')} ${result.output || `Workflow '${target}' failed.`}\n`);
3294
+ const output = serverOutput;
3295
+ if (output) {
3296
+ process.stderr.write('\n');
3297
+ process.stderr.write(renderMarkdown(String(output), { width: process.stderr.columns || 96 }));
3298
+ process.stderr.write('\n');
3299
+ }
2839
3300
  return;
2840
3301
  }
2841
3302
 
2842
- process.stderr.write(` ${c.green('✓')} ${c.dim(`Workflow '${target}' complete`)}\n`);
2843
- const details = [];
2844
- if (result?.run_id) details.push(`run ${result.run_id}`);
2845
- if (result?.duration_s) details.push(`${result.duration_s}s`);
2846
- if (result?.total_tokens) details.push(`${formatTokens(result.total_tokens)} tok`);
2847
- if (result?.total_cost) details.push(formatCostValue(result.total_cost));
2848
- if (details.length) process.stderr.write(` ${c.dim(details.join(' · '))}\n`);
2849
-
2850
- const output = result?.result || result?.output || '';
2851
- if (output) {
2852
- process.stderr.write('\n');
2853
- process.stderr.write(renderMarkdown(String(output), { width: process.stderr.columns || 96 }));
2854
- process.stderr.write('\n');
3303
+ // Local graph run. Session-substrate agent runs already push their own
3304
+ // conversation history; feed workflow results (and logged-out direct
3305
+ // runs) back so the main agent sees the outcome next turn.
3306
+ if (result.status === 'failed') {
3307
+ process.stderr.write(` ${c.red('✗')} ${result.output || `'${target}' failed.`}\n`);
3308
+ }
3309
+ const historyCovered = Boolean(runnableAgent) && Boolean(creds.token);
3310
+ if (result.output && result.status !== 'failed' && !historyCovered) {
3311
+ session.history.push(
3312
+ { role: 'user', content: `[${target}] ${instruction || 'run'}` },
3313
+ { role: 'assistant', content: String(result.output) },
3314
+ );
2855
3315
  }
2856
3316
  } catch (err) {
2857
3317
  process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
@@ -2859,6 +3319,38 @@ async function handleRunCommand(rest = '', ctx) {
2859
3319
  }
2860
3320
  }
2861
3321
 
3322
+ // Bridges the graph engine's session substrate onto runAgentDefinition:
3323
+ // the node's events stream through the same renderer, and the run keeps
3324
+ // its existing conversation-history feedback.
3325
+ function makeSessionSubstrate(ctx) {
3326
+ return (agent, node, instruction) => (async function* () {
3327
+ const queue = [];
3328
+ let notify = null;
3329
+ let finished = false;
3330
+ const push = (event) => {
3331
+ queue.push(event);
3332
+ const wake = notify; notify = null;
3333
+ wake?.();
3334
+ };
3335
+ const execContext = await prepareDirectAgentRunContext(ctx, instruction);
3336
+ const done = runAgentDefinition(agent, instruction, ctx, session, push, {
3337
+ cwd: execContext.cwd,
3338
+ execContext,
3339
+ pluginRegistry,
3340
+ }).catch(err => push({ type: 'error', data: { message: err?.message || String(err) } }))
3341
+ .finally(() => {
3342
+ finished = true;
3343
+ const wake = notify; notify = null;
3344
+ wake?.();
3345
+ });
3346
+ while (!finished || queue.length) {
3347
+ if (!queue.length) await new Promise(resolve => { notify = resolve; });
3348
+ while (queue.length) yield queue.shift();
3349
+ }
3350
+ await done;
3351
+ })();
3352
+ }
3353
+
2862
3354
  async function handleCommand(input, ctx) {
2863
3355
  const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
2864
3356
  if (aliasTarget) {
@@ -3411,7 +3903,8 @@ async function handleCommand(input, ctx) {
3411
3903
  session.agentHistory.length = 0;
3412
3904
  session.toolCalls = 0;
3413
3905
  session.subAgentToolCalls = 0;
3414
- runtime.foldedSubAgentTools = null;
3906
+ session.activeSubAgentRuns = new Map();
3907
+ resetFoldedSubAgentTools();
3415
3908
  clearCards();
3416
3909
  process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
3417
3910
  return;
@@ -3720,6 +4213,11 @@ async function handleCommand(input, ctx) {
3720
4213
  await handleSkillsCommand(rest, ctx);
3721
4214
  return;
3722
4215
 
4216
+ case '/plugin':
4217
+ case '/plugins':
4218
+ await handlePluginsCommand(rest, ctx);
4219
+ return;
4220
+
3723
4221
  case '/explore':
3724
4222
  case '/review':
3725
4223
  case '/architect': {
@@ -3813,6 +4311,7 @@ export async function startTerminalRepl() {
3813
4311
  return createToolExecutor({
3814
4312
  checkpoints,
3815
4313
  hookRunner,
4314
+ pluginRegistry,
3816
4315
  interactionHandler: askUserInteraction,
3817
4316
  onAutoRegisterStart: shouldShowIndexStatus ? (root) => {
3818
4317
  const name = path.basename(root || safeCwd()) || root || 'project';
@@ -3828,6 +4327,7 @@ export async function startTerminalRepl() {
3828
4327
  });
3829
4328
  }
3830
4329
 
4330
+ const pluginRegistry = new PluginRegistry().scan();
3831
4331
  let toolExecutor = null;
3832
4332
  const skipPerms = cliArgs.skipPermissions;
3833
4333
  let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
@@ -3844,6 +4344,26 @@ export async function startTerminalRepl() {
3844
4344
 
3845
4345
  const ctx = { auth, toolExecutor: null, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
3846
4346
 
4347
+ // Wake-on-finish: background jobs with on_complete dispatch their target
4348
+ // agent through the trigger funnel when they exit. The ctx builder runs
4349
+ // lazily at fire time so it sees the live tool executor.
4350
+ registerJobCompletionDispatch(() => {
4351
+ const creds = ctx.auth?.loadCredentials?.() || {};
4352
+ return {
4353
+ toolExecutor: ctx.toolExecutor,
4354
+ listRunnables: () => ctx.toolExecutor?.listRunnables?.() || [],
4355
+ listLocalWorkflows: () => listLocalWorkflows(safeCwd()),
4356
+ renderEvent,
4357
+ sessionSubstrate: makeSessionSubstrate(ctx),
4358
+ auth: { token: creds.token || null },
4359
+ credentials: {
4360
+ apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
4361
+ openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
4362
+ },
4363
+ cwd: safeCwd(),
4364
+ };
4365
+ });
4366
+
3847
4367
  let startupOutputRow = 1;
3848
4368
  let startupOutputCol = 1;
3849
4369
 
@@ -3897,7 +4417,7 @@ export async function startTerminalRepl() {
3897
4417
  flushContent();
3898
4418
  flushPendingHead();
3899
4419
  flushExploreRun();
3900
- runtime.foldedSubAgentTools = null;
4420
+ resetFoldedSubAgentTools();
3901
4421
  clearCards();
3902
4422
 
3903
4423
  const preserved = {
@@ -3964,6 +4484,7 @@ export async function startTerminalRepl() {
3964
4484
  lastTurnDuration: 0,
3965
4485
  toolCounts: {},
3966
4486
  subAgentCounts: {},
4487
+ activeSubAgentRuns: new Map(),
3967
4488
  savedUsd: 0,
3968
4489
  lastTask: '',
3969
4490
  lastReasoning: '',
@@ -4224,8 +4745,8 @@ export async function startTerminalRepl() {
4224
4745
  printBanner(auth);
4225
4746
 
4226
4747
  // Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
4227
- // KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
4228
- if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.skipPermissions) {
4748
+ // BAHULAM_NO_PREFLIGHT=1 (used by tests / scripted runs).
4749
+ if (process.env.BAHULAM_NO_PREFLIGHT !== '1' && !cliArgs.skipPermissions) {
4229
4750
  try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
4230
4751
  catch { /* preflight is best-effort */ }
4231
4752
  }
@@ -4902,6 +5423,7 @@ export async function startTerminalRepl() {
4902
5423
  token: creds.token,
4903
5424
  toolExecutor,
4904
5425
  approvalManager: approval,
5426
+ pluginRegistry,
4905
5427
  });
4906
5428
  }
4907
5429
  const client = streamClient;