@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.
- package/CHANGELOG.md +107 -51
- package/README.md +91 -48
- package/dist/agent/controller.js +52 -47
- package/dist/agent/headless.js +7 -0
- package/dist/agent/reducer.js +0 -4
- package/dist/cli.js +0 -13
- package/dist/commands/builtins.js +21 -20
- package/dist/commands/index-build.d.ts +39 -0
- package/dist/commands/index-build.js +61 -0
- package/dist/compaction/child.d.ts +10 -8
- package/dist/compaction/child.js +23 -25
- package/dist/compaction/limits.d.ts +0 -2
- package/dist/compaction/limits.js +0 -2
- package/dist/config/cli-commands.d.ts +2 -2
- package/dist/config/cli-commands.js +19 -15
- package/dist/config/load.d.ts +0 -17
- package/dist/config/load.js +0 -6
- package/dist/config/schema.d.ts +41 -9
- package/dist/config/schema.js +18 -11
- package/dist/exec/events.d.ts +16 -9
- package/dist/exec/events.js +2 -2
- package/dist/exec/runner.js +17 -1
- package/dist/fast/limits.d.ts +2 -2
- package/dist/fast/limits.js +2 -2
- package/dist/fast/prompt.d.ts +10 -11
- package/dist/fast/prompt.js +19 -20
- package/dist/fast/reviewer.d.ts +1 -1
- package/dist/fast/reviewer.js +1 -1
- package/dist/fast/types.d.ts +0 -2
- package/dist/fast/wiring.d.ts +18 -34
- package/dist/fast/wiring.js +20 -51
- package/dist/input/csiu-keys.js +13 -2
- package/dist/input/limits.d.ts +15 -0
- package/dist/input/limits.js +15 -0
- package/dist/input/win32-input-mode.js +7 -1
- package/dist/logging/install.d.ts +9 -0
- package/dist/logging/install.js +11 -0
- package/dist/session/validate-session.js +3 -1
- package/dist/team/child-reviewer.d.ts +105 -0
- package/dist/team/child-reviewer.js +117 -0
- package/dist/team/human-queue.d.ts +11 -0
- package/dist/team/human-queue.js +18 -1
- package/dist/team/limits.d.ts +40 -0
- package/dist/team/limits.js +41 -0
- package/dist/team/normalize.d.ts +7 -26
- package/dist/team/normalize.js +9 -22
- package/dist/team/overseer.d.ts +157 -0
- package/dist/team/overseer.js +348 -0
- package/dist/team/prompt.d.ts +0 -11
- package/dist/team/prompt.js +3 -12
- package/dist/team/report.d.ts +4 -19
- package/dist/team/report.js +27 -28
- package/dist/team/runtime.d.ts +76 -16
- package/dist/team/runtime.js +410 -57
- package/dist/team/subagent.d.ts +48 -19
- package/dist/team/subagent.js +88 -31
- package/dist/team/task-tool.d.ts +6 -26
- package/dist/team/task-tool.js +17 -48
- package/dist/team/types.d.ts +65 -41
- package/dist/ui/App.d.ts +1 -1
- package/dist/ui/App.js +64 -4
- package/dist/ui/BottomStatusRow.js +2 -1
- package/dist/ui/Header.js +13 -2
- package/dist/ui/PromptInput.js +11 -1
- package/dist/ui/StatusBar.js +2 -1
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/entries/TeamCard.js +1 -6
- package/dist/ui/layout/status-detail-layout.js +12 -2
- package/dist/ui/layout/status-layout.d.ts +4 -2
- package/dist/ui/layout/status-layout.js +24 -1
- package/dist/ui/overlays/HelpOverlay.js +2 -2
- package/package.json +2 -2
- package/skills/project-indexer/SKILL.md +163 -0
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
- package/skills/project-indexer/templates/cleancode-template.md +94 -0
- package/skills/project-indexer/templates/config-template.md +69 -0
- package/skills/project-indexer/templates/index-template.md +129 -0
- package/dist/ui/ModeChip.d.ts +0 -18
- 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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -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)
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -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))] });
|
package/dist/ui/TeamPanel.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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.
|