@aragon-agent/cli 0.6.16 → 0.6.18

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 (79) hide show
  1. package/CHANGELOG.md +107 -51
  2. package/README.md +91 -48
  3. package/dist/agent/controller.js +52 -47
  4. package/dist/agent/headless.js +7 -0
  5. package/dist/agent/reducer.js +0 -4
  6. package/dist/cli.js +0 -13
  7. package/dist/commands/builtins.js +21 -20
  8. package/dist/commands/index-build.d.ts +39 -0
  9. package/dist/commands/index-build.js +61 -0
  10. package/dist/compaction/child.d.ts +10 -8
  11. package/dist/compaction/child.js +23 -25
  12. package/dist/compaction/limits.d.ts +0 -2
  13. package/dist/compaction/limits.js +0 -2
  14. package/dist/config/cli-commands.d.ts +2 -2
  15. package/dist/config/cli-commands.js +19 -15
  16. package/dist/config/load.d.ts +0 -17
  17. package/dist/config/load.js +0 -6
  18. package/dist/config/schema.d.ts +41 -9
  19. package/dist/config/schema.js +18 -11
  20. package/dist/exec/events.d.ts +16 -9
  21. package/dist/exec/events.js +2 -2
  22. package/dist/exec/runner.js +17 -1
  23. package/dist/fast/limits.d.ts +2 -2
  24. package/dist/fast/limits.js +2 -2
  25. package/dist/fast/prompt.d.ts +10 -11
  26. package/dist/fast/prompt.js +19 -20
  27. package/dist/fast/reviewer.d.ts +1 -1
  28. package/dist/fast/reviewer.js +1 -1
  29. package/dist/fast/types.d.ts +0 -2
  30. package/dist/fast/wiring.d.ts +18 -34
  31. package/dist/fast/wiring.js +20 -51
  32. package/dist/input/csiu-keys.js +13 -2
  33. package/dist/input/limits.d.ts +15 -0
  34. package/dist/input/limits.js +15 -0
  35. package/dist/input/win32-input-mode.js +7 -1
  36. package/dist/logging/install.d.ts +9 -0
  37. package/dist/logging/install.js +11 -0
  38. package/dist/session/validate-session.js +3 -1
  39. package/dist/team/child-reviewer.d.ts +105 -0
  40. package/dist/team/child-reviewer.js +117 -0
  41. package/dist/team/human-queue.d.ts +11 -0
  42. package/dist/team/human-queue.js +18 -1
  43. package/dist/team/limits.d.ts +40 -0
  44. package/dist/team/limits.js +41 -0
  45. package/dist/team/normalize.d.ts +7 -26
  46. package/dist/team/normalize.js +9 -22
  47. package/dist/team/overseer.d.ts +157 -0
  48. package/dist/team/overseer.js +348 -0
  49. package/dist/team/prompt.d.ts +0 -11
  50. package/dist/team/prompt.js +3 -12
  51. package/dist/team/report.d.ts +4 -19
  52. package/dist/team/report.js +27 -28
  53. package/dist/team/runtime.d.ts +76 -16
  54. package/dist/team/runtime.js +410 -57
  55. package/dist/team/subagent.d.ts +48 -19
  56. package/dist/team/subagent.js +88 -31
  57. package/dist/team/task-tool.d.ts +6 -26
  58. package/dist/team/task-tool.js +17 -48
  59. package/dist/team/types.d.ts +65 -41
  60. package/dist/ui/App.d.ts +1 -1
  61. package/dist/ui/App.js +64 -4
  62. package/dist/ui/BottomStatusRow.js +2 -1
  63. package/dist/ui/Header.js +13 -2
  64. package/dist/ui/PromptInput.js +11 -1
  65. package/dist/ui/StatusBar.js +2 -1
  66. package/dist/ui/TeamPanel.js +1 -1
  67. package/dist/ui/entries/TeamCard.js +1 -6
  68. package/dist/ui/layout/status-detail-layout.js +12 -2
  69. package/dist/ui/layout/status-layout.d.ts +4 -2
  70. package/dist/ui/layout/status-layout.js +24 -1
  71. package/dist/ui/overlays/HelpOverlay.js +2 -2
  72. package/package.json +2 -2
  73. package/skills/project-indexer/SKILL.md +163 -0
  74. package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
  75. package/skills/project-indexer/templates/cleancode-template.md +94 -0
  76. package/skills/project-indexer/templates/config-template.md +69 -0
  77. package/skills/project-indexer/templates/index-template.md +129 -0
  78. package/dist/ui/ModeChip.d.ts +0 -18
  79. package/dist/ui/ModeChip.js +0 -24
package/dist/ui/App.js CHANGED
@@ -48,6 +48,8 @@ import { useRenderGovernor } from './use-render-governor.js';
48
48
  import { readScrollbarPerf, setPerfResetHook, setPerfSnapshotProvider, } from '../commands/perf.js';
