@aragon-agent/cli 0.6.17 → 0.6.19

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 (43) hide show
  1. package/CHANGELOG.md +121 -51
  2. package/README.md +48 -2
  3. package/dist/agent/controller.js +13 -0
  4. package/dist/agent/headless.js +7 -0
  5. package/dist/commands/builtins.js +18 -3
  6. package/dist/commands/index-build.d.ts +39 -0
  7. package/dist/commands/index-build.js +61 -0
  8. package/dist/config/cli-commands.d.ts +1 -1
  9. package/dist/config/cli-commands.js +3 -0
  10. package/dist/config/schema.d.ts +24 -4
  11. package/dist/config/schema.js +4 -0
  12. package/dist/exec/events.d.ts +4 -1
  13. package/dist/exec/runner.js +13 -0
  14. package/dist/input/csiu-keys.js +13 -2
  15. package/dist/input/limits.d.ts +15 -0
  16. package/dist/input/limits.js +15 -0
  17. package/dist/input/win32-input-mode.js +19 -1
  18. package/dist/logging/install.d.ts +9 -0
  19. package/dist/logging/install.js +11 -0
  20. package/dist/team/human-queue.d.ts +11 -0
  21. package/dist/team/human-queue.js +18 -1
  22. package/dist/team/limits.d.ts +40 -0
  23. package/dist/team/limits.js +41 -0
  24. package/dist/team/overseer.d.ts +157 -0
  25. package/dist/team/overseer.js +348 -0
  26. package/dist/team/report.js +26 -1
  27. package/dist/team/runtime.d.ts +71 -6
  28. package/dist/team/runtime.js +380 -23
  29. package/dist/team/subagent.d.ts +30 -1
  30. package/dist/team/subagent.js +8 -1
  31. package/dist/team/task-tool.js +4 -1
  32. package/dist/team/types.d.ts +63 -0
  33. package/dist/ui/App.d.ts +1 -1
  34. package/dist/ui/App.js +61 -0
  35. package/dist/ui/Header.js +13 -2
  36. package/dist/ui/PromptInput.js +11 -1
  37. package/dist/ui/overlays/HelpOverlay.js +1 -0
  38. package/package.json +2 -2
  39. package/skills/project-indexer/SKILL.md +163 -0
  40. package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
  41. package/skills/project-indexer/templates/cleancode-template.md +94 -0
  42. package/skills/project-indexer/templates/config-template.md +69 -0
  43. package/skills/project-indexer/templates/index-template.md +129 -0
@@ -148,6 +148,57 @@ export interface SubagentRun {
148
148
  attempt: number;
149
149
  maxRetries: number;
150
150
  };
151
+ /**
152
+ * Supervisor inspections this child received (team-overseer). Optional so
153
+ * every existing fixture compiles; absent and `0` read the same.
154
+ */
155
+ interventions?: number;
156
+ /**
157
+ * Supervisor-driven rebuilds this child went through (team-overseer). The
158
+ * run object survives a rebuild with the same label, so this is where the
159
+ * report reads "this row is the Nth body of the same brief".
160
+ */
161
+ replacements?: number;
162
+ }
163
+ /** What the supervisor may do with one inspected child. */
164
+ export type OverseerAction = 'wait' | 'nudge' | 'replace' | 'abandon';
165
+ /**
166
+ * One supervisor decision, already normalized and clamped.
167
+ *
168
+ * Produced ONLY by `normalizeOverseerDecision` (repair, never reject): an
169
+ * unparseable or out-of-budget model answer becomes `wait` with the default
170
+ * interval, because supervision must not kill a healthy child.
171
+ */
172
+ export interface OverseerDecision {
173
+ action: OverseerAction;
174
+ /** <= TEAM_LIMITS.overseerReasonChars. Why, for the report and the log. */
175
+ reason: string;
176
+ /** `nudge` only: <= TEAM_LIMITS.overseerGuidanceChars. Steered into the child. */
177
+ guidance?: string;
178
+ /** Model-chosen next check, clamped to the structural range. */
179
+ nextCheckMs?: number;
180
+ }
181
+ /** One supervisor intervention, as recorded on the outcome and rendered. */
182
+ export interface OverseerIntervention {
183
+ label: string;
184
+ at: number;
185
+ action: OverseerAction;
186
+ reason: string;
187
+ /** What the trigger was: event stall, wall-clock check, or post-mortem. */
188
+ trigger: 'silence' | 'clock' | 'postmortem';
189
+ }
190
+ /**
191
+ * A terminal supervisor decision handed to the WORKER LOOP, which is the only
192
+ * place a child's `prompt()` is awaited and therefore the only place a
193
+ * lifecycle change can be applied without orphaning the replacement (I-OV1).
194
+ *
195
+ * Set by `TeamRuntime` on the handle, then `handle.abort()`; consumed and
196
+ * left in place (never cleared) by `runOne` after the await resolves.
197
+ */
198
+ export interface OverseerVerdict {
199
+ action: 'replace' | 'abandon';
200
+ reason: string;
201
+ guidance?: string;
151
202
  }
