@bahulam/code 0.1.11 → 0.1.13

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/auth/tarang-auth.mjs +313 -0
  3. package/src/commands/agent.mjs +7 -7
  4. package/src/commands/install.mjs +295 -0
  5. package/src/commands/plugin-manage.mjs +280 -88
  6. package/src/config/cli-args.mjs +16 -0
  7. package/src/config/settings-loader.mjs +15 -0
  8. package/src/core/background-tasks.mjs +186 -0
  9. package/src/core/headless.mjs +54 -3
  10. package/src/core/local-agent.mjs +10 -1
  11. package/src/core/risk-tier.mjs +1 -0
  12. package/src/core/stream-client.mjs +95 -15
  13. package/src/core/tool-executor.mjs +266 -15
  14. package/src/local-service/agent-relay.mjs +1 -1
  15. package/src/local-service/server.mjs +116 -14
  16. package/src/orchestration/approval.mjs +30 -0
  17. package/src/orchestration/completion-triggers.mjs +40 -0
  18. package/src/orchestration/dispatch.mjs +118 -0
  19. package/src/orchestration/events.mjs +19 -0
  20. package/src/orchestration/graph.mjs +126 -0
  21. package/src/orchestration/node-runner.mjs +193 -0
  22. package/src/orchestration/runner.mjs +200 -0
  23. package/src/plugins/executor.mjs +2 -2
  24. package/src/plugins/manifest.mjs +30 -27
  25. package/src/plugins/pi-compat/loader-hook.mjs +45 -0
  26. package/src/plugins/pi-compat/probe.mjs +294 -0
  27. package/src/plugins/pi-compat/scaffold.mjs +487 -0
  28. package/src/plugins/pi-compat/shim.mjs +134 -0
  29. package/src/plugins/pi-compose.mjs +147 -0
  30. package/src/plugins/preflight.mjs +35 -10
  31. package/src/plugins/registry.mjs +6 -0
  32. package/src/terminal/agents.mjs +8 -3
  33. package/src/terminal/main.mjs +39 -7
  34. package/src/terminal/paste-input.mjs +23 -0
  35. package/src/terminal/repl-render.mjs +65 -10
  36. package/src/terminal/repl-state.mjs +4 -2
  37. package/src/terminal/repl.mjs +624 -103
  38. package/src/tools/agent.mjs +6 -2
  39. package/src/tools/registry.mjs +107 -4
  40. package/src/ui/input-dock.mjs +5 -2
  41. package/src/ui/slash-commands.mjs +1 -1
  42. package/src/ui/sub-agent.mjs +14 -8
@@ -64,10 +64,14 @@ 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';
67
70
  import { PluginRegistry } from '../plugins/registry.mjs';
68
71
  import { SessionManager } from '../core/session-manager.mjs';
69
72
  import { parseArgs } from '../config/cli-args.mjs';
70
73
  import { pickModelOverridesForm } from './repl-model-form.mjs';
74
+ import { isRawMultilinePasteChunk, normalizePastedText, pastedTextLabel } from './paste-input.mjs';
71
75
  import {
72
76
  MODEL_CATEGORY_ORDER,
73
77
  formatCategoryBadge,
@@ -115,7 +119,7 @@ import {
115
119
  flushPendingHead,
116
120
  isInlineOutcomeTool,
117
121
  pushSubAgentWindowLine,
118
- rebuildSubAgentWindow,
122
+ rebuildSubAgentWindowGroups,
119
123
  renderBlockBoundary,
120
124
  renderExploreRun,
121
125
  renderFileDiffEvent,
@@ -966,6 +970,7 @@ async function handleAgentsCommand(rest = '', ctx) {
966
970
  force: Boolean(flags.force),
967
971
  });
968
972
  process.stderr.write(` ${c.green('✓')} ${c.dim('Created local agent:')} ${created.filePath}\n`);
973
+ process.stderr.write(` ${c.dim('Available now in this workspace:')} /run ${created.slug} "<task>"\n`);
969
974
  const shouldOpen = !flags['no-open'] && (Boolean(flags.open) || isVsCodeTerminal());
970
975
  if (shouldOpen) {
971
976
  const opened = openAgentFile(created.filePath, {
@@ -977,7 +982,7 @@ async function handleAgentsCommand(rest = '', ctx) {
977
982
  process.stderr.write(` ${c.dim(opened.reason)}\n`);
978
983
  }
979
984
  }
980
- process.stderr.write(` ${c.dim('Sync explicitly with:')} /agents sync ${created.slug}\n`);
985
+ process.stderr.write(` ${c.dim('Optional cloud/account sync:')} /agents sync ${created.slug}\n`);
981
986
  } catch (err) {
982
987
  process.stderr.write(` ${c.red(err.message || String(err))}\n`);
983
988
  }
@@ -1003,7 +1008,7 @@ async function handleAgentsCommand(rest = '', ctx) {
1003
1008
  process.stderr.write(` ${c.yellow('!')} ${c.dim(opened.reason)}\n`);
1004
1009
  process.stderr.write(` ${c.dim('Agent file:')} ${agent.source}\n`);
1005
1010
  }
1006
- process.stderr.write(` ${c.dim('Sync after editing:')} /agents sync ${agent.slug}\n`);
1011
+ process.stderr.write(` ${c.dim('Local changes are available immediately in this workspace. Optional cloud/account sync:')} /agents sync ${agent.slug}\n`);
1007
1012
  return;
1008
1013
  }
1009
1014
 
@@ -1025,7 +1030,7 @@ async function handleAgentsCommand(rest = '', ctx) {
1025
1030
  agents: selected,
1026
1031
  });
1027
1032
  const synced = result.synced ?? selected.length;
1028
- process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to Supabase.`)}\n`);
1033
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to the backend for account/cloud reuse.`)}\n`);
1029
1034
  } catch (err) {
1030
1035
  process.stderr.write(` ${c.red(err.message || String(err))}\n`);
1031
1036
  }
@@ -1520,10 +1525,14 @@ function isDeniedStatusMessage(message = '') {
1520
1525
  }
1521
1526
 