49
49
  import { buildFrameBudget } from './layout/budget.js';
50
50
  import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
51
+ import { INDEX_KEY_FRAME } from '../input/limits.js';
52
+ import { INDEX_SKILL_NAME, buildIndexConfirmSummary, hasProjectIndex, indexBuildInvocationArgs, } from '../commands/index-build.js';
51
53
  import { installConsoleBridge } from './console-bridge.js';
52
54
  import { publishExitSnapshot } from './exit-snapshot.js';
53
55
  import { OverlayFrame } from './layout/OverlayFrame.js';
@@ -648,6 +650,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
648
650
  dispatch({ type: 'teamUpdate', snapshot });
649
651
  break;
650
652
  }
653
+ case 'overseer': {
654
+ // A supervisor decision was applied (team-overseer). The roster
655
+ // already refreshed through the agent_update the runtime publishes
656
+ // with it; re-read the snapshot so the panel cannot miss the row.
657
+ const snap = controller.getTeamSnapshot();
658
+ if (snap)
659
+ dispatch({ type: 'teamUpdate', snapshot: snap });
660
+ break;
661
+ }
651
662
  case 'usage': {
652
663
  // Child spend is REAL SPEND and must reach the status bar, or the
653
664
  // session cost readout under-reports by however much the team consumed
@@ -662,10 +673,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
662
673
  // so it closes over the `cfg` object of the render it mounted in, and
663
674
  // `setModel` builds a NEW one.
664
675
  const live = controller.getConfig();
665
- const fastTier = event.tier === 'fast' ? controller.getFastStatus().tier : null;
666
- const ref = fastTier && fastTier.ok
667
- ? fastTier.ref
668
- : { providerId: live.provider, modelId: live.model };
676
+ // Every child runs the lead's own model (main-agent parity), so the
677
+ // lead's live connection IS the tier that billed.
678
+ const ref = { providerId: live.provider, modelId: live.model };
669
679
  // Unknown pricing contributes ZERO rather than a fabricated figure
670
680
  // (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
671
681
  const costDelta = controller.isPricedModel(ref)
@@ -1554,6 +1564,46 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1554
1564
  setConfirmState(null);
1555
1565
  dispatch({ type: 'setOverlay', overlay: null });
1556
1566
  };
1567
+ // --- Ctrl+I: run the bundled project-indexer skill. -------------------
1568
+ //
1569
+ // SAME PATH AS A TYPED COMMAND, ON PURPOSE. After the Yes/No gate (the
1570
+ // second confirmation the user asked for) this dispatches
1571
+ // `/skill:project-indexer <args>` through `executeSlashInput`, so the
1572
+ // load/activate/queue-frame/submit lifecycle and the streaming tool cards
1573
+ // in the transcript are the ones every skill command already has. The
1574
+ // busy guard refuses rather than queueing: a queued skill body would
1575
+ // arrive as steering mid-run, which is not what "build the index" means.
1576
+ const requestIndexBuild = () => {
1577
+ if (controller.isRunning() || interactionPhase.current !== 'idle') {
1578
+ // A notice entry, not a toast: the running status row's feedback slot is
1579
+ // owned by the activity phrase, and a busy warning nobody can read is
1580
+ // the same as no warning.
1581
+ notify('warn', 'The agent is busy. Let it finish before building the index (Ctrl+I).');
1582
+ return;
1583
+ }
1584
+ if (cfg.skills.enabled) {
1585
+ const record = controller.getSkillService().get(INDEX_SKILL_NAME);
1586
+ if (!record || record.disabled || record.invalid) {
1587
+ notify('warn', `Skill "${INDEX_SKILL_NAME}" is not available. Check /skills list.`);
1588
+ return;
1589
+ }
1590
+ }
1591
+ else {
1592
+ notify('warn', 'Skills are disabled; the project index needs the bundled skill.');
1593
+ return;
1594
+ }
1595
+ const cwd = controller.getCwd();
1596
+ const existing = hasProjectIndex(cwd);
1597
+ void (async () => {
1598
+ const approved = await new Promise((resolve) => {
1599
+ setConfirmState({ summary: buildIndexConfirmSummary({ cwd, hasExistingIndex: existing }), resolve });
1600
+ dispatch({ type: 'setOverlay', overlay: 'confirm' });
1601
+ });
1602
+ if (approved) {
1603
+ await executeSlashInput(`/skill:${INDEX_SKILL_NAME} ${indexBuildInvocationArgs(existing)}`);
1604
+ }
1605
+ })();
1606
+ };
1557
1607
  /** Answer every outstanding request with the same response and close up. */
