cli-five 0.2.16 → 0.2.17

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/README.md CHANGED
@@ -156,13 +156,51 @@ Optional integrations live outside the default scaffold and are installed with `
156
156
 
157
157
  ```bash
158
158
  npx cli-five add # list known targets
159
- npx cli-five add codegraph # stub in this release
159
+ npx cli-five add codegraph # CodeGraph MCP registration + AGENTS.md instructions
160
+ npx cli-five add jev # tier-routing tool for the Planner (OpenCode only)
160
161
  npx cli-five list-addons # installed vs. available status
161
162
  ```
162
163
 
163
- This release ships the **dispatcher and merge utility** only — no integration is wired in yet. Jev and the CodeGraph migration are future commits.
164
+ Add-ons use `mergeBlock(file, markerFence, content)` to layer a fenced block into an existing JSON or Markdown file without touching the rest of it (distinct from init's blunt overwrite gate). Markdown gets `<!-- NAME_START -->` / `<!-- NAME_END -->` fences; JSON is deep-merged with existing keys preserved. Re-running an add-on is idempotent — no duplicate MCP entries or AGENTS.md sections.
165
+
166
+ ### CodeGraph (`add codegraph`)
167
+
168
+ `add codegraph` registers the CodeGraph MCP server and adds the CodeGraph section to `AGENTS.md`. It works on **both platforms**:
169
+
170
+ | Target | MCP registration | Instructions |
171
+ |---|---|---|
172
+ | Copilot | `.vscode/mcp.json` → `servers.codegraph` | `AGENTS.md` block |
173
+ | OpenCode | `opencode.json` → `mcp.codegraph` | `AGENTS.md` block |
174
+
175
+ **The `init` flags are now thin wrappers.** `init --codegraph` and `init --no-codegraph` still behave exactly as before, but they call the *same* underlying `add codegraph` logic — one implementation, two entry points. CodeGraph remains off by default in minimal `init` and on with `--full-interview`; pass `--codegraph` to force it on, `--no-codegraph` to force it off.
176
+
177
+ cli-five still does **not** run `codegraph init` itself — it only registers the server and reminds you to index the project:
178
+
179
+ ```bash
180
+ npm i -g @colbymchenry/codegraph
181
+ codegraph init
182
+ ```
183
+
184
+ ### Jev (`add jev`) — tier-routing only, OpenCode only
185
+
186
+ `add jev` scaffolds an OpenCode plugin that adds a `local_tier_heuristic` tool. The Planner calls it once per task to classify the work as `trivial` / `minor` / `major`, then scales planning depth accordingly.
187
+
188
+ **Two things it deliberately does *not* do, stated plainly:**
189
+
190
+ 1. **It does not call Jev.** `jev-harness` 0.2.0's `route` subcommand exposes no custom-criteria interface — it emits its own fixed tier vocabulary (`deterministic` / `lightweight_system2` / `heavy_system2`) and returns a constant confidence (`0.88`) under its offline/mock engine, so it cannot be thresholded on. The shipped tool is therefore a **local heuristic**, truthfully named `local_tier_heuristic`. The swap point for real Jev wiring is marked in `templates/opencode/plugin/jev-tier-router/index.js` (`JEVR_SWAP_POINT`).
191
+ 2. **The test-gate is parked.** Gating Reviewer spawns via plugin interception (`tool.execute.before` / `permission.ask`) does not work: OpenCode plugin hooks do not fire under OpenChamber's embedded-server routing. Do not expect `add jev` to gate anything.
192
+
193
+ `list-addons` reports each add-on's honest capability rather than a bare "installed":
194
+
195
+ ```
196
+ codegraph installed (MCP registration + AGENTS.md instructions) available MCP registration + AGENTS.md instructions
197
+ jev installed (tier-routing only) available local heuristic — …; test-gate parked — <issue link>
198
+ ```
199
+
200
+ **Fail-open is non-negotiable.** If the classifier is unavailable, errors, or returns malformed output, the tool returns `available: false` with tier `major` (the expensive tier) and never throws. The Planner falls back to its own judgment. cli-five and the scaffolded agents behave identically whether the plugin works, is missing, or is broken.
201
+
202
+ **Copilot has no equivalent.** `add jev` refuses cleanly on a Copilot target (exit 1, no files written) — there is no `tools.add`-style surface there.
164
203
 
165
- Add-ons use `mergeBlock(file, markerFence, content)` to layer a fenced block into an existing JSON or Markdown file without touching the rest of it (distinct from init's blunt overwrite gate). Markdown gets `<!-- NAME_START -->` / `<!-- NAME_END -->` fences; JSON is deep-merged with existing keys preserved.
166
204
 
167
205
  ## Model providers
168
206
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-five",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "Code Like I'm Five — scaffold a 5-agent AI team (GitHub Copilot or OpenCode) into any repo.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,47 @@
1
+ import kleur from 'kleur';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { log } from '../util/log.mjs';
5
+ import { PLATFORM_COPILOT, PLATFORM_OPENCODE, platformLabel } from '../util/platforms.mjs';
6
+ import { addCodegraphTo, CODEGRAPH_INIT_REMINDER, mcpTargetFor } from './codegraph.mjs';
7
+
8
+ /**
9
+ * `add codegraph` — register the CodeGraph MCP server + AGENTS.md section.
10
+ *
11
+ * Works on both platforms. Same content and behavior as the previous inline
12
+ * init implementation; only the invocation path and write mechanism changed
13
+ * (mergeBlock instead of blunt overwrite).
14
+ */
15
+ export async function runCodegraph({ cwd, args = {} }) {
16
+ const platform = detectPlatform(cwd);
17
+
18
+ if (platform === 'unknown') {
19
+ log.err('No cli-five scaffold detected (neither .opencode/agents nor .github/agents).');
20
+ log.dim('Run `npx cli-five init` first, then `npx cli-five add codegraph`.');
21
+ process.exitCode = 1;
22
+ return [];
23
+ }
24
+
25
+ const dryRun = Boolean(args.dryRun);
26
+ const touched = addCodegraphTo({ cwd, platform, dryRun, track: true });
27
+
28
+ for (const t of touched) {
29
+ const rel = t.path.replace(cwd + '/', '');
30
+ const symbol = t.action === 'created' || t.action === 'updated' ? '+' : '~';
31
+ log.raw(` ${symbol} ${rel} ${kleur.dim(`(${t.action})`)}`);
32
+ }
33
+
34
+ log.ok(`CodeGraph registered for ${platformLabel(platform)}`);
35
+ log.dim(`MCP: ${mcpTargetFor(platform)} · instructions: AGENTS.md`);
36
+ log.dim(CODEGRAPH_INIT_REMINDER.replace('CodeGraph is configured. ', ''));
37
+
38
+ return touched;
39
+ }
40
+
41
+ function detectPlatform(cwd) {
42
+ if (existsSync(join(cwd, '.opencode', 'agents'))) return PLATFORM_OPENCODE;
43
+ if (existsSync(join(cwd, '.github', 'agents', 'orchestrator.agent.md'))) return PLATFORM_COPILOT;
44
+ return 'unknown';
45
+ }
46
+
47
+ export const __testables = { detectPlatform };
@@ -0,0 +1,124 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { mergeBlock } from '../util/merge.mjs';
4
+ import { PLATFORM_COPILOT, PLATFORM_OPENCODE } from '../util/platforms.mjs';
5
+
6
+ export const CODEGRAPH_STATUS = 'MCP registration + AGENTS.md instructions';
7
+ export const CODEGRAPH_BLOCK_NAME = 'codegraph';
8
+ export const CODEGRAPH_INIT_REMINDER =
9
+ 'CodeGraph is configured. Remember to run `codegraph init` before asking agents to explore the codebase.';
10
+
11
+ // The AGENTS.md section is byte-identical to the pre-migration inline constant
12
+ // in scaffold.mjs. Do not change its content — only the write path changed.
13
+ export const CODEGRAPH_BLOCK = `## CodeGraph
14
+
15
+ This project is configured to use [CodeGraph](https://codegraph.ru) for graph-backed codebase context.
16
+ When you need to understand relationships, call paths, or impacts, use:
17
+
18
+ \`\`\`
19
+ codegraph explore "<your question>"
20
+ \`\`\`
21
+
22
+ The CodeGraph MCP server is registered in the project config. Run \`codegraph init\` in this directory
23
+ if the project has not been indexed yet.`;
24
+
25
+ /**
26
+ * The CodeGraph MCP server entries, per platform. Unchanged from the previous
27
+ * inline implementation — same server, same command, same shape.
28
+ */
29
+ export function codegraphOpencodeConfig() {
30
+ return {
31
+ codegraph: {
32
+ type: 'local',
33
+ command: ['codegraph', 'serve', '--mcp'],
34
+ enabled: true,
35
+ },
36
+ };
37
+ }
38
+
39
+ export function codegraphCopilotMcpJson() {
40
+ return {
41
+ inputs: [],
42
+ servers: {
43
+ codegraph: {
44
+ command: 'codegraph',
45
+ args: ['serve', '--mcp'],
46
+ },
47
+ },
48
+ };
49
+ }
50
+
51
+ /** Which MCP file a given platform owns. Preserves the old per-platform split. */
52
+ export function mcpTargetFor(platform) {
53
+ return platform === PLATFORM_OPENCODE ? 'opencode.json' : join('.vscode', 'mcp.json');
54
+ }
55
+
56
+ /**
57
+ * Register CodeGraph in a workspace, using mergeBlock() for every write.
58
+ *
59
+ * Shared by `add codegraph` and init's legacy --codegraph flag — one
60
+ * implementation, two entry points.
61
+ *
62
+ * @returns {Array<{path:string, action:string}>} the files touched.
63
+ */
64
+ export function addCodegraphTo({ cwd, platform, dryRun = false, track = false }) {
65
+ const touched = [];
66
+ const isOpenCode = platform === PLATFORM_OPENCODE;
67
+
68
+ // 1. MCP registration.
69
+ if (isOpenCode) {
70
+ const opencodePath = join(cwd, 'opencode.json');
71
+ // Preserve a pre-existing `plugins[]` (e.g. jev): deepMerge replaces arrays,
72
+ // so carry the existing value through untouched at the root.
73
+ const existing = readJson(opencodePath) || {};
74
+ const patch = { mcp: codegraphOpencodeConfig() };
75
+ if (Array.isArray(existing.plugins)) patch.plugins = existing.plugins;
76
+ touched.push(mergeBlock(opencodePath, 'codegraph', patch, { dryRun, track }));
77
+ } else {
78
+ const mcpPath = join(cwd, '.vscode', 'mcp.json');
79
+ const existing = readJson(mcpPath) || {};
80
+ // Preserve the old key order (`inputs` first) so output matches the
81
+ // pre-migration inline implementation byte-for-byte. `inputs` is an array;
82
+ // carry it through so deepMerge's array-replace is a no-op.
83
+ const patch = { inputs: Array.isArray(existing.inputs) ? existing.inputs : [] };
84
+ patch.servers = { codegraph: codegraphCopilotMcpJson().servers.codegraph };
85
+ touched.push(mergeBlock(mcpPath, 'codegraph', patch, { dryRun, track }));
86
+ }
87
+
88
+ // 2. AGENTS.md section (fenced block; same marker name as the old template).
89
+ const agentsPath = join(cwd, 'AGENTS.md');
90
+ touched.push(mergeBlock(agentsPath, CODEGRAPH_BLOCK_NAME, CODEGRAPH_BLOCK, { dryRun }));
91
+
92
+ return touched;
93
+ }
94
+
95
+ /**
96
+ * Idempotent remove-of-duplicates is intentionally NOT implemented: mergeBlock
97
+ * already replaces an existing fenced block in place and deep-merges JSON, so
98
+ * re-running add codegraph does not duplicate anything.
99
+ */
100
+ export function isCodegraphPresent(cwd, platform) {
101
+ const mcpPath = join(cwd, mcpTargetFor(platform));
102
+ const cfg = readJson(mcpPath);
103
+ if (platform === PLATFORM_OPENCODE) return Boolean(cfg?.mcp?.codegraph);
104
+ return Boolean(cfg?.servers?.codegraph);
105
+ }
106
+
107
+ function readJson(filePath) {
108
+ if (!existsSync(filePath)) return null;
109
+ try {
110
+ return JSON.parse(readFileSync(filePath, 'utf8'));
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /** Ensure a file exists with given content (used for a created-not-merged file). */
117
+ export function ensureFile(filePath, contents) {
118
+ if (existsSync(filePath)) return false;
119
+ mkdirSync(dirname(filePath), { recursive: true });
120
+ writeFileSync(filePath, contents);
121
+ return true;
122
+ }
123
+
124
+ export const __testables = { readJson, mcpTargetFor };
@@ -0,0 +1,98 @@
1
+ import kleur from 'kleur';
2
+ import { existsSync, readFileSync, cpSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { log } from '../util/log.mjs';
5
+ import { mergeBlock } from '../util/merge.mjs';
6
+ import { templatePath } from '../util/fs.mjs';
7
+
8
+ export const JEVR_STATUS =
9
+ 'local heuristic — jev-harness lacks a custom-criteria interface as of 2026-09-26';
10
+ export const JEVR_TOOL = 'local_tier_heuristic';
11
+ export const TEST_GATE_ISSUE = 'https://github.com/idusortus/cli-five/issues';
12
+
13
+ const PLUGIN_REL = join('.opencode', 'plugin', 'jev-tier-router');
14
+
15
+ /**
16
+ * `add jev` — tier-routing only.
17
+ *
18
+ * Ships a local tier-classification tool for the Planner. Does NOT wire the
19
+ * test-gate (parked: OpenCode plugin hooks don't fire under OpenChamber's
20
+ * embedded-server routing) and does NOT call Jev (no custom-criteria
21
+ * interface in jev-harness as of this release).
22
+ */
23
+ export async function runJev({ cwd, args }) {
24
+ const platform = detectPlatform(cwd);
25
+
26
+ if (platform !== 'opencode') {
27
+ log.err('jev is OpenCode-only. No tools.add-equivalent surface exists for Copilot.');
28
+ log.dim('Re-run init with --target opencode, then `npx cli-five add jev`.');
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+
33
+ const written = [];
34
+
35
+ // 1. Plugin directory (package.json + index.js), confirmed convention.
36
+ const pluginDir = join(cwd, PLUGIN_REL);
37
+ mkdirSync(pluginDir, { recursive: true });
38
+ for (const file of ['package.json', 'index.js']) {
39
+ const src = templatePath('opencode', 'plugin', 'jev-tier-router', file);
40
+ const dest = join(pluginDir, file);
41
+ cpSync(src, dest);
42
+ written.push({ path: dest, written: true });
43
+ }
44
+
45
+ // 2. Register the plugin path in opencode.json's `plugins` array.
46
+ // mergeBlock deep-merges JSON but replaces arrays, so read the existing
47
+ // array, union it, and hand the unioned value to mergeBlock (single
48
+ // write path — no second merge approach).
49
+ const opencodePath = join(cwd, 'opencode.json');
50
+ const plugins = readPlugins(opencodePath);
51
+ const pluginRef = toPosix(PLUGIN_REL);
52
+ if (!plugins.includes(pluginRef)) plugins.push(pluginRef);
53
+ mergeBlock(opencodePath, 'jev', { plugins }, { track: true, metaKey: '$cliFive' });
54
+
55
+ // 3. Planner instruction in AGENTS.md (fenced block, idempotent).
56
+ const agentsPath = join(cwd, 'AGENTS.md');
57
+ mergeBlock(agentsPath, 'jev-tier-routing', plannerInstruction());
58
+
59
+ log.ok(`Plugin written to ${kleur.bold(toPosix(PLUGIN_REL))}`);
60
+ log.ok(`Registered in opencode.json (plugins[])`);
61
+ log.ok('Planner instruction added to AGENTS.md');
62
+ log.raw('');
63
+ log.info(`Tool: ${kleur.bold(JEVR_TOOL)} (tier-routing only)`);
64
+ log.warn('Status: ' + JEVR_STATUS);
65
+ log.dim(`Test-gate is parked — see ${TEST_GATE_ISSUE}`);
66
+ }
67
+
68
+ function plannerInstruction() {
69
+ return `## Tier routing (local heuristic)
70
+
71
+ Before planning, call the \`${JEVR_TOOL}\` tool once with the task description.
72
+
73
+ - If it returns \`confidence\` >= 0.6, use its \`tier\` (trivial | minor | major) as your planning depth.
74
+ - If \`confidence\` < 0.6, or the tool is unavailable, use your own judgment and default to \`major\`.
75
+ - This is a local heuristic, not a Jev call. Never block or fail a turn because the tool is unavailable.`;
76
+ }
77
+
78
+ function readPlugins(opencodePath) {
79
+ if (!existsSync(opencodePath)) return [];
80
+ try {
81
+ const cfg = JSON.parse(readFileSync(opencodePath, 'utf8'));
82
+ return Array.isArray(cfg.plugins) ? [...cfg.plugins] : [];
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
88
+ function detectPlatform(cwd) {
89
+ if (existsSync(join(cwd, '.opencode', 'agents'))) return 'opencode';
90
+ if (existsSync(join(cwd, '.github', 'agents', 'orchestrator.agent.md'))) return 'copilot';
91
+ return 'unknown';
92
+ }
93
+
94
+ function toPosix(p) {
95
+ return p.split('\\').join('/');
96
+ }
97
+
98
+ export const __testables = { plannerInstruction, readPlugins, detectPlatform };
@@ -1,5 +1,8 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { runJev, JEVR_STATUS, TEST_GATE_ISSUE } from './jev.mjs';
4
+ import { runCodegraph } from './codegraph-command.mjs';
5
+ import { CODEGRAPH_STATUS } from './codegraph.mjs';
3
6
 
4
7
  /**
5
8
  * Add-on registry for `cli-five add <name>`.
@@ -16,19 +19,27 @@ export const ADDONS = {
16
19
  name: 'codegraph',
17
20
  label: 'CodeGraph',
18
21
  description: 'Graph-backed codebase context MCP server and agent instructions.',
19
- available: false,
20
- note: 'Currently installed by init (on by default). `add` migration is a future commit.',
22
+ // Matches jev's status-object pattern: explicit capability + status string,
23
+ // not a bare boolean.
24
+ available: true,
25
+ capability: 'MCP registration + AGENTS.md instructions',
26
+ status: CODEGRAPH_STATUS,
27
+ platforms: ['copilot', 'opencode'],
21
28
  detect: detectCodeGraph,
22
- run: null,
29
+ run: runCodegraph,
23
30
  },
24
31
  jev: {
25
32
  name: 'jev',
26
33
  label: 'Jev',
27
- description: 'Optional Jev integration (purpose to be defined in a future commit).',
28
- available: false,
29
- note: 'Reserved target. No integration wired in this release.',
30
- detect: () => [],
31
- run: null,
34
+ description: 'Tier-routing tool for the Planner (local heuristic; test-gate parked).',
35
+ // Not a bare boolean: jev ships tier-routing ONLY. The test-gate half is
36
+ // parked because OpenCode plugin hooks don't fire under OpenChamber routing.
37
+ available: true,
38
+ capability: 'tier-routing only',
39
+ status: `${JEVR_STATUS}; test-gate parked — ${TEST_GATE_ISSUE}`,
40
+ platforms: ['opencode'],
41
+ detect: detectJev,
42
+ run: runJev,
32
43
  },
33
44
  };
34
45
 
@@ -94,3 +105,22 @@ function readJson(filePath) {
94
105
  return null;
95
106
  }
96
107
  }
108
+
109
+ /**
110
+ * Detect the jev tier-router plugin: its directory on disk and/or its entry in
111
+ * opencode.json's `plugins` array. Read-only.
112
+ */
113
+ export function detectJev(cwd) {
114
+ const signals = [];
115
+
116
+ if (existsSync(join(cwd, '.opencode', 'plugin', 'jev-tier-router', 'index.js'))) {
117
+ signals.push('.opencode/plugin/jev-tier-router');
118
+ }
119
+
120
+ const cfg = readJson(join(cwd, 'opencode.json'));
121
+ if (Array.isArray(cfg?.plugins) && cfg.plugins.some((p) => String(p).includes('jev-tier-router'))) {
122
+ signals.push('opencode.json plugins[]');
123
+ }
124
+
125
+ return signals;
126
+ }
@@ -28,7 +28,7 @@ export async function add(args) {
28
28
  if (typeof addon.run !== 'function') {
29
29
  log.warn(`${addon.label} is registered but not implemented yet.`);
30
30
  log.info('The add dispatcher works — this target is reserved for a future release.');
31
- if (addon.note) log.dim(addon.note);
31
+ if (addon.note || addon.status) log.dim(addon.note || addon.status);
32
32
  log.dim('Nothing was written to your project.');
33
33
  return;
34
34
  }
@@ -5,24 +5,33 @@ import { detectAddon, listAddons } from '../addons/registry.mjs';
5
5
  /**
6
6
  * `cli-five list-addons` — show what is installed vs. available.
7
7
  *
8
- * "Installed" is detected read-only from workspace artifacts. CodeGraph is
9
- * listed honestly even though its `add` plumbing has not moved yet.
8
+ * "Installed" is detected read-only from workspace artifacts. Status text is
9
+ * deliberately honest: an add-on that ships partial capability (e.g. jev's
10
+ * tier-routing without the parked test-gate) must not read as fully installed.
10
11
  */
11
12
  export function listAddonsCommand(args) {
12
13
  const cwd = args.cwd;
13
14
  log.raw(kleur.bold().magenta('\ncli-five list-addons') + kleur.gray(` ${cwd}`));
14
15
  log.raw('');
15
16
 
16
- log.raw(` ${kleur.gray(pad('ADD-ON', 12))} ${kleur.gray(pad('STATUS', 12))} ${kleur.gray(pad('ADD', 10))} ${kleur.gray('DETAIL')}`);
17
- log.raw(` ${'─'.repeat(12)} ${'─'.repeat(12)} ${'─'.repeat(10)} ${'─'.repeat(30)}`);
17
+ log.raw(` ${kleur.gray(pad('ADD-ON', 12))} ${kleur.gray(pad('STATUS', 14))} ${kleur.gray(pad('ADD', 10))} ${kleur.gray('DETAIL')}`);
18
+ log.raw(` ${'─'.repeat(12)} ${'─'.repeat(14)} ${'─'.repeat(10)} ${'─'.repeat(30)}`);
18
19
 
19
20
  for (const addon of listAddons()) {
20
21
  const signals = detectAddon(addon, cwd);
21
22
  const installed = signals.length > 0;
22
23
  const addable = typeof addon.run === 'function';
23
- const detail = installed ? signals.join(', ') : addon.note || '';
24
24
 
25
- const statusText = pad(installed ? 'installed' : 'not found', 12);
25
+ // An installed add-on with a `capability` note is partial — say so.
26
+ const statusLabel = installed
27
+ ? (addon.capability ? `installed (${addon.capability})` : 'installed')
28
+ : 'not found';
29
+
30
+ const detail = installed
31
+ ? [addon.status || signals.join(', ')].filter(Boolean).join(' — ')
32
+ : addon.note || addon.status || '';
33
+
34
+ const statusText = pad(statusLabel, 14);
26
35
  const status = installed ? kleur.green(statusText) : kleur.gray(statusText);
27
36
  const addableText = pad(addable ? 'available' : 'planned', 10);
28
37
  const addableColored = addable ? kleur.green(addableText) : kleur.yellow(addableText);
@@ -31,7 +40,7 @@ export function listAddonsCommand(args) {
31
40
  }
32
41
 
33
42
  log.raw('');
34
- log.dim('Install with `npx cli-five add <name>` once a target is available.');
43
+ log.dim('Install with `npx cli-five add <name>`.');
35
44
  log.raw('');
36
45
  }
37
46
 
@@ -3,6 +3,7 @@ import { log } from '../util/log.mjs';
3
3
  import { readTemplate, render, writeFile, listFilesRecursive, relTo, templatePath } from '../util/fs.mjs';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { PLATFORM_COPILOT, PLATFORM_OPENCODE, agentDirFor, agentFileFor } from '../util/platforms.mjs';
6
+ import { addCodegraphTo } from '../addons/codegraph.mjs';
6
7
 
7
8
  const AGENT_NAMES = ['orchestrator', 'planner', 'coder', 'designer', 'reviewer'];
8
9
  const HISTORY_FILES = ['orchestrator.md', 'planner.md', 'coder.md', 'designer.md', 'reviewer.md'];
@@ -44,6 +45,9 @@ export function scaffold({ cwd, answers, args }) {
44
45
  }
45
46
 
46
47
  // Shared memory primitives
48
+ // NOTE: AGENTS.md is written WITHOUT the CodeGraph block here. When CodeGraph
49
+ // is enabled, the block is merged in afterward by addCodegraphTo() — the same
50
+ // code path `add codegraph` uses (one implementation, two entry points).
47
51
  for (const tmpl of [
48
52
  'AGENTS.md.tmpl',
49
53
  'PROJECT.md.tmpl',
@@ -61,6 +65,13 @@ export function scaffold({ cwd, answers, args }) {
61
65
  written.push(writeFile(join(cwd, 'histories', file), readTemplate('histories', file), args));
62
66
  }
63
67
 
68
+ // CodeGraph — delegated to the shared add codegraph implementation.
69
+ if (answers.codegraph) {
70
+ for (const t of addCodegraphTo({ cwd, platform, dryRun: args.dryRun })) {
71
+ written.push({ path: t.path, written: !args.dryRun && t.action !== 'unchanged' });
72
+ }
73
+ }
74
+
64
75
  return written;
65
76
  }
66
77
 
@@ -93,11 +104,6 @@ function scaffoldCopilot({ cwd, answers, args, vars }) {
93
104
  writeFile(join(cwd, '.github', 'skills', 'README.md'), readTemplate('.github', 'skills', 'README.md'), args),
94
105
  );
95
106
 
96
- // CodeGraph MCP for VS Code Copilot
97
- if (answers.codegraph) {
98
- written.push(writeFile(join(cwd, '.vscode', 'mcp.json'), JSON.stringify(codegraphMcpJson(), null, 2), args));
99
- }
100
-
101
107
  return written;
102
108
  }
103
109
 
@@ -144,34 +150,10 @@ function buildOpencodeConfig({ answers, orchestratorModel }) {
144
150
  subagent_depth: 2,
145
151
  };
146
152
 
147
- if (answers.codegraph) {
148
- config.mcp = {
149
- codegraph: {
150
- type: 'local',
151
- command: ['codegraph', 'serve', '--mcp'],
152
- enabled: true,
153
- },
154
- };
155
- }
156
-
157
153
  return config;
158
154
  }
159
155
 
160
- function codegraphMcpJson() {
161
- return {
162
- inputs: [],
163
- servers: {
164
- codegraph: {
165
- command: 'codegraph',
166
- args: ['serve', '--mcp'],
167
- },
168
- },
169
- };
170
- }
171
-
172
156
  function buildVars(a) {
173
- const codegraphBlock = a.codegraph ? CODEGRAPH_BLOCK : '';
174
-
175
157
  return {
176
158
  PROJECT_NAME: a.projectName,
177
159
  ONE_LINER: a.oneLiner || 'TODO — write a one-line vision statement.',
@@ -190,7 +172,6 @@ ${a.docs}
190
172
  ` : '',
191
173
  DATE: new Date().toISOString().slice(0, 10),
192
174
  PERSONA_BLOCK: a.snark ? PERSONA_BLOCK : '',
193
- CODEGRAPH_BLOCK: codegraphBlock,
194
175
  };
195
176
  }
196
177
 
@@ -221,22 +202,6 @@ const PERSONA_BLOCK = `# Persona
221
202
 
222
203
  `;
223
204
 
224
- const CODEGRAPH_BLOCK = `
225
- <!-- CODEGRAPH_START -->
226
- ## CodeGraph
227
-
228
- This project is configured to use [CodeGraph](https://codegraph.ru) for graph-backed codebase context.
229
- When you need to understand relationships, call paths, or impacts, use:
230
-
231
- \`\`\`
232
- codegraph explore "<your question>"
233
- \`\`\`
234
-
235
- The CodeGraph MCP server is registered in the project config. Run \`codegraph init\` in this directory
236
- if the project has not been indexed yet.
237
- <!-- CODEGRAPH_END -->
238
- `;
239
-
240
205
  export function summarize(written, cwd) {
241
206
  const lines = [];
242
207
  for (const w of written) {
@@ -24,4 +24,3 @@ and Copilot all read `AGENTS.md` per the [agents.md](https://agents.md) conventi
24
24
  4. Per-agent memory lives in `histories/<agent>.md`.
25
25
  5. Append a session summary to `agent-diary.md` when work completes.
26
26
 
27
- {{CODEGRAPH_BLOCK}}
@@ -0,0 +1,187 @@
1
+ // cli-five jev-tier-router plugin (OpenCode).
2
+ //
3
+ // Ships a `local_tier_heuristic` tool that classifies a task description into
4
+ // cli-five's tier vocabulary: trivial | minor | major.
5
+ //
6
+ // TRUTHFUL NAMING: this is a LOCAL heuristic. It does NOT call Jev. As of
7
+ // 2026-09-26, jev-harness's `route` subcommand exposes no custom-criteria
8
+ // interface (it emits its own fixed tier vocabulary: deterministic /
9
+ // lightweight_system2 / heavy_system2) and returns a constant confidence
10
+ // (0.88) under its offline/mock engine, so it cannot be thresholded on.
11
+ // The name `local_tier_heuristic` is deliberate — do not revive `jev_tier_route`
12
+ // unless/until real Jev wiring is verified. See JEVR_SWAP_POINT below.
13
+ //
14
+ // Fail-open: if anything goes wrong the tool reports `available: false` and
15
+ // tier "major" (the expensive tier), so the Planner falls back to its own
16
+ // judgment. It never throws in a way that would break the Planner's turn.
17
+
18
+ import { appendFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+
21
+ const CONFIDENCE_CUTOFF = 0.6;
22
+ const FALLBACK_TIER = 'major';
23
+
24
+ const TIERS = ['trivial', 'minor', 'major'];
25
+
26
+ // Weighted signals. `strong` matches dominate; `moderate` accumulate.
27
+ const SIGNALS = [
28
+ // trivial — mechanical, single-token, no reasoning
29
+ { tier: 'trivial', weight: 3, re: /\b(typo|typos|whitespace|lint|linting|format|formatting|rename|renaming|comment|comments|docstring|spelling|indent(ation)?)\b/i },
30
+ { tier: 'trivial', weight: 2, re: /\b(README|changelog|CHANGELOG|\.md\b|docs?)\b/i },
31
+ { tier: 'trivial', weight: 2, re: /\b(one[- ]?line|single[- ]?(file|line)|small tweak|quick fix|minor tweak)\b/i },
32
+ { tier: 'trivial', weight: 2, re: /\b(delete|remove)\b[\s\w]{0,20}\b(console\.log|stray|unused|dead code|tmp|temp file)\b/i },
33
+
34
+ // major — architectural, cross-cutting, ambiguous scope
35
+ { tier: 'major', weight: 3, re: /\b(architect(ure|ural)?|redesign|rearchitect|rewrite|overhaul|migrat(e|ion)|replatform|distributed|scalab(le|ility)|multi[- ]?(tenant|region|service))\b/i },
36
+ { tier: 'major', weight: 3, re: /\b(entire|whole|across (the )?(codebase|repo(sitory)?|project)|end[- ]to[- ]end|system[- ]wide)\b/i },
37
+ { tier: 'major', weight: 2, re: /\b(concurren(cy|t)|race condition|deadlock|transaction(al)?|consistency|eventual consistency|saga|retry (architecture|strategy)|queue|scheduler|orchestrat(e|ion))\b/i },
38
+ { tier: 'major', weight: 2, re: /\b(performance|latency|throughput|optimi[sz]e|profil(e|ing)|security|auth(entication|orization)?|encryption|compliance|HIPAA|SOC ?2|GDPR)\b/i },
39
+ { tier: 'major', weight: 1, re: /\b(design|feature|implement|build|add support for|new (module|service|system))\b/i },
40
+
41
+ // minor — bounded, local, incremental
42
+ { tier: 'minor', weight: 3, re: /\b(validation|validate|error handling|error message|edge case|bug ?fix|fix (a |the )?bug|patch|handle null|guard clause)\b/i },
43
+ { tier: 'minor', weight: 2, re: /\b(component|function|method|handler|endpoint|form|button|modal|tooltip|dropdown)\b/i },
44
+ { tier: 'minor', weight: 2, re: /\b(refactor|extract|rename (the )?(function|method|class|module)|tidy|clean ?up)\b/i },
45
+ { tier: 'minor', weight: 1, re: /\b(add|update|adjust|tweak|improve|tidy)\b/i },
46
+ ];
47
+
48
+ /**
49
+ * Classify a task description locally.
50
+ *
51
+ * Returns { tier, confidence, rationale, available, source }.
52
+ * Ambiguity fails toward the expensive tier (major), never the cheap one.
53
+ */
54
+ export function classifyTask(description) {
55
+ const text = String(description ?? '').trim();
56
+ if (!text) {
57
+ return {
58
+ tier: FALLBACK_TIER,
59
+ confidence: 0,
60
+ rationale: 'Empty task description; defaulting to the expensive tier.',
61
+ available: true,
62
+ source: 'local_heuristic',
63
+ };
64
+ }
65
+
66
+ const scores = { trivial: 0, minor: 0, major: 0 };
67
+ const hits = { trivial: [], minor: [], major: [] };
68
+
69
+ for (const signal of SIGNALS) {
70
+ if (signal.re.test(text)) {
71
+ scores[signal.tier] += signal.weight;
72
+ hits[signal.tier].push(signal.re.source.slice(0, 40));
73
+ }
74
+ }
75
+
76
+ // Length is a weak major signal: long, multi-clause prompts rarely stay local.
77
+ const words = text.split(/\s+/).filter(Boolean).length;
78
+ if (words > 25) scores.major += 1;
79
+ if (words > 60) scores.major += 1;
80
+
81
+ const ranked = TIERS.map((tier) => ({ tier, score: scores[tier] })).sort((a, b) => b.score - a.score);
82
+ const [top, second] = ranked;
83
+
84
+ if (top.score === 0) {
85
+ // Nothing matched — ambiguous. Fail toward the expensive tier.
86
+ return {
87
+ tier: FALLBACK_TIER,
88
+ confidence: 0.3,
89
+ rationale: 'No tier signals matched; ambiguous, so defaulting to the expensive tier.',
90
+ available: true,
91
+ source: 'local_heuristic',
92
+ };
93
+ }
94
+
95
+ const total = TIERS.reduce((sum, tier) => sum + scores[tier], 0);
96
+ const separation = (top.score - (second?.score ?? 0)) / top.score;
97
+ const share = top.score / total;
98
+ let confidence = 0.5 * share + 0.5 * separation;
99
+
100
+ // A single weak hit with no corroboration is not a confident call.
101
+ if (top.score <= 1) confidence = Math.min(confidence, 0.5);
102
+
103
+ confidence = Math.round(confidence * 100) / 100;
104
+
105
+ if (confidence < CONFIDENCE_CUTOFF) {
106
+ return {
107
+ tier: FALLBACK_TIER,
108
+ confidence,
109
+ rationale: `Low confidence (${confidence} < ${CONFIDENCE_CUTOFF}) between ${top.tier} and ${second?.tier ?? 'n/a'}; defaulting to the expensive tier.`,
110
+ available: true,
111
+ source: 'local_heuristic',
112
+ };
113
+ }
114
+
115
+ return {
116
+ tier: top.tier,
117
+ confidence,
118
+ rationale: `Matched ${hits[top.tier].length} ${top.tier} signal(s).`,
119
+ available: true,
120
+ source: 'local_heuristic',
121
+ };
122
+ }
123
+
124
+ // ── JEVR_SWAP_POINT ───────────────────────────────────────────────────
125
+ // Real-Jev wiring would replace classifyTask() above with a shell-out to
126
+ // jev-harness route --json --task "<description>"
127
+ // and map the returned `selected_tier` onto cli-five's tiers, thresholding on
128
+ // `confidence`. It is NOT wired because, as of 2026-09-26, `route` exposes no
129
+ // custom-criteria interface, emits a fixed vocabulary, and returns a constant
130
+ // confidence under the offline engine. Re-verify before swapping.
131
+ // Exact call site: the `execute` handler below that calls classifyTask().
132
+ // ──────────────────────────────────────────────────────────────────────
133
+
134
+ export const __testables = { classifyTask, CONFIDENCE_CUTOFF, FALLBACK_TIER };
135
+
136
+ export default {
137
+ id: 'cli-five-jev-tier-router',
138
+ setup: async (ctx) => {
139
+ if (!ctx || !ctx.tool || typeof ctx.tool.transform !== 'function') return;
140
+
141
+ await ctx.tool.transform((tools) => {
142
+ tools.add({
143
+ name: 'local_tier_heuristic',
144
+ description:
145
+ 'Classify a task description into cli-five\'s tier vocabulary (trivial | minor | major) using a local heuristic. ' +
146
+ 'Call this once per task, before planning. Trust the returned tier when confidence >= 0.6; otherwise fall back to "major". ' +
147
+ 'This is a local heuristic, not a Jev call.',
148
+ input: {
149
+ type: 'object',
150
+ properties: {
151
+ description: {
152
+ type: 'string',
153
+ description: 'The task to classify, verbatim (the user request or planning prompt).',
154
+ },
155
+ },
156
+ required: ['description'],
157
+ additionalProperties: false,
158
+ },
159
+ async execute(input) {
160
+ try {
161
+ const result = classifyTask(input?.description);
162
+ return { content: JSON.stringify(result) };
163
+ } catch (err) {
164
+ // Fail-open: never break the caller's turn.
165
+ try {
166
+ appendFileSync(
167
+ process.env.CLI_FIVE_LOGFILE || join(process.cwd(), '.opencode', 'journals', 'jev-tier-router.log'),
168
+ `${new Date().toISOString()} local_tier_heuristic fail-open: ${err?.message || err}\n`,
169
+ );
170
+ } catch {
171
+ /* logging is best-effort */
172
+ }
173
+ return {
174
+ content: JSON.stringify({
175
+ tier: FALLBACK_TIER,
176
+ confidence: 0,
177
+ rationale: 'Tier classifier unavailable; defaulting to the expensive tier.',
178
+ available: false,
179
+ source: 'local_heuristic',
180
+ }),
181
+ };
182
+ }
183
+ },
184
+ });
185
+ });
186
+ },
187
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "cli-five-jev-tier-router",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.js"
8
+ }
9
+ }