152
203
  /** One message on the team bus. */
153
204
  export interface TeamMessage {
@@ -168,6 +219,12 @@ export interface DispatchOutcome {
168
219
  aborted: boolean;
169
220
  /** Messages addressed to `lead`; surfaced in their own report section. */
170
221
  leadMail: TeamMessage[];
222
+ /**
223
+ * Every supervisor decision that was applied (team-overseer), in order.
224
+ * Bounded by the per-child inspection budget; rendered as the report's
225
+ * Overseer section so the lead can see WHO was nudged or replaced and why.
226
+ */
227
+ interventions?: OverseerIntervention[];
171
228
  /**
172
229
  * Summed across all children. Real spend; it must reach the status bar
173
230
  * (§3.9). Every child runs the LEAD's model (main-agent parity), so one
@@ -212,6 +269,12 @@ export type TeamEvent = {
212
269
  type: 'message';
213
270
  dispatchId: string;
214
271
  message: TeamMessage;
272
+ } | {
273
+ type: 'overseer';
274
+ dispatchId: string;
275
+ label: string;
276
+ decision: OverseerDecision;
277
+ trigger: OverseerIntervention['trigger'];
215
278
  } | {
216
279
  type: 'dispatch_end';
217
280
  dispatchId: string;
package/dist/ui/App.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { ConfirmRequest } from '../tools/index.js';
11
11
  import { type Overlay, type ToastLevel } from '../agent/reducer.js';
12
12
  import type { UpdateBridge } from '../update/types.js';
13
13
  import type { MouseSource } from '../input/stdin-filter.js';
14
- import type { PasteBridge } from '../input/limits.js';
14
+ import { type PasteBridge } from '../input/limits.js';
15
15
  import type { SelectionBridge } from './selection/selection-controller.js';
16
16
  import type { HumanInputBridge } from '../tools/human-input.js';
17
17
  export interface ConfirmBridge {
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
@@ -1553,6 +1564,46 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1553
1564
  setConfirmState(null);
1554
1565
  dispatch({ type: 'setOverlay', overlay: null });
1555
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
+ };
1556
1607
  /** Answer every outstanding request with the same response and close up. */
1557
1608
  const resolveHuman = (response) => {
1558
1609
  for (const entry of [...pendingHuman.current])
@@ -1714,6 +1765,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1714
1765
  setStatusExpanded(value => !value);
1715
1766
  return;
1716
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
+ }
1717
1778
  if (tooSmall)
1718
1779
  return;
1719
1780
  // R-P1-7: the overlay branch MUST come before the transcript scroll branch
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)
@@ -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)'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
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.29",
59
+ "@aragon-agent/core": "^0.2.31",
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.
@@ -0,0 +1,94 @@
1
+ <!--
2
+ cleancode-template.md — rendered into CLAUDE.md by project-indexer (Step 5).
3
+ Authoring rules:
4
+ - Chinese character body must measure 1,400–1,700 characters
5
+ (matched by regex `[一-鿿]`).
6
+ - Verify after editing by counting matches of `[一-鿿]` in this file.
7
+ - Placeholders: {{MAX_FILE_LINES}} {{MAX_METHOD_LINES}}
8
+ {{MAX_FUNCTION_PARAMS}} {{MAX_CYCLOMATIC}} {{MAX_LINE_LENGTH}}
9
+ {{MAX_NESTING_DEPTH}} {{PRIMARY_LANGUAGES}} {{LANGUAGE_HINTS}}
10
+ - At runtime the skill must ensure no `{{...}}` placeholder leaks into
11
+ the rendered output. Numeric character counting at runtime is NOT
12
+ required.
13
+ -->
14
+
15
+ > 本节由 `project-indexer` 自动维护,灵感取自谷歌风格,并按本项目主要语言收敛阈值。请勿手工编辑 markers 之间的内容,需要刷新时请运行「regenerate index」。
16
+
17
+ ### 一、概述
18
+
19
+ 本节是本项目在使用 AI 辅助开发时必须遵守的代码规范。在写或改动任何代码之前,先把下列条目读完一遍,再动键盘。规则的目的不是束缚发挥,而是让人类与 agent 写出的代码在阈值上保持可比的下限,避免出现千行文件与几百行方法,避免命名混乱、错误吞咽、测试缺失这些会在后续维护中迅速发酵的问题。所有阈值都允许根据项目实际情况调整,但调整必须在 `.claude-index/config.md` 中显式记录,并在评审中说明理由,不要靠默认值偷偷放宽。
20
+
21
+ ### 二、文件与函数尺寸
22
+
23
+ - 单文件不超过 **{{MAX_FILE_LINES}}** 行,超过即按职责拆分到新模块。
24
+ - 单方法不超过 **{{MAX_METHOD_LINES}}** 行,写到一半感觉膨胀就先抽函数。
25
+ - 函数形参不超过 **{{MAX_FUNCTION_PARAMS}}** 个,多了请改用配置对象、命名参数或聚合结构体,禁止靠位置堆参数。
26
+ - 圈复杂度不超过 **{{MAX_CYCLOMATIC}}**,分支累积过多必须拆函数或引入查找表。
27
+ - 单行长度不超过 **{{MAX_LINE_LENGTH}}** 列,超长就换行或抽变量名。判定标准很简单:阈值超了就是技术债,提交前自查并修正。
28
+
29
+ ### 三、命名
30
+
31
+ - 函数以动词开头表达动作,例如 `loadConfig`、`parseToken`;查询类用名词或 `get` / `find`。
32
+ - 返回布尔值的函数与变量以 `is`、`has`、`should`、`can` 起头,便于阅读条件表达式。
33
+ - 常量使用大写蛇形,模块名短小且语义化,避免堆叠首字母缩写。
34
+ - 不缩写领域词,宁可名字长五个字母也要让读者一眼看懂含义。
35
+ - 类与接口使用大驼峰命名,方法与变量使用小驼峰或语言推荐的蛇形写法,保持整个仓库风格一致。
36
+ - 避免使用 `data`、`info`、`temp`、`obj` 这类无信息量的名字;给出具体的领域名词。
37
+
38
+ ### 四、注释与文档
39
+
40
+ - 注释只解释「为什么」,不复述「做什么」,代码本身就是「做什么」的真相。
41
+ - 所有公开 API 必须写 docstring,标注参数、返回值与可能抛出的异常。
42
+ - 删除一切僵尸注释与被注释掉的旧代码;保留 `TODO` 必须带责任人与日期。
43
+ - 行级注释稀缺即珍贵,不要在每行上方堆一句无信息量的话。
44
+ - 复杂业务规则、踩坑记录、临时性 workaround 必须在注释中写清楚原因与可回退条件。
45
+
46
+ ### 五、错误处理
47
+
48
+ - 错误显式传播,不要默默吞异常或返回 `null` 假装一切正常。
49
+ - 外部输入在边界处一次性校验,进入内部后信任内部数据,禁止层层重复校验。
50
+ - 不要为「理论上不会发生」的分支写防御性 `try/except`,那只是噪音。
51
+ - 错误信息必须包含定位线索:发生在哪、关键参数是什么、下游应如何处理。
52
+ - 在 IO、网络、子进程等不可控边界,使用带超时与重试的结构化封装,而非裸调用。
53
+ - 捕获异常时尽量缩小作用域,只处理你确实有办法应对的错误类型,其他原样向上抛出。
54
+ - 对外暴露的错误码或异常类必须稳定,更名等同于破坏性变更,需要在变更日志中显式标注。
55
+
56
+ ### 六、复杂度与嵌套
57
+
58
+ - 嵌套深度不超过 **{{MAX_NESTING_DEPTH}}** 层,深嵌套是函数过长的前兆。
59
+ - 优先使用早返回(guard clause)取代 `else` 阶梯。
60
+ - 单个表达式里出现超过三个布尔条件,立刻抽成命名清晰的辅助函数。
61
+ - 循环里若出现复杂条件分支,改写为查找表或策略对象往往更易读。
62
+ - 一个函数只做一件事;当解释函数职责时需要用「并」「同时」「再」这类连词,就是应该拆分的信号。
63
+
64
+ ### 七、测试
65
+
66
+ - 修 bug 必须先写一个能稳定复现该 bug 的测试用例,再动业务代码。
67
+ - 公开行为都要有单元测试覆盖正常路径与至少一条异常路径。
68
+ - 测试名描述被验证的行为而非实现细节,例如 `rejects_expired_token`。
69
+ - 不允许 mock 数据库迁移、鉴权签名等高风险路径,必须使用真实集成测试。
70
+ - 单测应快速且可重复运行;外部依赖通过桩或本地容器隔离,禁止依赖共享环境状态。
71
+ - 任何被发现的回归 bug 都要补上对应的测试,把过去的坑变成未来的护栏。
72
+
73
+ ### 八、代码评审清单
74
+
75
+ 提交前对照以下 8 条自查,任何一条不满足即视为未完成:
76
+
77
+ 1. 命名是否自解释,是否避免无意义缩写。
78
+ 2. 文件、方法、参数、嵌套、圈复杂度是否都在阈值内。
79
+ 3. 错误是否显式传播,边界校验是否到位。
80
+ 4. 注释是否只解释「为什么」,是否还有僵尸代码或孤儿 TODO。
81
+ 5. 测试是否覆盖正常路径与异常路径,是否能稳定复现已修复的 bug。
82
+ 6. 是否引入新依赖,依赖是否必要、是否有安全或许可证风险。
83
+ 7. 是否泄露密钥、个人数据、内部链接等敏感信息。
84
+ 8. 是否与现有架构与命名风格一致,是否制造新的孤岛。
85
+
86
+ ### 九、语言特有提示
87
+
88
+ 当前项目主要语言:**{{PRIMARY_LANGUAGES}}**。
89
+
90
+ {{LANGUAGE_HINTS}}
91
+
92
+ ### 十、元信息
93
+
94
+ 模板版本与上方 markers 中的版本一致。说「regenerate index」或「更新索引」可重新生成本节内容;说「disable cleancode」可移除本节。
@@ -0,0 +1,69 @@
1
+ # Index Configuration
2
+
3
+ > Configuration for project-indexer. Edit this file to customize index generation.
4
+
5
+ ## Excluded Directories
6
+
7
+ Directories to skip during indexing:
8
+
9
+ - node_modules
10
+ - .git
11
+ - dist
12
+ - build
13
+ - __pycache__
14
+ - .venv
15
+ - venv
16
+ - vendor
17
+ - coverage
18
+ - .next
19
+ - .nuxt
20
+ - .cache
21
+ - target
22
+ - bin
23
+ - obj
24
+ - .idea
25
+ - .vscode
26
+
27
+ ## Priority Directories
28
+
29
+ Directories to index in detail (leave empty for auto-detection):
30
+
31
+ - src/
32
+ - lib/
33
+ - app/
34
+ - core/
35
+
36
+ ## Tech Stack
37
+
38
+ Detected or specified technology stack:
39
+
40
+ - **Frontend:** {React | Vue | Angular | Svelte | Next.js | Nuxt | etc.}
41
+ - **Backend:** {Express | FastAPI | Django | Rails | Spring | etc.}
42
+ - **Language:** {TypeScript | JavaScript | Python | Go | etc.}
43
+
44
+ ## Index Settings
45
+
46
+ - **Generated:** {YYYY-MM-DD HH:MM:SS}
47
+ - **Project Root:** {/path/to/project}
48
+ - **Index Version:** 1.0
49
+ - **Total Files Indexed:** {N}
50
+ - **Total Symbols Extracted:** {N}
51
+
52
+ ## Clean Code Settings
53
+
54
+ - Enabled: {Yes | No}
55
+ - Preset: {strict | relaxed | custom | disabled}
56
+ - MAX_FILE_LINES: 1000
57
+ - MAX_METHOD_LINES: 60
58
+ - MAX_FUNCTION_PARAMS: 5
59
+ - MAX_CYCLOMATIC: 10
60
+ - MAX_LINE_LENGTH: 100
61
+ - MAX_NESTING_DEPTH: 4
62
+
63
+ ## Regeneration Notes
64
+
65
+ Notes for future regeneration:
66
+
67
+ - {Any special considerations}
68
+ - {Files that need manual review}
69
+ - {Known limitations}