1558
1608
  const resolveHuman = (response) => {
1559
1609
  for (const entry of [...pendingHuman.current])
@@ -1715,6 +1765,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1715
1765
  setStatusExpanded(value => !value);
1716
1766
  return;
1717
1767
  }
1768
+ // Ctrl+I arrives as INDEX_KEY_FRAME, never as key.ctrl + 'i': legacy
1769
+ // terminals encode it as the Tab byte and the stdin filter's translators
1770
+ // are the only layer that can tell them apart (see input/limits.ts).
1771
+ // Guarded exactly like Ctrl+G above it.
1772
+ if (input.includes(INDEX_KEY_FRAME)) {
1773
+ if (tooSmall || stateRef.current.overlay)
1774
+ return;
1775
+ requestIndexBuild();
1776
+ return;
1777
+ }
1718
1778
  if (tooSmall)
1719
1779
  return;
1720
1780
  // R-P1-7: the overlay branch MUST come before the transcript scroll branch
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from 'react';
4
4
  import { Box, Text } from 'ink';
5
5
  export function BottomStatusRow({ plan, columns, theme }) {
6
- const tones = { normal: theme.primary, muted: theme.muted, warning: theme.noticeWarn, error: theme.noticeError };
6
+ const tones = { normal: theme.primary, muted: theme.muted, warning: theme.noticeWarn, error: theme.noticeError,
7
+ accent: theme.accent };
7
8
  return _jsxs(Box, { height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), plan.fields.map((field, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: plan.separator }) }), _jsx(Box, { width: field.cells, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: tones[field.tone], children: field.text }) })] }, field.id))] });
8
9
  }
package/dist/ui/Header.js CHANGED
@@ -5,7 +5,18 @@ import { Logo } from './Logo.js';
5
5
  export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false }) {
6
6
  const scroll = Number.isFinite(scrolledLines) && scrolledLines > 0
7
7
  ? (scrolledLines > 9999 ? '^9999+' : '^' + Math.floor(scrolledLines)) + (overlayOpen ? ' ' : ' ') : '';
8
+ // The Ctrl+I project-index affordance sits LEFT of the ^G detail toggle:
9
+ // collapsed it names the key and the noun ('^I Index'); expanded - the
10
+ // same Ctrl+G state that adds the status detail row - it spells the action
11
+ // ('^I build index'). Hidden while an overlay owns the header and below the
12
+ // 72-column cutoff the composer hints also use: on a narrow terminal the
13
+ // scroll chip and the ^G toggle are the two affordances that must survive.
14
+ const indexHint = overlayOpen || columns < 72
15
+ ? ''
16
+ : (statusExpanded ? '^I build index ' : '^I Index ');
8
17
  const detail = overlayOpen ? (statusExpanded ? 'Details on' : 'Details off')
9
- : '^G ' + (statusExpanded ? 'less' : 'more');
10
- return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - 18), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: 18, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: scroll + detail }) })] });
18
+ : indexHint + '^G ' + (statusExpanded ? 'less' : 'more');
19
+ // Widest content: '^9999+ ' (8) + '^I build index ^G less' (23) = 31.
20
+ const detailCells = indexHint ? 32 : 18;
21
+ return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - detailCells), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: detailCells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: scroll + detail }) })] });
11
22
  }
@@ -37,7 +37,7 @@ import { draftMaxRows } from './composer-limits.js';
37
37
  import { layoutComposer, splitRowAtColumn } from './composer-rows.js';
38
38
  import { moveVisualCursor, snapGrapheme, stepGrapheme } from './editor-navigation.js';
39
39
  import { interactionCopy } from './interaction-copy.js';
40
- import { PASTE_DRAFT_MAX_BYTES, PASTE_MAX_BLOCKS, formatPasteSize, } from '../input/limits.js';
40
+ import { INDEX_KEY_FRAME, PASTE_DRAFT_MAX_BYTES, PASTE_MAX_BLOCKS, formatPasteSize, } from '../input/limits.js';
41
41
  // ---------------------------------------------------------------------------
42
42
  // The scroll chip (§4.2)
43
43
  // ---------------------------------------------------------------------------
@@ -488,6 +488,16 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
488
488
  insert('\n');
489
489
  return;
490
490
  }
491
+ // App owns the Ctrl+I index frame (see input/limits.ts). Standalone it
492
+ // never reaches here - isControlSeq drops it at the printable-insert
493
+ // branch below - but a frame glued to typed text in one chunk must
494
+ // insert ONLY the text, so strip it before any other interpretation.
495
+ if (input.includes(INDEX_KEY_FRAME)) {
496
+ const rest = input.split(INDEX_KEY_FRAME).join('');
497
+ if (rest.length > 0)
498
+ insert(rest);
499
+ return;
500
+ }
491
501
  if (hasPasteFrame(input) || hasEnterFrame(input) || /[\r\n]/.test(input)) {
492
502
  const intents = splitEnterFrames(input).map((frame) => frame.kind === 'paste' ? { ...frame, id: allocatePasteId() } : frame);
493
503
  if (intents.length > 0)
@@ -27,7 +27,8 @@ export function StatusBar(props) {
27
27
  ecoRung: props.ecoRung, fastActive: props.fastActive, scrolledLines: props.scrolledLines,
28
28
  });