1522
1527
  function toolCallId(data = {}, tool = 'tool') {
1523
- return data.call_id || data._callId || data.request_id || data.id ||
1528
+ return data.call_id || data._callId || data.tool_call_id || data.request_id || data.id ||
1524
1529
  `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1525
1530
  }
1526
1531
 
1532
+ function explicitToolCallId(data = {}) {
1533
+ return data.call_id || data._callId || data.tool_call_id || data.request_id || data.id || null;
1534
+ }
1535
+
1527
1536
  function isSubAgentToolEvent(data = {}) {
1528
1537
  return Boolean(data?.internal || data?.sub_agent);
1529
1538
  }
@@ -1533,7 +1542,7 @@ function shouldFoldSubAgentTool(data = {}) {
1533
1542
  }
1534
1543
 
1535
1544
  function foldedSubAgentName(data = {}) {
1536
- return data?.sub_agent || data?.agent || data?.type || 'sub-agent';
1545
+ return data?.sub_agent_label || data?.sub_agent || data?.agent || data?.type || 'sub-agent';
1537
1546
  }
1538
1547
 
1539
1548
  function subAgentStartingLine(agentType = 'sub-agent') {
@@ -1546,19 +1555,131 @@ function subAgentStartingLine(agentType = 'sub-agent') {
1546
1555
  return `→ starting ${normalized}`;
1547
1556
  }
1548
1557
 
1549
- function ensureFoldedSubAgentTools(agentType) {
1558
+ function subAgentRunId(data = {}) {
1559
+ return data?.run_id || data?.sub_agent_run_id || null;
1560
+ }
1561
+
1562
+ function normalizeSubAgentRunData(data = {}) {
1563
+ if (!data || typeof data !== 'object') return data;
1564
+ const runId = subAgentRunId(data);
1565
+ if (!runId) return data;
1566
+ const laneRun = session.activeSubAgentRuns?.get(runId);
1567
+ const patch = {};
1568
+ if (!data.run_id) patch.run_id = runId;
1569
+ if (laneRun?.type && !data.sub_agent) patch.sub_agent = laneRun.type;
1570
+ if (laneRun && !data.sub_agent_label) patch.sub_agent_label = subAgentLaneLabel(laneRun);
1571
+ return Object.keys(patch).length ? { ...data, ...patch } : data;
1572
+ }
1573
+
1574
+ function foldedSubAgentKey(data = {}) {
1575
+ return subAgentRunId(data) || foldedSubAgentName(data);
1576
+ }
1577
+
1578
+ function hasParallelSubAgentRuns() {
1579
+ return activeSubAgentLanes().length > 1;
1580
+ }
1581
+
1582
+ function hasReliableSubAgentAttribution(data = {}) {
1583
+ return !(hasParallelSubAgentRuns() && isSubAgentToolEvent(data) && !subAgentRunId(data));
1584
+ }
1585
+
1586
+ function ensureFoldedSubAgentTools(agentType, key = agentType, data = {}) {
1587
+ runtime.foldedSubAgentToolMap = runtime.foldedSubAgentToolMap || new Map();
1550
1588
  const current = runtime.foldedSubAgentTools;
1551
- if (current && current.agentType === agentType) return current;
1552
- if (current?.entries?.length) flushFoldedSubAgentTools();
1553
- runtime.foldedSubAgentTools = {
1554
- agentType,
1555
- entries: [],
1556
- startedAt: Date.now(),
1589
+ if (current && current.key === key) return current;
1590
+
1591
+ let fold = runtime.foldedSubAgentToolMap.get(key);
1592
+ if (!fold) {
1593
+ const runId = subAgentRunId(data);
1594
+ const laneRun = runId ? session.activeSubAgentRuns?.get(runId) : null;
1595
+ fold = {
1596
+ key,
1597
+ runId,
1598
+ agentType,
1599
+ label: laneRun?.label || agentType,
1600
+ query: laneRun?.query || data?.query || '',
1601
+ entries: [],
1602
+ startedAt: Date.now(),
1603
+ };
1604
+ runtime.foldedSubAgentToolMap.set(key, fold);
1605
+ } else {
1606
+ const runId = subAgentRunId(data);
1607
+ const laneRun = runId ? session.activeSubAgentRuns?.get(runId) : null;
1608
+ if (runId && !fold.runId) fold.runId = runId;
1609
+ if (laneRun) fold.label = subAgentLaneLabel(laneRun);
1610
+ if (laneRun?.query && !fold.query) fold.query = laneRun.query;
1611
+ }
1612
+ runtime.foldedSubAgentTools = fold;
1613
+ return fold;
1614
+ }
1615
+
1616
+ function createSubAgentLane(agentType, query, runId, data = {}) {
1617
+ session.activeSubAgentRuns = session.activeSubAgentRuns || new Map();
1618
+ const sameTypeOrdinals = [...session.activeSubAgentRuns.values()]
1619
+ .filter(run => run.type === agentType)
1620
+ .map(run => Number(run.ordinal || 1));
1621
+ const ordinal = sameTypeOrdinals.length ? Math.max(...sameTypeOrdinals) + 1 : 1;
1622
+ const lane = {
1623
+ type: agentType,
1624
+ ordinal,
1625
+ label: agentType,
1626
+ runId,
1627
+ query,
1628
+ tools: 0,
1629
+ // Backend signals the run starts as one of N siblings — label with
1630
+ // the ordinal from the first event (explore#1) so the open block and
1631
+ // the close line agree even for the first run of a batch.
1632
+ forceOrdinal: Number(data?.parallel_batch) > 1,
1557
1633
  };
1558
- return runtime.foldedSubAgentTools;
1634
+ session.activeSubAgentRuns.set(runId, lane);
1635
+ ensureFoldedSubAgentTools(agentType, runId, { type: agentType, query, run_id: runId });
1636
+ _syncSubAgentWindow();
1637
+ return lane;
1638
+ }
1639
+
1640
+ function activeSubAgentLanes() {
1641
+ return session.activeSubAgentRuns instanceof Map
1642
+ ? [...session.activeSubAgentRuns.values()]
1643
+ : [];
1559
1644
  }
1560
1645
 
1561
- function findFoldedToolEntry(fold, callId, tool) {
1646
+ function subAgentLaneLabel(lane, lanes = activeSubAgentLanes()) {
1647
+ const sameType = lanes.filter(item => item.type === lane.type).length;
1648
+ if (lane.forceOrdinal || sameType > 1 || Number(lane.ordinal || 1) > 1) {
1649
+ return `${lane.type}#${lane.ordinal || 1}`;
1650
+ }
1651
+ return lane.label || lane.type || 'sub-agent';
1652
+ }
1653
+
1654
+ function removeFoldedSubAgentTools(key) {
1655
+ const map = runtime.foldedSubAgentToolMap;
1656
+ if (key && map?.has(key)) {
1657
+ const fold = map.get(key);
1658
+ map.delete(key);
1659
+ if (runtime.foldedSubAgentTools?.key === key) {
1660
+ runtime.foldedSubAgentTools = map.values().next().value || null;
1661
+ }
1662
+ return fold;
1663
+ }
1664
+ if (!key && map?.size) {
1665
+ const folds = [...map.values()];
1666
+ map.clear();
1667
+ runtime.foldedSubAgentTools = null;
1668
+ return folds;
1669
+ }
1670
+ const fold = runtime.foldedSubAgentTools;
1671
+ runtime.foldedSubAgentTools = null;
1672
+ if (map?.clear) map.clear();
1673
+ return fold;
1674
+ }
1675
+
1676
+ function resetFoldedSubAgentTools() {
1677
+ runtime.foldedSubAgentTools = null;
1678
+ if (runtime.foldedSubAgentToolMap?.clear) runtime.foldedSubAgentToolMap.clear();
1679
+ else runtime.foldedSubAgentToolMap = new Map();
1680
+ }
1681
+
1682
+ function findFoldedToolEntry(fold, callId, tool, summary = null) {
1562
1683
  if (!fold) return null;
1563
1684
  if (callId) {
1564
1685
  const exact = fold.entries.find(entry => entry.callId === callId);
@@ -1568,21 +1689,37 @@ function findFoldedToolEntry(fold, callId, tool) {
1568
1689
  const entry = fold.entries[i];
1569
1690
  if (entry.tool === tool && !entry.result) return entry;
1570
1691
  }
1692
+ // The same underlying call arrives on two event streams with different
1693
+ // id namespaces (bridge tool_call call_id vs framework sub_agent_tool
1694
+ // tool_id). When an entry for this exact tool+summary exists — even one
1695
+ // already resolved — treat the second stream's event as the same call
1696
+ // instead of double-counting it (54 "tool uses" for 27 real calls).
1697
+ if (summary) {
1698
+ for (let i = fold.entries.length - 1; i >= 0; i--) {
1699
+ const entry = fold.entries[i];
1700
+ if (entry.tool === tool && entry.summary === summary) return entry;
1701
+ }
1702
+ }
1571
1703
  return null;
1572
1704
  }
1573
1705
 
1574
1706
  function foldSubAgentToolCall(data = {}) {
1707
+ // Under parallel runs, an event without a run_id cannot be attributed
1708
+ // to a lane — drop it from the fold rather than guess (it would render
1709
+ // under whichever agent's group happens to match by name).
1710
+ if (!hasReliableSubAgentAttribution(data)) return;
1575
1711
  const tool = data?.tool || 'unknown';
1576
1712
  const args = data?.args || {};
1577
- const callId = toolCallId(data, tool);
1713
+ const callId = explicitToolCallId(data);
1578
1714
  const agentType = foldedSubAgentName(data);
1579
- const fold = ensureFoldedSubAgentTools(agentType);
1580
- const existing = findFoldedToolEntry(fold, callId, tool);
1715
+ const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
1716
+ const displaySummary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
1717
+ const existing = findFoldedToolEntry(fold, callId, tool, displaySummary);
1581
1718
  const entry = existing || {
1582
1719
  callId,
1583
1720
  tool,
1584
1721
  args,
1585
- summary: toolDisplaySummary(tool, args, { cwd: safeCwd() }),
1722
+ summary: displaySummary,
1586
1723
  startedAt: Date.now(),
1587
1724
  result: null,
1588
1725
  durationMs: null,
@@ -1592,7 +1729,9 @@ function foldSubAgentToolCall(data = {}) {
1592
1729
  if (!existing) fold.entries.push(entry);
1593
1730
  recordCard({ id: callId, tool, args, startedAt: entry.startedAt });
1594
1731
  session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
1595
- updateSpinner(`${agentType} ${tool}`);
1732
+ // Parallel runs own the aggregate spinner text ("2 agents · explore#1 8
1733
+ // · explore#2 10") — don't clobber it with a single run's tool.
1734
+ if (!hasParallelSubAgentRuns()) updateSpinner(`${agentType} → ${tool}`);
1596
1735
  // Live sub-agent window: rebuild from the accumulating fold entries so
1597
1736
  // the same rich '• tool head — outcome' bullets that appear in the
1598
1737
  // final summary render live during the run. Users previously saw only
@@ -1602,13 +1741,37 @@ function foldSubAgentToolCall(data = {}) {
1602
1741
  _syncSubAgentWindow(fold);
1603
1742
  }
1604
1743
 
1744
+ function foldSubAgentToolProgress(data = {}) {
1745
+ const tool = data?.tool || 'unknown';
1746
+ const args = data?.args || {};
1747
+ const callId = explicitToolCallId(data);
1748
+ const agentType = foldedSubAgentName(data);
1749
+ const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
1750
+ const displaySummary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
1751
+ const existing = findFoldedToolEntry(fold, callId, tool, displaySummary);
1752
+ if (!existing) {
1753
+ fold.entries.push({
1754
+ callId,
1755
+ tool,
1756
+ args,
1757
+ summary: displaySummary,
1758
+ startedAt: Date.now(),
1759
+ result: null,
1760
+ durationMs: null,
1761
+ outcome: '',
1762
+ tone: 'dim',
1763
+ });
1764
+ }
1765
+ _syncSubAgentWindow(fold);
1766
+ }
1767
+
1605
1768
  function foldSubAgentToolResult(data = {}) {
1606
1769
  const tool = data?.tool || data?._tool || 'unknown';
1607
1770
  const args = data?.args || {};
1608
- const callId = toolCallId(data, tool);
1771
+ const callId = explicitToolCallId(data);
1609
1772
  const agentType = foldedSubAgentName(data);
1610
- const fold = ensureFoldedSubAgentTools(agentType);
1611
- let entry = findFoldedToolEntry(fold, callId, tool);
1773
+ const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
1774
+ let entry = findFoldedToolEntry(fold, callId, tool, toolDisplaySummary(tool, args, { cwd: safeCwd() }));
1612
1775
  if (!entry) {
1613
1776
  entry = {
1614
1777
  callId,
@@ -1632,7 +1795,7 @@ function foldSubAgentToolResult(data = {}) {
1632
1795
  entry.tone = summary.tone || 'dim';
1633
1796
  if (data._blocked) session.blockedOps++;
1634
1797
  recordCard({ id: callId, tool, args: entry.args, result: data, durationMs, startedAt: entry.startedAt });
1635
- updateSpinner(`${agentType} → ${tool}`);
1798
+ if (!hasParallelSubAgentRuns()) updateSpinner(`${agentType} → ${tool}`);
1636
1799
  // Same live-window sync on result — updates the '• tool' line into
1637
1800
  // '• tool — outcome' as each result lands, without waiting for the
1638
1801
  // whole sub-agent to complete.
@@ -1645,15 +1808,63 @@ function foldSubAgentToolResult(data = {}) {
1645
1808
  * Uses the same foldedToolLine formatter as the completion summary for
1646
1809
  * consistency.
1647
1810
  */
1648
- function _syncSubAgentWindow(fold) {
1649
- if (!fold || !Array.isArray(fold.entries)) return;
1811
+ function _syncSubAgentWindow() {
1650
1812
  const cols = process.stderr.columns || 120;
1651
1813
  // Reserve column budget for the ' ' indent the window applies in
1652
1814
  // presentStatus so we don't wrap.
1653
- const lines = fold.entries.slice(-7).map(entry =>
1654
- foldedToolLine(entry, '', Math.max(40, cols - 4)).trimStart()
1815
+ const maxToolRows = 6;
1816
+ const lanes = activeSubAgentLanes();
1817
+ const visibleLanes = lanes.length ? lanes : (
1818
+ runtime.foldedSubAgentToolMap?.size
1819
+ ? [...runtime.foldedSubAgentToolMap.values()].map(fold => ({
1820
+ type: fold.agentType,
1821
+ label: fold.label,
1822
+ runId: fold.runId || fold.key,
1823
+ ordinal: 1,
1824
+ }))
1825
+ : []
1655
1826
  );
1656
- rebuildSubAgentWindow(lines);
1827
+ if (!visibleLanes.length) return;
1828
+ const multi = visibleLanes.length > 1;
1829
+ const groups = [];
1830
+ for (const lane of visibleLanes) {
1831
+ // With multiple lanes, a lane shows ONLY its own run's fold — never
1832
+ // the label/name-keyed or "current" fallbacks, which would leak
1833
+ // unattributed or sibling entries into the wrong agent's group.
1834
+ const fold = runtime.foldedSubAgentToolMap?.get(lane.runId)
1835
+ || (multi ? null : (runtime.foldedSubAgentToolMap?.get(lane.label) || runtime.foldedSubAgentTools));
1836
+ const entries = Array.isArray(fold?.entries) ? fold.entries : [];
1837
+ const label = subAgentLaneLabel(lane, visibleLanes);
1838
+ if (multi) {
1839
+ // One rotating row per agent under the aggregate spinner: the
1840
+ // latest tool, refreshed as calls arrive. Counts live in the
1841
+ // spinner summary ("2 agents · explore#1 31 · explore#2 14").
1842
+ const last = entries[entries.length - 1];
1843
+ const body = last
1844
+ ? foldedToolLine(last, '', Math.max(40, cols - 16)).trimStart()
1845
+ : subAgentStartingLine(lane.type);
1846
+ groups.push({
1847
+ key: lane.runId || label,
1848
+ runId: lane.runId,
1849
+ label,
1850
+ header: '',
1851
+ lines: [fitAnsiLine(`${paint.brand.data(label)} ${body}`, cols)],
1852
+ });
1853
+ continue;
1854
+ }
1855
+ const lines = entries.slice(-maxToolRows).map(entry =>
1856
+ foldedToolLine(entry, '', Math.max(40, cols - 4)).trimStart()
1857
+ );
1858
+ if (!lines.length) lines.push(subAgentStartingLine(lane.type));
1859
+ groups.push({
1860
+ key: lane.runId || lane.label || lane.type,
1861
+ runId: lane.runId,
1862
+ label,
1863
+ header: '',
1864
+ lines,
1865
+ });
1866
+ }
1867
+ rebuildSubAgentWindowGroups(groups);
1657
1868
  }
1658
1869
 
1659
1870
  function foldedOutcome(entry) {
@@ -1675,11 +1886,8 @@ function foldedToolLine(entry, indent, columns) {
1675
1886
  return fitAnsiLine(line, Math.max(32, columns));
1676
1887
  }
1677
1888
 
1678
- function flushFoldedSubAgentTools() {
1679
- const fold = runtime.foldedSubAgentTools;
1680
- if (!fold) return;
1889
+ function flushOneFoldedSubAgentTools(fold) {
1681
1890
  const entries = Array.isArray(fold.entries) ? fold.entries : [];
1682
- runtime.foldedSubAgentTools = null;
1683
1891
  if (!entries.length) return;
1684
1892
 
1685
1893
  renderBlockBoundary('tool', { compactSame: true });
@@ -1712,6 +1920,16 @@ function flushFoldedSubAgentTools() {
1712
1920
  runtime.lastRenderedBlock = 'tool';
1713
1921
  }
1714
1922
 
1923
+ function flushFoldedSubAgentTools(key = null) {
1924
+ const fold = removeFoldedSubAgentTools(key);
1925
+ if (!fold) return;
1926
+ if (Array.isArray(fold)) {
1927
+ for (const item of fold) flushOneFoldedSubAgentTools(item);
1928
+ return;
1929
+ }
1930
+ flushOneFoldedSubAgentTools(fold);
1931
+ }
1932
+
1715
1933
  function renderEvent(event) {
1716
1934
  const { type, data } = event;
1717
1935
 
@@ -1907,10 +2125,11 @@ function renderEvent(event) {
1907
2125
 
1908
2126
  case 'tool_call':
1909
2127
  case 'tool_request': {
2128
+ const eventData = normalizeSubAgentRunData(data);
1910
2129
  if (watchState.active) {
1911
- watchState.addEntry('tool', { label: data?.tool, detail: data?.args?.file_path || data?.args?.path || data?.args?.pattern || data?.args?.query || '' });
2130
+ watchState.addEntry('tool', { label: eventData?.tool, detail: eventData?.args?.file_path || eventData?.args?.path || eventData?.args?.pattern || eventData?.args?.query || '' });
1912
2131
  }
1913
- const isInternal = Boolean(data?.internal || data?.sub_agent);
2132
+ const isInternal = Boolean(eventData?.internal || eventData?.sub_agent);
1914
2133
  if (isInternal) {
1915
2134
  session.subAgentToolCalls++;
1916
2135
  session.totalSubAgentToolCalls++;
@@ -1919,13 +2138,16 @@ function renderEvent(event) {
1919
2138
  session.totalPrimaryToolCalls++;
1920
2139
  }
1921
2140
  session.totalToolCalls++;
1922
- if (shouldFoldSubAgentTool(data)) {
1923
- foldSubAgentToolCall(data);
2141
+ if (!hasReliableSubAgentAttribution(eventData)) {
2142
+ break;
2143
+ }
2144
+ if (shouldFoldSubAgentTool(eventData)) {
2145
+ foldSubAgentToolCall(eventData);
1924
2146
  break;
1925
2147
  }
1926
2148
  stopSpinner();
1927
2149
  flushContent();
1928
- renderToolCall(data);
2150
+ renderToolCall(eventData);
1929
2151
  break;
1930
2152
  }
1931
2153
 
@@ -1967,16 +2189,20 @@ function renderEvent(event) {
1967
2189
 
1968
2190
  case 'tool_result':
1969
2191
  case 'tool_done': {
2192
+ const eventData = normalizeSubAgentRunData(data);
1970
2193
  if (watchState.active) {
1971
- const success = data?.success !== false;
1972
- watchState.addEntry('done', { label: data?.tool, detail: success ? '✓' : '✗' });
2194
+ const success = eventData?.success !== false;
2195
+ watchState.addEntry('done', { label: eventData?.tool, detail: success ? '✓' : '✗' });
1973
2196
  }
1974
- if (shouldFoldSubAgentTool(data)) {
1975
- foldSubAgentToolResult(data);
2197
+ if (!hasReliableSubAgentAttribution(eventData)) {
2198
+ break;
2199
+ }
2200
+ if (shouldFoldSubAgentTool(eventData)) {
2201
+ foldSubAgentToolResult(eventData);
1976
2202
  break;
1977
2203
  }
1978
2204
  stopSpinner();
1979
- renderToolResult(data, type);
2205
+ renderToolResult(eventData, type);
1980
2206
  break;
1981
2207
  }
1982
2208
 
@@ -2090,32 +2316,69 @@ function renderEvent(event) {
2090
2316
  if (watchState.active) {
2091
2317
  watchState.addEntry('spawn', { type: data?.type, label: (data?.query || '').slice(0, 60) });
2092
2318
  }
2093
- stopSpinner();
2094
- clearPendingHead();
2095
- flushFoldedSubAgentTools();
2096
2319
  const agentType = data?.type || 'sub-agent';
2097
2320
  const query = data?.query || '';
2321
+ // Parallel sub-agents: track each run in its own display lane. Two
2322
+ // concurrent runs of the same type are only distinguishable by the
2323
+ // backend-issued run_id; label lanes explore#1 / explore#2 when
2324
+ // more than one run is active. Solo runs render exactly as before.
2325
+ session.activeSubAgentRuns = session.activeSubAgentRuns || new Map();
2326
+ const hadActiveRuns = session.activeSubAgentRuns.size > 0;
2327
+ if (!hadActiveRuns) {
2328
+ stopSpinner();
2329
+ clearPendingHead();
2330
+ flushFoldedSubAgentTools();
2331
+ }
2332
+ const runId = data?.run_id || `${agentType}:${Date.now().toString(36)}`;
2333
+ const lane = createSubAgentLane(agentType, query, runId, data);
2334
+ const parallel = session.activeSubAgentRuns.size > 1 || lane.forceOrdinal;
2335
+ const label = subAgentLaneLabel(lane);
2098
2336
  renderBlockBoundary('subagent');
2099
- process.stderr.write(renderSubAgentOpen({ type: agentType, query }).replace(/^\n/, '') + '\n');
2337
+ process.stderr.write(renderSubAgentOpen({ id: runId, type: label, query, parentDepth: parallel ? 0 : undefined }).replace(/^\n/, '') + '\n');
2100
2338
  runtime.lastRenderedBlock = 'subagent';
2101
2339
  session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
2102
2340
  session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
2103
2341
  // Fixed-height live tool window under the spinner (queue mode):
2104
2342
  // inner tool calls stream here instead of flooding the transcript.
2105
2343
  setSubAgentWindowActive(true);
2106
- // Phase per sub-agent run: the status line counts elapsed time and
2107
- // tool calls live ("plan agent · 4 calls · 32s") for the whole run.
2108
- startSpinner(`${agentType} agent`, { phase: `sub:${agentType}:${Date.now()}` });
2109
- pushSubAgentWindowLine(subAgentStartingLine(agentType));
2344
+ if (hadActiveRuns) {
2345
+ updateSpinner(`${session.activeSubAgentRuns.size} agents running`);
2346
+ } else {
2347
+ // Phase per sub-agent run: the status line counts elapsed time and
2348
+ // tool calls live ("plan agent · 4 calls · 32s") for the whole run.
2349
+ // First run of a signaled batch starts the spinner with its
2350
+ // ordinal label so the display matches the open block.
2351
+ startSpinner(`${label} agent`, { phase: `sub:${agentType}:${Date.now()}` });
2352
+ }
2353
+ _syncSubAgentWindow();
2110
2354
  break;
2111
2355
  }
2112
2356
 
2113
2357
  case 'sub_agent_tool': {
2114
2358
  // The regular tool_call event renders the card, indented by the
2115
2359
  // sub-agent stack depth. Just update the spinner text here.
2360
+ const eventData = normalizeSubAgentRunData(data);
2116
2361
  const agentType = data?.type || 'sub-agent';
2117
- const tool = data?.tool || '';
2362
+ const tool = eventData?.tool || '';
2118
2363
  if (!tool) break;
2364
+ // Lane attribution for parallel runs: prefix window/spinner lines
2365
+ // with the run's label so interleaved streams stay readable.
2366
+ const eventRunId = subAgentRunId(eventData);
2367
+ if (!eventRunId && hasParallelSubAgentRuns()) {
2368
+ break;
2369
+ }
2370
+ const laneRun = session.activeSubAgentRuns?.get(eventRunId);
2371
+ if (laneRun) laneRun.tools++;
2372
+ foldSubAgentToolProgress(eventData);
2373
+ const laneParallel = (session.activeSubAgentRuns?.size || 0) > 1;
2374
+ if (laneParallel) {
2375
+ bumpSpinnerProgress();
2376
+ const activeLanes = activeSubAgentLanes();
2377
+ const lanes = activeLanes
2378
+ .map(r => `${subAgentLaneLabel(r, activeLanes)} ${r.tools}`).join(' · ');
2379
+ updateSpinner(`${session.activeSubAgentRuns.size} agents · ${lanes}`);
2380
+ break;
2381
+ }
2119
2382
  // Feed the live window from THIS event — it always fires (55/55 in
2120
2383
  // observed runs), unlike the inner tool_call render path which
2121
2384
  // diverts for explore-category tools and folded verbosity modes.
@@ -2130,7 +2393,7 @@ function renderEvent(event) {
2130
2393
  // + window; the fallback line is only for the "otherwise blind"
2131
2394
  // case that surfaced in earlier local terminal reports.
2132
2395
  if (!rqueue.isActive()) {
2133
- const label = data?.label || '';
2396
+ const label = eventData?.label || '';
2134
2397
  const hint = label ? ` · ${label}` : '';
2135
2398
  const key = `${agentType}:${tool}:${label}`;
2136
2399
  if (session._lastSubAgentInlineKey !== key) {
@@ -2149,6 +2412,15 @@ function renderEvent(event) {
2149
2412
  break;
2150
2413
  }
2151
2414
 
2415
+ case 'sub_agent_tool_result': {
2416
+ const eventData = normalizeSubAgentRunData(data);
2417
+ if (!hasReliableSubAgentAttribution(eventData)) {
2418
+ break;
2419
+ }
2420
+ foldSubAgentToolResult(eventData);
2421
+ break;
2422
+ }
2423
+
2152
2424
  case 'sub_agent_complete': {
2153
2425
  if (watchState.active) {
2154
2426
  const agentType = data?.type || 'sub-agent';
@@ -2156,11 +2428,42 @@ function renderEvent(event) {
2156
2428
  const durationS = data?.duration_s || 0;
2157
2429
  watchState.addEntry('done', { type: agentType, detail: `${toolCalls} tools · ${durationS.toFixed(1)}s`, status: 'done' });
2158
2430
  }
2159
- setSubAgentWindowActive(false);
2160
- stopSpinner();
2161
- clearPendingHead();
2162
- flushFoldedSubAgentTools();
2431
+ const eventData = normalizeSubAgentRunData(data);
2163
2432
  const agentType = data?.type || 'sub-agent';
2433
+ // Retire this run's display lane; while sibling runs are still
2434
+ // active keep the shared window/spinner alive for them.
2435
+ const eventRunId = subAgentRunId(eventData);
2436
+ const doneRun = session.activeSubAgentRuns?.get(eventRunId);
2437
+ const doneLabel = doneRun ? subAgentLaneLabel(doneRun) : agentType;
2438
+ const doneFold = eventRunId ? removeFoldedSubAgentTools(eventRunId) : null;
2439
+ if (doneFold) {
2440
+ doneFold.agentType = doneLabel;
2441
+ doneFold.label = doneLabel;
2442
+ }
2443
+ if (eventRunId) session.activeSubAgentRuns?.delete(eventRunId);
2444
+ else session.activeSubAgentRuns?.clear();
2445
+ const siblingsActive = (session.activeSubAgentRuns?.size || 0) > 0;
2446
+ // In verbose mode each sub-agent tool already rendered a full
2447
+ // transcript card live — flushing the fold would list every tool a
2448
+ // second time. The fold batch is the durable record ONLY when tools
2449
+ // were folded (default verbosity).
2450
+ const toolsRenderedLive = showSubAgentTools(getVerbosity());
2451
+ if (siblingsActive) {
2452
+ if (doneFold && !toolsRenderedLive) flushOneFoldedSubAgentTools(doneFold);
2453
+ _syncSubAgentWindow();
2454
+ } else {
2455
+ setSubAgentWindowActive(false);
2456
+ stopSpinner();
2457
+ clearPendingHead();
2458
+ session._lanesPreservedAtTurnEnd = false;
2459
+ if (toolsRenderedLive) {
2460
+ if (!doneFold) removeFoldedSubAgentTools(null);
2461
+ } else if (doneFold) {
2462
+ flushOneFoldedSubAgentTools(doneFold);
2463
+ } else {
2464
+ flushFoldedSubAgentTools();
2465
+ }
2466
+ }
2164
2467
  const usage = data?.usage || {};
2165
2468
  // Output tokens = generation size. Summing input+output across a
2166
2469
  // multi-iteration sub-agent double-counts the context re-shipped each
@@ -2173,7 +2476,8 @@ function renderEvent(event) {
2173
2476
  const summary = data?.result_summary
2174
2477
  || (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
2175
2478
  process.stderr.write(renderSubAgentClose({
2176
- type: agentType,
2479
+ id: eventRunId,
2480
+ type: doneLabel,
2177
2481
  success: data?.success !== false,
2178
2482
  summary,
2179
2483
  costUsd,
@@ -2424,8 +2728,24 @@ function renderEvent(event) {
2424
2728
  if (session.turns === 1 && session.user) telemetry.track('first_answer', {});
2425
2729
  stopSpinner();
2426
2730
  flushContent();
2427
- flushFoldedSubAgentTools();
2428
- resetSubAgents();
2731
+ // Background/parallel runs can outlive the turn. When lanes are
2732
+ // still active at turn end, preserve them (folds, stack, window) so
2733
+ // late events land correctly instead of corrupting fresh state —
2734
+ // but only for ONE turn boundary: if they're still around at the
2735
+ // next complete with no closure, force-clean to avoid stale lanes.
2736
+ const lanesLive = (session.activeSubAgentRuns?.size || 0) > 0;
2737
+ if (lanesLive && !session._lanesPreservedAtTurnEnd) {
2738
+ session._lanesPreservedAtTurnEnd = true;
2739
+ const n = session.activeSubAgentRuns.size;
2740
+ process.stderr.write(` ${c.dim(`${n} agent run${n === 1 ? '' : 's'} still active in background — progress continues below`)}\n`);
2741
+ } else {
2742
+ session._lanesPreservedAtTurnEnd = false;
2743
+ if (showSubAgentTools(getVerbosity())) resetFoldedSubAgentTools();
2744
+ else flushFoldedSubAgentTools();
2745
+ resetSubAgents();
2746
+ session.activeSubAgentRuns?.clear();
2747
+ setSubAgentWindowActive(false);
2748
+ }
2429
2749
  session.inSubAgent = false;
2430
2750
 
2431
2751
  const summary = data?.summary || '';
@@ -2839,57 +3159,160 @@ async function prepareDirectAgentRunContext(ctx, instruction = '') {
2839
3159
  return execContext;
2840
3160
  }
2841
3161
 
3162
+ function stripWrappingQuotes(value = '') {
3163
+ const text = String(value || '').trim();
3164
+ if (text.length >= 2) {
3165
+ const first = text[0];
3166
+ const last = text[text.length - 1];
3167
+ if ((first === '"' || first === "'") && first === last) {
3168
+ return text.slice(1, -1).trim();
3169
+ }
3170
+ }
3171
+ return text;
3172
+ }
3173
+
3174
+ function addRunTarget(map, agent, kind = 'agent') {
3175
+ const slug = String(agent?.slug || agent?.command || agent?.name || '').trim();
3176
+ if (!slug || map.has(`${kind}:${slug}`)) return;
3177
+ map.set(`${kind}:${slug}`, {
3178
+ slug,
3179
+ name: agent?.name || slug,
3180
+ description: agent?.description || '',
3181
+ scope: agent?.source_scope || agent?.source || kind,
3182
+ kind,
3183
+ });
3184
+ }
3185
+
3186
+ function printRunUsage(ctx) {
3187
+ process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
3188
+ process.stderr.write(` ${c.gray('Example: /run docker-analyzer Analyze all running Docker containers')}\n`);
3189
+
3190
+ const targets = new Map();
3191
+ for (const agent of listLocalAgents(safeCwd())) addRunTarget(targets, agent);
3192
+ for (const agent of BUILTIN_AGENTS) addRunTarget(targets, agent);
3193
+ for (const agent of ctx.toolExecutor?.listRunnables?.() || []) addRunTarget(targets, agent);
3194
+ for (const agent of pluginRegistry?.listAgents?.() || []) addRunTarget(targets, agent);
3195
+
3196
+ const agents = [...targets.values()].filter(item => item.kind === 'agent').slice(0, 12);
3197
+ if (agents.length) {
3198
+ process.stderr.write(`\n ${c.dim('Agents')}\n`);
3199
+ for (const agent of agents) {
3200
+ const desc = agent.description ? ` ${c.dim('- ' + agent.description)}` : '';
3201
+ const scope = agent.scope ? ` ${c.dim('[' + agent.scope + ']')}` : '';
3202
+ process.stderr.write(` ${c.brand(agent.slug)}${scope}${desc}\n`);
3203
+ }
3204
+ }
3205
+
3206
+ const workflows = listLocalWorkflows(safeCwd()).slice(0, 8);
3207
+ if (workflows.length) {
3208
+ process.stderr.write(`\n ${c.dim('Workflows')}\n`);
3209
+ for (const workflow of workflows) {
3210
+ const slug = workflow.slug || workflow.name;
3211
+ const desc = workflow.description ? ` ${c.dim('- ' + workflow.description)}` : '';
3212
+ process.stderr.write(` ${c.brand(slug)}${desc}\n`);
3213
+ }
3214
+ }
3215
+ }
3216
+
2842
3217
  async function handleRunCommand(rest = '', ctx) {
2843
3218
  const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
2844
3219
  const target = parts.shift();
2845
- const instruction = parts.join(' ');
3220
+ const instruction = stripWrappingQuotes(parts.join(' '));
2846
3221
 
2847
3222
  if (!target) {
2848
- process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
3223
+ printRunUsage(ctx);
2849
3224
  return;
2850
3225
  }
2851
3226
 
2852
3227
  const localAgent = listLocalAgents(safeCwd()).find(agent => localAgentMatches(agent, target));
2853
3228
  const builtinAgent = findBuiltinAgent(target);
2854
- const runnableAgent = localAgent || builtinAgent;
2855
- if (runnableAgent) {
2856
- try {
2857
- const execContext = await prepareDirectAgentRunContext(ctx, instruction || target);
2858
- return await runAgentDefinition(runnableAgent, instruction, ctx, session, renderEvent, {
2859
- cwd: execContext.cwd,
2860
- execContext,
2861
- });
2862
- } catch (err) {
2863
- process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
3229
+ const registeredAgent = ctx.toolExecutor?.listRunnables?.()
3230
+ ?.find(agent => localAgentMatches(agent, target));
3231
+ const pluginAgent = pluginRegistry?.listAgents?.()
3232
+ ?.find(agent => localAgentMatches(agent, target));
3233
+ const runnableAgent = localAgent || builtinAgent || registeredAgent || pluginAgent;
3234
+
3235
+ const creds = ctx.auth?.loadCredentials?.() || {};
3236
+ const dispatchCtx = {
3237
+ toolExecutor: ctx.toolExecutor,
3238
+ listRunnables: () => ctx.toolExecutor?.listRunnables?.() || [],
3239
+ listLocalWorkflows: () => listLocalWorkflows(safeCwd()),
3240
+ renderEvent,
3241
+ sessionSubstrate: makeSessionSubstrate(ctx),
3242
+ auth: { token: creds.token || null },
3243
+ credentials: {
3244
+ apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
3245
+ openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
3246
+ },
3247
+ cwd: safeCwd(),
3248
+ };
3249
+
3250
+ try {
3251
+ if (!runnableAgent) process.stderr.write(` ${c.dim(`Running workflow '${target}'...`)}\n`);
3252
+ const outcome = await dispatch({
3253
+ type: 'manual',
3254
+ source: 'repl:/run',
3255
+ target: runnableAgent ? { kind: 'agent', slug: target, agent: runnableAgent } : target,
3256
+ params: { instruction },
3257
+ channel: null,
3258
+ }, dispatchCtx);
3259
+
3260
+ if (!outcome.dispatched) {
3261
+ process.stderr.write(` ${c.red('✗')} ${outcome.reason}\n`);
3262
+ process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
2864
3263
  return;
2865
3264
  }
2866
- }
2867
3265
 
2868
- try {
2869
- process.stderr.write(` ${c.dim(`Running workflow '${target}'...`)}\n`);
2870
- const result = await ctx.toolExecutor.execute('workflow_run_multi', {
2871
- name: target,
2872
- instruction,
2873
- });
3266
+ const result = outcome.result || {};
3267
+ if (outcome.channel === 'server') {
3268
+ if (result?.success === false) {
3269
+ process.stderr.write(` ${c.red('✗')} ${result.output || `Workflow '${target}' failed.`}\n`);
3270
+ return;
3271
+ }
3272
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`Workflow '${target}' complete`)}\n`);
3273
+ const details = [];
3274
+ if (result?.run_id) details.push(`run ${result.run_id}`);
3275
+ if (result?.duration_s) details.push(`${result.duration_s}s`);
3276
+ if (result?.total_tokens) details.push(`${formatTokens(result.total_tokens)} tok`);
3277
+ if (result?.total_cost) details.push(formatCostValue(result.total_cost));
3278
+ if (details.length) process.stderr.write(` ${c.dim(details.join(' · '))}\n`);
3279
+
3280
+ // Push server workflow output to conversation history so the main
3281
+ // agent sees the outcome next turn (mirrors local graph run below).
3282
+ const serverOutput = result?.result || result?.output || '';
3283
+ if (serverOutput && result?.success !== false) {
3284
+ session.history.push(
3285
+ { role: 'user', content: `[${target}] ${instruction || 'run'}` },
3286
+ { role: 'assistant', content: String(serverOutput) },
3287
+ );
3288
+ } else if (result?.success === false && result?.output) {
3289
+ session.history.push(
3290
+ { role: 'user', content: `[${target}] ${instruction || 'run'}` },
3291
+ { role: 'assistant', content: `Workflow failed: ${String(result.output)}` },
3292
+ );
3293
+ }
2874
3294
 
2875
- if (result?.success === false) {
2876
- process.stderr.write(` ${c.red('✗')} ${result.output || `Workflow '${target}' failed.`}\n`);
3295
+ const output = serverOutput;
3296
+ if (output) {
3297
+ process.stderr.write('\n');
3298
+ process.stderr.write(renderMarkdown(String(output), { width: process.stderr.columns || 96 }));
3299
+ process.stderr.write('\n');
3300
+ }
2877
3301
  return;
2878
3302
  }
2879
3303
 
2880
- process.stderr.write(` ${c.green('✓')} ${c.dim(`Workflow '${target}' complete`)}\n`);
2881
- const details = [];
2882
- if (result?.run_id) details.push(`run ${result.run_id}`);
2883
- if (result?.duration_s) details.push(`${result.duration_s}s`);
2884
- if (result?.total_tokens) details.push(`${formatTokens(result.total_tokens)} tok`);
2885
- if (result?.total_cost) details.push(formatCostValue(result.total_cost));
2886
- if (details.length) process.stderr.write(` ${c.dim(details.join(' · '))}\n`);
2887
-
2888
- const output = result?.result || result?.output || '';
2889
- if (output) {
2890
- process.stderr.write('\n');
2891
- process.stderr.write(renderMarkdown(String(output), { width: process.stderr.columns || 96 }));
2892
- process.stderr.write('\n');
3304
+ // Local graph run. Session-substrate agent runs already push their own
3305
+ // conversation history; feed workflow results (and logged-out direct
3306
+ // runs) back so the main agent sees the outcome next turn.
3307
+ if (result.status === 'failed') {
3308
+ process.stderr.write(` ${c.red('✗')} ${result.output || `'${target}' failed.`}\n`);
3309
+ }
3310
+ const historyCovered = Boolean(runnableAgent) && Boolean(creds.token);
3311
+ if (result.output && result.status !== 'failed' && !historyCovered) {
3312
+ session.history.push(
3313
+ { role: 'user', content: `[${target}] ${instruction || 'run'}` },
3314
+ { role: 'assistant', content: String(result.output) },
3315
+ );
2893
3316
  }
2894
3317
  } catch (err) {
2895
3318
  process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
@@ -2897,6 +3320,38 @@ async function handleRunCommand(rest = '', ctx) {
2897
3320
  }
2898
3321
  }
2899
3322
 
3323
+ // Bridges the graph engine's session substrate onto runAgentDefinition:
3324
+ // the node's events stream through the same renderer, and the run keeps
3325
+ // its existing conversation-history feedback.
3326
+ function makeSessionSubstrate(ctx) {
3327
+ return (agent, node, instruction) => (async function* () {
3328
+ const queue = [];
3329
+ let notify = null;
3330
+ let finished = false;
3331
+ const push = (event) => {
3332
+ queue.push(event);
3333
+ const wake = notify; notify = null;
3334
+ wake?.();
3335
+ };
3336
+ const execContext = await prepareDirectAgentRunContext(ctx, instruction);
3337
+ const done = runAgentDefinition(agent, instruction, ctx, session, push, {
3338
+ cwd: execContext.cwd,
3339
+ execContext,
3340
+ pluginRegistry,
3341
+ }).catch(err => push({ type: 'error', data: { message: err?.message || String(err) } }))
3342
+ .finally(() => {
3343
+ finished = true;
3344
+ const wake = notify; notify = null;
3345
+ wake?.();
3346
+ });
3347
+ while (!finished || queue.length) {
3348
+ if (!queue.length) await new Promise(resolve => { notify = resolve; });
3349
+ while (queue.length) yield queue.shift();
3350
+ }
3351
+ await done;
3352
+ })();
3353
+ }
3354
+
2900
3355
  async function handleCommand(input, ctx) {
2901
3356
  const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
2902
3357
  if (aliasTarget) {
@@ -3449,7 +3904,8 @@ async function handleCommand(input, ctx) {
3449
3904
  session.agentHistory.length = 0;
3450
3905
  session.toolCalls = 0;
3451
3906
  session.subAgentToolCalls = 0;
3452
- runtime.foldedSubAgentTools = null;
3907
+ session.activeSubAgentRuns = new Map();
3908
+ resetFoldedSubAgentTools();
3453
3909
  clearCards();
3454
3910
  process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
3455
3911
  return;
@@ -3889,6 +4345,26 @@ export async function startTerminalRepl() {
3889
4345
 
3890
4346
  const ctx = { auth, toolExecutor: null, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
3891
4347
 
4348
+ // Wake-on-finish: background jobs with on_complete dispatch their target
4349
+ // agent through the trigger funnel when they exit. The ctx builder runs
4350
+ // lazily at fire time so it sees the live tool executor.
4351
+ registerJobCompletionDispatch(() => {
4352
+ const creds = ctx.auth?.loadCredentials?.() || {};
4353
+ return {
4354
+ toolExecutor: ctx.toolExecutor,
4355
+ listRunnables: () => ctx.toolExecutor?.listRunnables?.() || [],
4356
+ listLocalWorkflows: () => listLocalWorkflows(safeCwd()),
4357
+ renderEvent,
4358
+ sessionSubstrate: makeSessionSubstrate(ctx),
4359
+ auth: { token: creds.token || null },
4360
+ credentials: {
4361
+ apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
4362
+ openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
4363
+ },
4364
+ cwd: safeCwd(),
4365
+ };
4366
+ });
4367
+
3892
4368
  let startupOutputRow = 1;
3893
4369
  let startupOutputCol = 1;
3894
4370
 
@@ -3942,7 +4418,7 @@ export async function startTerminalRepl() {
3942
4418
  flushContent();
3943
4419
  flushPendingHead();
3944
4420
  flushExploreRun();
3945
- runtime.foldedSubAgentTools = null;
4421
+ resetFoldedSubAgentTools();
3946
4422
  clearCards();
3947
4423
 
3948
4424
  const preserved = {
@@ -4009,6 +4485,7 @@ export async function startTerminalRepl() {
4009
4485
  lastTurnDuration: 0,
4010
4486
  toolCounts: {},
4011
4487
  subAgentCounts: {},
4488
+ activeSubAgentRuns: new Map(),
4012
4489
  savedUsd: 0,
4013
4490
  lastTask: '',
4014
4491
  lastReasoning: '',
@@ -4381,6 +4858,9 @@ export async function startTerminalRepl() {
4381
4858
  let _bracketedPasteStartLine = '';
4382
4859
  let _bracketedPasteStartCursor = 0;
4383
4860
  let _promptHasInsertedPaste = false;
4861
+ let _suppressRawPasteLines = false;
4862
+ let _pastedInputValue = '';
4863
+ let _pastedInputLabel = '';
4384
4864
  const _pasteEndListeners = new Set();
4385
4865
  function onBracketedPasteEnd(cb) { _pasteEndListeners.add(cb); return () => _pasteEndListeners.delete(cb); }
4386
4866
  function isInBracketedPaste() { return _inBracketedPaste; }
@@ -4400,7 +4880,25 @@ export async function startTerminalRepl() {
4400
4880
  while (i < s.length) {
4401
4881
  if (!_inBracketedPaste) {
4402
4882
  const start = s.indexOf(PASTE_BEGIN, i);
4403
- if (start === -1) return;
4883
+ if (start === -1) {
4884
+ if (isRawMultilinePasteChunk(s)) {
4885
+ _suppressRawPasteLines = true;
4886
+ const baseLine = String(rl?.line || '');
4887
+ const baseCursor = typeof rl?.cursor === 'number' ? rl.cursor : baseLine.length;
4888
+ setImmediate(() => {
4889
+ try {
4890
+ insertPromptText(normalizePastedText(s), {
4891
+ baseLine,
4892
+ baseCursor,
4893
+ fromPaste: true,
4894
+ });
4895
+ } finally {
4896
+ _suppressRawPasteLines = false;
4897
+ }
4898
+ });
4899
+ }
4900
+ return;
4901
+ }
4404
4902
  _inBracketedPaste = true;
4405
4903
  _bracketedPasteBuffer = '';
4406
4904
  _suppressBracketedPasteLines = true;
@@ -4662,7 +5160,11 @@ export async function startTerminalRepl() {
4662
5160
  const line = String(baseLine || '');
4663
5161
  const cursor = typeof baseCursor === 'number' ? Math.max(0, Math.min(line.length, baseCursor)) : line.length;
4664
5162
  const next = `${line.slice(0, cursor)}${payload}${line.slice(cursor)}`;
4665
- if (fromPaste) _promptHasInsertedPaste = true;
5163
+ if (fromPaste) {
5164
+ _promptHasInsertedPaste = true;
5165
+ _pastedInputValue = next;
5166
+ _pastedInputLabel = pastedTextLabel(payload);
5167
+ }
4666
5168
  replaceReadlineLine(next, cursor + payload.length);
4667
5169
  renderIdleDockInput();
4668
5170
  }
@@ -4702,15 +5204,28 @@ export async function startTerminalRepl() {
4702
5204
 
4703
5205
  function renderIdleDockInput() {
4704
5206
  if (!isInputDockMounted()) return false;
5207
+ const line = rl.line || '';
5208
+ let displayLine = line;
5209
+ let displayCursor = typeof rl.cursor === 'number' ? rl.cursor : null;
5210
+ let fixedRows = null;
5211
+ if (_pastedInputValue && line === _pastedInputValue) {
5212
+ displayLine = _pastedInputLabel;
5213
+ displayCursor = _pastedInputLabel.length;
5214
+ fixedRows = 1;
5215
+ } else if (_pastedInputValue) {
5216
+ _pastedInputValue = '';
5217
+ _pastedInputLabel = '';
5218
+ }
4705
5219
  // rl.cursor is readline's byte offset within rl.line. Threading it
4706
5220
  // through to focusDockInput makes arrow-key navigation visually move
4707
5221
  // the terminal cursor within the buffer instead of always landing at
4708
5222
  // the end of the string.
4709
- return renderDockInput(userPrompt(), rl.line || '', {
5223
+ return renderDockInput(userPrompt(), displayLine, {
4710
5224
  context: buildContextStrip(),
4711
5225
  meta: buildDockMeta(),
4712
5226
  tips: idleInputTips(),
4713
- cursor: typeof rl.cursor === 'number' ? rl.cursor : null,
5227
+ cursor: displayCursor,
5228
+ fixedRows,
4714
5229
  });
4715
5230
  }
4716
5231
 
@@ -4756,7 +5271,7 @@ export async function startTerminalRepl() {
4756
5271
  readline.emitKeypressEvents(process.stdin, rl);
4757
5272
  process.stdin.on('keypress', (_str, key = {}) => {
4758
5273
  if (!inputActive) return;
4759
- if (_inBracketedPaste || _suppressBracketedPasteLines) return;
5274
+ if (_inBracketedPaste || _suppressBracketedPasteLines || _suppressRawPasteLines) return;
4760
5275
  if (key.name === 'return' || key.name === 'enter') return;
4761
5276
  if (key.name === 'f2') {
4762
5277
  clearSlashHint();
@@ -4767,7 +5282,7 @@ export async function startTerminalRepl() {
4767
5282
  }
4768
5283
  setImmediate(() => {
4769
5284
  if (!inputActive) return;
4770
- if (_inBracketedPaste || _suppressBracketedPasteLines) return;
5285
+ if (_inBracketedPaste || _suppressBracketedPasteLines || _suppressRawPasteLines) return;
4771
5286
  if (slashHintVisible && key.name === 'tab' && acceptSlashHint()) return;
4772
5287
  if (slashHintVisible && key.name === 'down' && moveSlashHintSelection(1)) return;
4773
5288
  if (slashHintVisible && key.name === 'up' && moveSlashHintSelection(-1)) return;
@@ -4829,12 +5344,16 @@ export async function startTerminalRepl() {
4829
5344
  if (pastedLines.length > 1 || trailing) {
4830
5345
  const text = [...pastedLines, trailing].join('\n');
4831
5346
  _promptHasInsertedPaste = true;
5347
+ _pastedInputValue = text;
5348
+ _pastedInputLabel = pastedTextLabel(text);
4832
5349
  replaceReadlineLine(text);
4833
5350
  renderIdleDockInput();
4834
5351
  return;
4835
5352
  }
4836
5353
  const line = pastedLines.join('\n');
4837
5354
  _promptHasInsertedPaste = false;
5355
+ _pastedInputValue = '';
5356
+ _pastedInputLabel = '';
4838
5357
  queueOrRunLine(line);
4839
5358
  }
4840
5359
 
@@ -4844,7 +5363,7 @@ export async function startTerminalRepl() {
4844
5363
  // or the user pressed Enter normally), the debounce falls back to old
4845
5364
  // behavior — a single Enter flushes almost instantly.
4846
5365
  rl.on('line', async (line) => {
4847
- if (_suppressBracketedPasteLines) {
5366
+ if (_suppressBracketedPasteLines || _suppressRawPasteLines) {
4848
5367
  _pasteLines = [];
4849
5368
  if (_pasteFlushTimer) {
4850
5369
  clearTimeout(_pasteFlushTimer);
@@ -4893,6 +5412,8 @@ export async function startTerminalRepl() {
4893
5412
  let input = line.trim();
4894
5413
  const selectedSlashCommand = selectedSlashCommandFor(input);
4895
5414
  inputActive = false;
5415
+ _pastedInputValue = '';
5416
+ _pastedInputLabel = '';
4896
5417
  clearSlashHint();
4897
5418
  if (selectedSlashCommand) input = selectedSlashCommand;
4898
5419
  if (!input) {