29
29
  const tones = { normal: props.theme.primary, muted: props.theme.muted,
30
- warning: props.theme.noticeWarn, error: props.theme.noticeError };
30
+ warning: props.theme.noticeWarn, error: props.theme.noticeError,
31
+ accent: props.theme.accent };
31
32
  return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
32
33
  ? _jsxs(_Fragment, { children: [spinner ?? glyph, item.text.slice(glyph.length)] })
33
34
  : item.text }) })] }, item.id))] });
@@ -130,7 +130,7 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
130
130
  // `reducedMotion` and a non-Unicode terminal fall back to the same
131
131
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
132
132
  const animate = isRunning(run) && !reducedMotion && caps.unicode;
133
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
133
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
134
134
  }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
135
135
  }
136
136
  function marker(run, glyphs) {
@@ -48,12 +48,7 @@ function TeamCardImpl(props) {
48
48
  : `${formatDuration(durationMs ?? 0)} (${ok} ok, ${failed} failed)`;
49
49
  const count = `${runs.length} ${runs.length === 1 ? 'subagent' : 'subagents'}`;
50
50
  const dropped = requested > runs.length ? ` (${runs.length} of ${requested} requested)` : '';
51
- // ABSENT WHEN NO FAST CHILD RAN, so an ordinary dispatch's card is unchanged
52
- // (fast-model-tier §6). `n fast` rather than a ratio: the total is already one
53
- // column to the left.
54
- const fastCount = runs.filter((r) => r.tier === 'fast').length;
55
- const fastNote = fastCount > 0 ? ` ${fastCount} fast` : '';
56
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, fastNote, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [' ', `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
51
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [' ', run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
57
52
  runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
58
53
  }
59
54
  /**
@@ -1,6 +1,17 @@
1
1
  import { formatCost, formatTokens, promptTokensOf } from '../../agent/usage.js';
2
2
  import { actionCandidates, clipStatusText, escapeClause } from '../interaction-hints.js';
3
+ import { MODE_LABEL } from '../../agent/agent-mode.js';
3
4
  import { statusField, statusLinePlan, validContext } from './status-layout.js';
5
+ /**
6
+ * Uppercase mode word for the details row, mirroring the primary row's field:
7
+ * same vocabulary (`MODE_LABEL`), same `CUR>PENDING` deferred-switch shape.
8
+ */
9
+ function detailModeText(mode, pendingMode) {
10
+ const word = (value) => MODE_LABEL[value] ?? value.toUpperCase();
11
+ if (pendingMode)
12
+ return `${word(mode || 'build')}>${word(pendingMode)}`;
13
+ return word(mode || 'build');
14
+ }
4
15
  export function planStatusDetail(input) {
5
16
  const { status, hints } = input;
6
17
  const budget = Math.max(0, Math.floor(status.columns) - 1);
@@ -26,8 +37,7 @@ export function planStatusDetail(input) {
26
37
  if (status.servicesActive?.live)
27
38
  add(statusField('services', 'Svc' + status.servicesActive.live));
28
39
  if (status.mode || status.pendingMode)
29
- add(statusField('mode', status.pendingMode
30
- ? (status.mode ?? 'build') + '>' + status.pendingMode : status.mode));
40
+ add(statusField('mode', detailModeText(status.mode, status.pendingMode), 'accent'));
31
41
  if (validContext(status.context)) {
32
42
  const approximate = status.context.source === 'estimate' || status.context.deltaTokens > 0 ? '~' : '';
33
43
  add(statusField('context', 'Context ' + approximate + formatTokens(status.context.occupied)
@@ -25,6 +25,7 @@ export interface StatusLayoutInput {
25
25
  max: number;
26
26
  secondsLeft: number;
27
27
  };
28
+ /** Named on the primary row in EVERY mode - the §6.4 guarantee, restored. */
28
29
  mode?: string;
29
30
  pendingMode?: string | null;
30
31
  servicesActive?: {
@@ -54,7 +55,8 @@ export interface StatusField {
54
55
  id: string;
55
56
  text: string;
56
57
  cells: number;
57
- tone: 'normal' | 'muted' | 'warning' | 'error';
58
+ /** 'accent' carries the mode word - the row's one always-on emphasis. */
59
+ tone: 'normal' | 'muted' | 'warning' | 'error' | 'accent';
58
60
  }
59
61
  export interface StatusLinePlan {
60
62
  fields: readonly StatusField[];
@@ -68,5 +70,5 @@ export declare function statusLinePlan(fields: readonly StatusField[], separator
68
70
  export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive'>): readonly [string, string];
69
71
  export declare function validContext(context: ContextUsageSnapshot): boolean;
70
72
  export declare function shortDuration(ms: number): string;
71
- /** All five fields survive each width rung; only the redraw carrier is outside this plan. */
73
+ /** Every field survives each width rung; only the redraw carrier is outside this plan. */
72
74
  export declare function planPrimaryStatusFields(input: StatusPrimaryInput): StatusLinePlan;
@@ -1,6 +1,7 @@
1
1
  import stringWidth from 'string-width';
2
2
  import stripAnsi from 'strip-ansi';
3
3
  import { formatDuration } from '../../agent/usage.js';
4
+ import { MODE_LABEL } from '../../agent/agent-mode.js';
4
5
  import { interactionCopy as copy } from '../interaction-copy.js';
5
6
  export function cleanStatusText(text) {
6
7
  // eslint-disable-next-line no-control-regex
@@ -89,7 +90,27 @@ function feedbackLabel(input, form) {
89
90
  return labels[feedback.kind][form === 'full' ? 0 : form === 'short' ? 1 : 2];
90
91
  }
91
92
  const thinkingCodes = { off: 'O', minimal: 'N', low: 'L', medium: 'M', high: 'H', xhigh: 'X' };
92
- /** All five fields survive each width rung; only the redraw carrier is outside this plan. */
93
+ /**
94
+ * The mode word for the primary row: full rungs name the mode with
95
+ * `MODE_LABEL`, tight rungs keep only its first letter, and a deferred switch
96
+ * renders `CUR>PENDING` in both tiers. `null` when the caller names no mode at
97
+ * all, so callers and tests that predate the field keep a byte-identical plan.
98
+ * The word must survive every width rung: the border color cannot be the only
99
+ * mode signal the default view carries (plan-mode P1-6).
100
+ */
101
+ function modeFieldText(mode, pendingMode, full) {
102
+ if (!mode && !pendingMode)
103
+ return null;
104
+ const word = (value) => {
105
+ const label = MODE_LABEL[value];
106
+ if (!label)
107
+ return full ? value.toUpperCase() : (value[0] ?? '?').toUpperCase();
108
+ return full ? label : label[0];
109
+ };
110
+ const current = word(mode || 'build');
111
+ return pendingMode ? `${current}>${word(pendingMode)}` : current;
112
+ }
113
+ /** Every field survives each width rung; only the redraw carrier is outside this plan. */
93
114
  export function planPrimaryStatusFields(input) {
94
115
  const budget = Math.max(0, (Number.isFinite(input.columns) ? Math.floor(input.columns) : 80) - 1);
95
116
  const labels = phaseLabels(input);
@@ -104,8 +125,10 @@ export function planPrimaryStatusFields(input) {
104
125
  const full = rung === 0;
105
126
  const label = feedbackLabel(input, full ? 'full' : rung >= 4 ? 'tiny' : 'short')
106
127
  ?? (full ? labels[1] : rung >= 4 ? labels[0].slice(0, 4) : labels[0]);
128
+ const modeText = modeFieldText(input.mode, input.pendingMode, full);
107
129
  return statusLinePlan([
108
130
  statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, tone),
131
+ ...(modeText ? [statusField('mode', modeText, 'accent')] : []),
109
132
  statusField('context', (full ? 'Context ' : 'C') + pct),
110
133
  statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
111
134
  : (rung >= 2 ? '' : 'Th:') + code),
@@ -20,6 +20,7 @@ function keyRows(times) {
20
20
  [`Ctrl+C ${times}2`, 'Exit'],
21
21
  ['Ctrl+L', 'Redraw the frame'],
22
22
  ['Ctrl+T', 'Show/hide thinking (off by default)'],
23
+ ['Ctrl+I', 'Build the project index (bundled skill; asks first; needs keyboard enhancement)'],
23
24
  ['Ctrl+G', 'Expand/collapse status details; disabled while an overlay is open'],
24
25
  ['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
25
26
  ['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
@@ -31,6 +32,7 @@ function keyRows(times) {
31
32
  ['Drag right edge', 'Scroll messages; input stays fixed; type to return to newest output'],
32
33
  ['Bottom rows', 'Fixed input, one status row; Ctrl+G adds one detail row'],
33
34
  ['C / Th / t/s', 'Context percent / thinking level / run-average output tokens per second'],
35
+ ['B / P / P>B', 'Status-row mode word: BUILD / PLAN; CUR>PENDING = switch deferred to run end'],
34
36
  ['O/N/L/M/H/X', 'Thinking: off/minimal/low/medium/high/xhigh'],
35
37
  ['~ / ? / --', 'Estimated context / unknown context / unavailable output speed'],
36
38
  ['Output speed', 'Main Agent reported output tokens / total run time, including tools and waits'],
@@ -40,7 +42,6 @@ function keyRows(times) {
40
42
  ['Ctrl+C (with a selection)', 'Copy the pending selection - does not arm exit'],
41
43
  ['Native terminal selection', 'Copy-on-select is controlled by your terminal settings'],
42
44
  ['Queue / /queue', 'Pending receipt; /queue opens full text, PgUp/PgDn pages, Esc closes'],
43
- ['plan>build / build>plan', 'A mode switch pending until the current run ends'],
44
45
  ['/bg / /todo status', 'Show full service and TODO counts when status fields are hidden'],
45
46
  ['Queue paused', 'Unreceived messages persist after interruption until accepted or cancelled'],
46
47
  ['Up / Down', 'Prompt history (empty input)'],
@@ -113,7 +114,6 @@ const FAST_ROWS = [
113
114
  ['/fast model <id>', 'Set the fast model; accepts provider:model'],
114
115
  ['/fast same', 'Run the fast tier on the main model'],
115
116
  ['/fast review <n> | off', 'Turns between automatic reviews, or turn them off'],
116
- ['/fast delegate on | off', 'Allow model:"fast" on task subagents'],
117
117
  ['--fast / --no-fast', 'Start a session with the tier on or off (also ARAGON_FAST=1)'],
118
118
  ['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
119
119
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.28",
59
+ "@aragon-agent/core": "^0.2.30",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: project-indexer
3
+ description: Generate and use a project index for quick codebase understanding in new sessions. Scans the workspace, extracts exported symbols, writes .claude-index/index.md plus config, and injects a Google-style Clean Code Guidelines section into CLAUDE.md. Use when the user says "explore the project", "generate index", "regenerate index", "更新索引", "生成索引", or presses Ctrl+I.
4
+ version: 1.1.0
5
+ license: MIT
6
+ keywords: [productivity, codebase, indexing, navigation, cleancode]
7
+ # The read-only tools (read_file, list_dir, glob, grep, ask_user, todo_write)
8
+ # are always available inside a skill frame via the tool floor; this ceiling
9
+ # adds exactly the two writers the generated files need.
10
+ allowed-tools:
11
+ - write_file
12
+ - edit_file
13
+ ---
14
+
15
+ # Project Indexer
16
+
17
+ Generate a structured index of any codebase so a new session understands the
18
+ project with minimal context usage. The index is a **navigation map**, not a
19
+ code copy: enough to see the project globally (type, stack, architecture),
20
+ navigate on demand (feature map, file index), and know which symbols exist
21
+ without reading every file.
22
+
23
+ ## Current request
24
+
25
+ $ARGUMENTS
26
+
27
+ ## Step 1: Check for an existing index
28
+
29
+ - If `.claude-index/index.md` exists and has the key sections (Project
30
+ Overview, Feature Map, File Index), go to "Using an existing index".
31
+ - If it exists but is empty or malformed, tell the user and regenerate
32
+ (first-time flow), preserving `config.md` if intact.
33
+ - Otherwise run the first-time flow.
34
+
35
+ ## Using an existing index
36
+
37
+ 1. Read `.claude-index/index.md` and `CLAUDE.md` (if present).
38
+ 2. Summarize in 3-5 bullets: structure, stack, feature areas, key entry
39
+ points. Offer to help.
40
+ 3. For later questions, navigate via the Feature Map / File Index and read
41
+ real source only when detail is needed.
42
+
43
+ ## First-time flow
44
+
45
+ ### Step 2: Gather configuration (ask_user)
46
+
47
+ Ask with `ask_user`, defaults offered in every question:
48
+
49
+ 1. **Exclusions** - A) defaults (node_modules, .git, dist, build,
50
+ __pycache__, .venv, vendor, coverage, .next, .nuxt, .cache, target, bin,
51
+ obj, .idea, .vscode) B) add more C) custom list.
52
+ 2. **Priority directories** - A) auto-detect (recommended) B) specify C) all
53
+ equally.
54
+ 3. **Tech stack** - auto-detect unless the user names one.
55
+ 4. **Clean Code Guidelines** (injected into CLAUDE.md) - A) Google strict
56
+ (method 60, file 1000 - recommended) B) Google relaxed (method 600)
57
+ C) custom D) skip.
58
+
59
+ If C: ask **one `ask_user` call per limit** showing default and range -
60
+ MAX_FILE_LINES 1000 (200-5000), MAX_METHOD_LINES 60 (20-600),
61
+ MAX_FUNCTION_PARAMS 5 (2-10), MAX_CYCLOMATIC 10 (5-25); fall back to the
62
+ default on an out-of-range answer and say so in the summary.
63
+ MAX_LINE_LENGTH (default 100, Python 80) and MAX_NESTING_DEPTH (4) are never
64
+ asked. If the user skips every question, use all defaults.
65
+
66
+ Treat the answer as `CleanCodeConfig.preset`: strict | relaxed | custom |
67
+ disabled. D (`disabled`) skips the Clean Code composition below, never adds
68
+ markers, and persists `Enabled: No`.
69
+
70
+ ### Step 3: Scan and analyze
71
+
72
+ 1. Traverse the tree with the exclusion rules (`glob`, never one giant read).
73
+ 2. Detect project type (frontend/backend/fullstack/library/CLI/monorepo),
74
+ languages with percentages, frameworks, entry points.
75
+ 3. Group files by feature/domain and extract exported symbols - full support
76
+ for JS/TS (.js/.jsx/.ts/.tsx/.mjs) and Python; best-effort for Java,
77
+ Kotlin, Go, Rust, C/C++, C#, Ruby, PHP; unknown types are listed without
78
+ symbols.
79
+ 4. Analyze module-level dependencies (import graph).
80
+
81
+ Adaptive sizing: <50 files -> full detail; 50-200 -> full coverage with
82
+ concise descriptions; 200+ -> detail the priority directories, summarize the
83
+ rest, group similar utilities. Over 1000 files after exclusions: warn and
84
+ suggest more exclusions before continuing; sample representative files with
85
+ glob/grep rather than reading everything. Keep language/framework findings in
86
+ working memory - do NOT write an intermediate JSON file.
87
+
88
+ ### Step 4: Write `.claude-index/`
89
+
90
+ 1. `config.md` - exclusions, priorities, tech stack, plus an Index Settings
91
+ block (Generated date, Project Root, Index Version) and a Clean Code
92
+ Settings block (Enabled / Preset / the six limits; when the user picked
93
+ skip, write `Enabled: No`, `Preset: disabled` and the strict numbers so a
94
+ later "enable cleancode" can flip it back).
95
+ 2. `index.md` - follow `templates/index-template.md` in this skill's
96
+ directory: Project Overview, Feature Map (per area: entry file, one-line
97
+ description, key exports with signatures), Module Dependencies, File Index
98
+ (per-directory tables, one line per file).
99
+
100
+ ### Step 5: Compose the Clean Code Guidelines (skip when disabled)
101
+
102
+ Read `templates/cleancode-template.md` and `templates/cleancode-overrides.md`
103
+ from this skill's directory, then:
104
+
105
+ 1. Primary languages = every language with >= 10% of post-exclusion source
106
+ files; if none clears 10%, use the `Other` row.
107
+ 2. Take the **minimum** across all matching rows for each numeric column.
108
+ 3. Apply the user preset: strict/relaxed values intersect with the language
109
+ minimum (never looser than the baseline); custom numbers are honoured
110
+ verbatim with divergence noted.
111
+ 4. Substitute every placeholder: `{{MAX_FILE_LINES}}`, `{{MAX_METHOD_LINES}}`,
112
+ `{{MAX_FUNCTION_PARAMS}}`, `{{MAX_CYCLOMATIC}}`, `{{MAX_LINE_LENGTH}}`,
113
+ `{{MAX_NESTING_DEPTH}}`, `{{PRIMARY_LANGUAGES}}` (comma-joined languages)
114
+ and `{{LANGUAGE_HINTS}}` (one `- Language: note` bullet per matching row,
115
+ deduped by exact string).
116
+ 5. Verify no `{{...}}` placeholder remains. Do not count characters at
117
+ runtime.
118
+
119
+ ### Step 6: Update CLAUDE.md
120
+
121
+ - `## Project Index` section: upsert (replace when present, else append at
122
+ end; create the file when missing). Contents: Location
123
+ `.claude-index/index.md`, Last Updated (YYYY-MM-DD), Contents line, a
124
+ Usage line, and "Regenerate: say 'regenerate index' / '更新索引'".
125
+ - `## Clean Code Guidelines` section (unless disabled): wrap the rendered
126
+ body in `<!-- project-indexer:cleancode:begin v1.1.0 -->` and
127
+ `<!-- project-indexer:cleancode:end -->`. Locate an existing block with
128
+ the version-agnostic regex `<!-- project-indexer:cleancode:begin
129
+ v\d+\.\d+\.\d+ -->` (NEVER a literal-version match) plus the literal end
130
+ marker; fallback: the region from a `## Clean Code Guidelines` heading to
131
+ the next `## ` heading or EOF. Replace the found block; if none, insert
132
+ after the Project Index section (first-insert only; when both sections
133
+ already exist preserve the user's ordering and everything between them).
134
+ Write UTF-8; add a trailing newline when missing.
135
+
136
+ ### Step 7: Report
137
+
138
+ List what was created/updated, the feature-area count, files indexed and
139
+ symbols extracted. Offer to start work.
140
+
141
+ ## Regeneration ("regenerate index" / "更新索引")
142
+
143
+ 1. Read `config.md` and preserve exclusions, priorities, stack.
144
+ 2. Clean Code handling: if CLAUDE.md has markers or the heading, ask
145
+ A) refresh B) keep C) remove; honour `Enabled: No` from config.md by
146
+ skipping silently with one reminder line. When no block exists and
147
+ Enabled is Yes/absent, compose and inject (announce before writing).
148
+ 3. Confirm the plan (exclusions, priorities, cleancode decision) with the
149
+ user, then re-run Steps 3-7.
150
+
151
+ Special triggers: "disable cleancode" removes the block and sets
152
+ `Enabled: No`; "enable cleancode" re-composes it and sets `Enabled: Yes` -
153
+ both without a full rescan.
154
+
155
+ ## Edge cases
156
+
157
+ - Inaccessible directories: list them under a "Skipped Directories" note and
158
+ continue; never fail the whole run for one unreadable path.
159
+ - No recognizable source files: generate a minimal index (overview + file
160
+ listing) and say so.
161
+ - The user pressed Ctrl+I: they already confirmed once - do not re-ask the
162
+ global "proceed?" question, but still ask the configuration questions
163
+ above (or reuse `.claude-index/config.md` when it exists).
@@ -0,0 +1,46 @@
1
+ # Clean Code — Language Override Table
2
+
3
+ > Read by `project-indexer` at Step 5. The skill intersects the rows below
4
+ > with the **primary languages** detected during the scan (any language whose
5
+ > source files account for >= 10% of the post-exclusion total).
6
+ >
7
+ > When two or more rows apply, the skill **takes the minimum** for every
8
+ > numeric column. Notes from each matching row are concatenated and rendered
9
+ > as bullets in the `{{LANGUAGE_HINTS}}` placeholder of
10
+ > `cleancode-template.md`.
11
+ >
12
+ > Defaults (used when no row matches, i.e. the `Other` row) live in the last
13
+ > row. Numeric units: `MAX_FILE_LINES` and `MAX_METHOD_LINES` are physical
14
+ > lines of source. `MAX_LINE_LENGTH` is columns. `MAX_FUNCTION_PARAMS` is the
15
+ > count of positional + keyword parameters in a single signature.
16
+
17
+ | Language | MAX_FILE_LINES | MAX_METHOD_LINES | MAX_LINE_LENGTH | MAX_FUNCTION_PARAMS | Notes |
18
+ |-------------|----------------|------------------|-----------------|---------------------|-------|
19
+ | TypeScript | 1000 | 60 | 100 | 5 | Prefer `interface` over `type` for public APIs. |
20
+ | JavaScript | 1000 | 60 | 100 | 5 | Avoid `any`; use JSDoc when no TS. |
21
+ | Python | 800 | 50 | 80 | 5 | PEP 8 + Google docstrings. |
22
+ | Go | 800 | 60 | 120 | 4 | Errors as values; no panic in library code. |
23
+ | Java | 1500 | 40 | 100 | 5 | Google Java Style. |
24
+ | Kotlin | 1500 | 40 | 100 | 5 | KtLint defaults. |
25
+ | Rust | 1000 | 60 | 100 | 5 | `clippy::pedantic` recommended. |
26
+ | C / C++ | 1000 | 50 | 100 | 5 | Google C++ Style. |
27
+ | Other | 1000 | 60 | 100 | 5 | Generic fallback. |
28
+
29
+ ## Merge Rules
30
+
31
+ 1. **Detection threshold** — only languages with >= 10% of source files (after
32
+ exclusions) contribute. If no language clears 10%, use the `Other` row.
33
+ 2. **Numeric merge** — `min()` across every matching row, per column.
34
+ 3. **Note merge** — collect each matching row's `Notes` cell, dedupe by exact
35
+ string match, render as a bullet list. Example for TypeScript + Python:
36
+ - `- TypeScript: Prefer interface over type for public APIs.`
37
+ - `- Python: PEP 8 + Google docstrings.`
38
+ 4. **Fixed defaults (not in the table, applied unconditionally)** —
39
+ `MAX_CYCLOMATIC = 10`, `MAX_NESTING_DEPTH = 4`. These can still be
40
+ overridden by the user via the Custom preset.
41
+
42
+ ## Adding a Language
43
+
44
+ When extending this table, keep the column order identical and use the
45
+ strictest reasonable value for each cell so the `min()` merge stays
46
+ meaningful for polyglot repos.