aiki-cli 0.2.0
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 +55 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/bench/arms.js +104 -0
- package/dist/bench/harness.js +251 -0
- package/dist/bench/results.js +70 -0
- package/dist/bench/scoring/seeded-bugs.js +31 -0
- package/dist/cli/bench.js +50 -0
- package/dist/cli/config.js +51 -0
- package/dist/cli/doctor.js +115 -0
- package/dist/cli/index.js +129 -0
- package/dist/cli/models.js +51 -0
- package/dist/cli/providers.js +31 -0
- package/dist/cli/resolve.js +159 -0
- package/dist/cli/resume.js +94 -0
- package/dist/cli/run.js +155 -0
- package/dist/cli/sessions.js +35 -0
- package/dist/cli/show.js +73 -0
- package/dist/config/config.js +102 -0
- package/dist/config/smoke-cache.js +65 -0
- package/dist/council/open.js +26 -0
- package/dist/council/view.js +873 -0
- package/dist/orchestration/cluster.js +83 -0
- package/dist/orchestration/context.js +277 -0
- package/dist/orchestration/engine.js +92 -0
- package/dist/orchestration/git.js +133 -0
- package/dist/orchestration/jsonStage.js +32 -0
- package/dist/orchestration/skills.js +39 -0
- package/dist/orchestration/stages/cr-ladder.js +63 -0
- package/dist/orchestration/stages/cr-map.js +62 -0
- package/dist/orchestration/stages/cr-report.js +83 -0
- package/dist/orchestration/stages/cr-s4-review.js +69 -0
- package/dist/orchestration/stages/cr-s8-crossexam.js +104 -0
- package/dist/orchestration/stages/cr-s9-judge.js +89 -0
- package/dist/orchestration/stages/s0-grill.js +79 -0
- package/dist/orchestration/stages/s1-intent.js +25 -0
- package/dist/orchestration/stages/s10-render.js +198 -0
- package/dist/orchestration/stages/s2-misread.js +76 -0
- package/dist/orchestration/stages/s3-prompts.js +55 -0
- package/dist/orchestration/stages/s4-analyze.js +50 -0
- package/dist/orchestration/stages/s5-drift.js +40 -0
- package/dist/orchestration/stages/s6-claims.js +56 -0
- package/dist/orchestration/stages/s7-disagreement.js +134 -0
- package/dist/orchestration/stages/s8-verify.js +56 -0
- package/dist/orchestration/stages/s9-judge.js +152 -0
- package/dist/orchestration/stages/s9b-plan.js +192 -0
- package/dist/providers/adapter-core.js +131 -0
- package/dist/providers/adapters.js +9 -0
- package/dist/providers/agy.js +29 -0
- package/dist/providers/claude.js +56 -0
- package/dist/providers/codex.js +35 -0
- package/dist/providers/detect.js +21 -0
- package/dist/providers/probe.js +43 -0
- package/dist/providers/profiles.js +38 -0
- package/dist/providers/profiles.json +5 -0
- package/dist/providers/smoke.js +26 -0
- package/dist/providers/spawn.js +152 -0
- package/dist/providers/types.js +17 -0
- package/dist/schemas/index.js +374 -0
- package/dist/skills/.gitkeep +0 -0
- package/dist/skills/code-review/judge.md +23 -0
- package/dist/skills/code-review/reviewer.md +38 -0
- package/dist/skills/idea-refinement/analyst.md +45 -0
- package/dist/skills/idea-refinement/planner.md +25 -0
- package/dist/storage/feedback.js +111 -0
- package/dist/storage/paths.js +20 -0
- package/dist/storage/replay.js +0 -0
- package/dist/storage/runs-read.js +95 -0
- package/dist/storage/runs.js +129 -0
- package/dist/storage/sessions.js +71 -0
- package/dist/tui/app.js +444 -0
- package/dist/tui/format.js +27 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/smart-entry.js +106 -0
- package/dist/tui/timeline.js +91 -0
- package/dist/workflows/code-review.js +76 -0
- package/dist/workflows/idea-refinement.js +105 -0
- package/package.json +64 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aiki CLI entrypoint. Subcommands: doctor / providers / run / show / resolve / config.
|
|
3
|
+
// Bare `aiki` mounts the interactive TUI (T8), honoring .aiki/config.json (T9).
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { doctor } from './doctor.js';
|
|
6
|
+
import { providers } from './providers.js';
|
|
7
|
+
import { runCommand } from './run.js';
|
|
8
|
+
import { show } from './show.js';
|
|
9
|
+
import { resolve } from './resolve.js';
|
|
10
|
+
import { resumeCommand } from './resume.js';
|
|
11
|
+
import { sessionsCommand } from './sessions.js';
|
|
12
|
+
import { config } from './config.js';
|
|
13
|
+
import { modelsCommand } from './models.js';
|
|
14
|
+
import { benchCommand } from './bench.js';
|
|
15
|
+
import { ConfigError, loadLayeredConfig } from '../config/config.js';
|
|
16
|
+
import { resolveRunsRoot } from '../storage/paths.js';
|
|
17
|
+
import { startTui } from '../tui/index.js';
|
|
18
|
+
export const VERSION = '0.2.0';
|
|
19
|
+
const program = new Command();
|
|
20
|
+
/** commander collector for a repeatable option. */
|
|
21
|
+
const collect = (v, acc) => {
|
|
22
|
+
acc.push(v);
|
|
23
|
+
return acc;
|
|
24
|
+
};
|
|
25
|
+
program
|
|
26
|
+
.name('aiki')
|
|
27
|
+
.description('Local multi-model orchestration — coordinate your installed AI CLIs.')
|
|
28
|
+
.version(VERSION);
|
|
29
|
+
program
|
|
30
|
+
.command('doctor')
|
|
31
|
+
.description('Detect providers + probe flags + smoke test; print status table. Exit 0 iff ≥2 ready.')
|
|
32
|
+
.option('--no-smoke', 'skip the live smoke test (detection + probe only; no model calls)')
|
|
33
|
+
.option('--fresh', 'bypass the 6h smoke cache and re-run the smoke test')
|
|
34
|
+
.action(async (opts) => {
|
|
35
|
+
process.exit(await doctor({ smoke: opts.smoke, fresh: opts.fresh }));
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('providers')
|
|
39
|
+
.description('Machine-readable provider status: resolved capability profiles (§7.4). No model calls.')
|
|
40
|
+
.option('--json', 'print the resolved capability profiles as JSON')
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
process.exit(await providers({ json: opts.json }));
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command('run')
|
|
46
|
+
.description('Headless run of a workflow (§5). idea-refinement: text/file. code-review: git diff or --diff.')
|
|
47
|
+
.argument('<workflow>', 'workflow id: idea-refinement | code-review')
|
|
48
|
+
.argument('[input]', 'idea-refinement: inline text or a path to a .md file')
|
|
49
|
+
.option('--budget <n>', 'max provider calls for this run (default 12)', (v) => parseInt(v, 10))
|
|
50
|
+
.option('--base <ref>', 'code-review: base git ref to diff from (default: detected default branch)')
|
|
51
|
+
.option('--head <ref>', 'code-review: head git ref to diff to (default HEAD)')
|
|
52
|
+
.option('--diff <file>', 'code-review: review a patch file instead of computing a git diff')
|
|
53
|
+
.option('--cheap', 'code-review: Gemini+Codex review, Claude judges only disputes (~⅓ the Opus; experimental)')
|
|
54
|
+
.option('--yes', 'skip the run-cost confirmation prompt')
|
|
55
|
+
.action(async (workflow, input, opts) => {
|
|
56
|
+
process.exit(await runCommand(workflow, input, { budget: opts.budget, base: opts.base, head: opts.head, diff: opts.diff, cheap: opts.cheap, yes: opts.yes }));
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command('show')
|
|
60
|
+
.description('Print a stored run\'s final report; --raw lists artifact files. No id → latest run.')
|
|
61
|
+
.argument('[run-id]', 'run id or a unique suffix/substring (omit for the most recent run)')
|
|
62
|
+
.option('--raw', 'list the run\'s artifact files instead of the report')
|
|
63
|
+
.option('--html', 'write a self-contained council-view HTML file and print its path')
|
|
64
|
+
.option('--open', 'with --html, open the generated file in the default browser')
|
|
65
|
+
.action(async (runId, opts) => {
|
|
66
|
+
process.exit(await show(runId, { raw: opts.raw, html: opts.html, open: opts.open, root: await resolveRunsRoot() }));
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command('resolve')
|
|
70
|
+
.description('Annotate a run\'s adjudicated disputes → .aiki/feedback.jsonl (§127). No id → latest run.')
|
|
71
|
+
.argument('[run-id]', 'run id or a unique suffix/substring (omit for the most recent run)')
|
|
72
|
+
.option('--verdict <id=verdict>', 'non-interactive verdict, repeatable: <item-id>=<correct|incorrect|unsure>', collect, [])
|
|
73
|
+
.action(async (runId, opts) => {
|
|
74
|
+
process.exit(await resolve(runId, { verdict: opts.verdict, root: await resolveRunsRoot() }));
|
|
75
|
+
});
|
|
76
|
+
program
|
|
77
|
+
.command('sessions')
|
|
78
|
+
.description('List runs from the global registry (~/.aiki/sessions.jsonl), newest first.')
|
|
79
|
+
.option('--json', 'print the registry as JSON')
|
|
80
|
+
.action(async (opts) => {
|
|
81
|
+
process.exit(await sessionsCommand({ json: opts.json }));
|
|
82
|
+
});
|
|
83
|
+
program
|
|
84
|
+
.command('resume')
|
|
85
|
+
.description('Continue a killed/timed-out run from where it stopped (replays completed calls; §V6.3).')
|
|
86
|
+
.argument('[run-id]', 'run id or a unique suffix/substring (see `aiki sessions`)')
|
|
87
|
+
.action(async (runId) => {
|
|
88
|
+
process.exit(await resumeCommand(runId, { root: await resolveRunsRoot() }));
|
|
89
|
+
});
|
|
90
|
+
program
|
|
91
|
+
.command('bench')
|
|
92
|
+
.description('Run benchmark arms A–D on a task set; writes bench/results/*.json + summary table (§17).')
|
|
93
|
+
.argument('<workflow>', 'workflow id (v1: code-review)')
|
|
94
|
+
.option('--arms <list>', 'comma-separated arms to run', 'A,B,C,D')
|
|
95
|
+
.option('--set <name>', 'task set: build | holdout', 'build')
|
|
96
|
+
.option('--resume', 'continue the latest results file: keep already-scored case×arm pairs, retry the rest')
|
|
97
|
+
.option('--yes', 'actually run; without it, print the pre-run Opus-call estimate and exit')
|
|
98
|
+
.action(async (workflow, opts) => {
|
|
99
|
+
process.exit(await benchCommand(workflow, { arms: opts.arms, set: opts.set, resume: opts.resume, yes: opts.yes }));
|
|
100
|
+
});
|
|
101
|
+
program
|
|
102
|
+
.command('models')
|
|
103
|
+
.description('Show configurable models per provider (lists via the CLI where supported) + your current pins.')
|
|
104
|
+
.action(async () => {
|
|
105
|
+
process.exit(await modelsCommand());
|
|
106
|
+
});
|
|
107
|
+
program
|
|
108
|
+
.command('config')
|
|
109
|
+
.description('Print the effective config; --edit opens .aiki/config.json (§128).')
|
|
110
|
+
.option('--edit', 'open .aiki/config.json in $VISUAL/$EDITOR (created if missing)')
|
|
111
|
+
.action(async (opts) => {
|
|
112
|
+
process.exit(await config({ edit: opts.edit }));
|
|
113
|
+
});
|
|
114
|
+
// Bare `aiki` (no subcommand) → interactive TUI, honoring layered config (roles/budget/models).
|
|
115
|
+
program.action(async () => {
|
|
116
|
+
let cfg;
|
|
117
|
+
try {
|
|
118
|
+
cfg = await loadLayeredConfig();
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (e instanceof ConfigError) {
|
|
122
|
+
process.stderr.write(`${e.message}\n`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
startTui({ roleOverrides: cfg.roles, budget: cfg.budget, runsRoot: await resolveRunsRoot(), providerModels: cfg.models, version: VERSION });
|
|
128
|
+
});
|
|
129
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// `aiki models` (V8) — show which model each provider can run and your current per-provider pins.
|
|
2
|
+
// Only agy exposes a `models` command (verified 2026-07-06, docs/PROVIDER_NOTES.md); claude/codex take
|
|
3
|
+
// any id via `--model` but don't enumerate, so we say "type any id". Listing is free (no inference).
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { PROVIDER_IDS, DISPLAY_NAME } from '../providers/types.js';
|
|
6
|
+
import { detect } from '../providers/detect.js';
|
|
7
|
+
import { loadLayeredConfig } from '../config/config.js';
|
|
8
|
+
import { homeAikiRoot } from '../storage/paths.js';
|
|
9
|
+
/** Models the CLI can enumerate, or null if it has no list command. `agy models` blocks on stdin without
|
|
10
|
+
* a TTY (the known agy trap — see PROVIDER_NOTES), so we close the child's stdin. */
|
|
11
|
+
function listModels(id) {
|
|
12
|
+
if (id !== 'agy')
|
|
13
|
+
return Promise.resolve(null); // only agy has a `models` subcommand
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const child = execFile('agy', ['models'], { timeout: 20_000, maxBuffer: 1 << 20 }, (err, stdout) => {
|
|
16
|
+
if (err)
|
|
17
|
+
return resolve(null);
|
|
18
|
+
resolve(stdout.split('\n').map((l) => l.trim()).filter(Boolean));
|
|
19
|
+
});
|
|
20
|
+
child.stdin?.end();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Build the `aiki models` report as a string (so the TUI can render it too, not just stdout). */
|
|
24
|
+
export async function formatModels() {
|
|
25
|
+
const cfg = await loadLayeredConfig();
|
|
26
|
+
const models = cfg.models ?? {};
|
|
27
|
+
const out = ['Models — aiki passes your choice to each CLI as `--model <id>`.', ''];
|
|
28
|
+
for (const id of PROVIDER_IDS) {
|
|
29
|
+
const det = await detect(id);
|
|
30
|
+
const pinned = models[id];
|
|
31
|
+
const current = pinned ? `pinned: ${pinned}` : 'CLI default';
|
|
32
|
+
if (det.status !== 'READY') {
|
|
33
|
+
out.push(`${DISPLAY_NAME[id]} (${id}) — not installed`, '');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
out.push(`${DISPLAY_NAME[id]} (${id}) · ${current}`);
|
|
37
|
+
const list = await listModels(id);
|
|
38
|
+
if (list && list.length)
|
|
39
|
+
list.forEach((m) => out.push(` ${m}`));
|
|
40
|
+
else
|
|
41
|
+
out.push(" (this CLI doesn't list models — set any id it accepts)");
|
|
42
|
+
out.push('');
|
|
43
|
+
}
|
|
44
|
+
out.push('Set a model in .aiki/config.json (this project) or ' + homeAikiRoot() + '/config.json (global):');
|
|
45
|
+
out.push(' { "models": { "agy": "Gemini 3.1 Pro (High)", "claude": "opus", "codex": "gpt-5-codex" } }');
|
|
46
|
+
return out.join('\n');
|
|
47
|
+
}
|
|
48
|
+
export async function modelsCommand() {
|
|
49
|
+
process.stdout.write(`${await formatModels()}\n`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolveProfiles } from '../providers/profiles.js';
|
|
2
|
+
/**
|
|
3
|
+
* `aiki providers` (§5) — machine-readable provider status. `--json` prints the capability
|
|
4
|
+
* profiles actually resolved on this machine; bare prints a compact human summary. Detection +
|
|
5
|
+
* flag probe only, no model calls (§8). Exit 0 always (informational).
|
|
6
|
+
*/
|
|
7
|
+
export async function providers(opts = {}) {
|
|
8
|
+
const resolved = await resolveProfiles();
|
|
9
|
+
if (opts.json) {
|
|
10
|
+
process.stdout.write(JSON.stringify(resolved, null, 2) + '\n');
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
process.stdout.write(renderHuman(resolved) + '\n');
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const pad = (s, w) => s.padEnd(w);
|
|
17
|
+
function renderHuman(rows) {
|
|
18
|
+
const out = ['', ' aiki providers — resolved capability profiles (no smoke)', ''];
|
|
19
|
+
out.push(` ${pad('PROVIDER', 10)}${pad('VERSION', 11)}${pad('INSTALLED', 11)}${pad('JSON', 6)}${pad('READ-ONLY', 11)}CAPABILITY (reason/codeNav/json/ctx)`);
|
|
20
|
+
out.push(` ${'─'.repeat(88)}`);
|
|
21
|
+
for (const r of rows) {
|
|
22
|
+
const c = r.capability;
|
|
23
|
+
const cap = `${c.reasoning}/${c.codeNav}/${c.jsonReliability}/${c.contextExplore} ${c.cost}`;
|
|
24
|
+
const json = r.flags ? (r.flags.jsonOutput ? 'yes' : 'no') : '—';
|
|
25
|
+
const ro = r.flags ? r.flags.readOnlyFlag : '—';
|
|
26
|
+
out.push(` ${pad(r.displayName, 10)}${pad(r.version ?? '—', 11)}${pad(r.installed ? 'yes' : 'no', 11)}${pad(json, 6)}${pad(ro, 11)}${cap}`);
|
|
27
|
+
}
|
|
28
|
+
out.push('');
|
|
29
|
+
out.push(' Use --json for the machine-readable form.');
|
|
30
|
+
return out.join('\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// `aiki resolve <run-id>` (§5/§127) — the human review gate. Records a verdict on each of a run's
|
|
2
|
+
// annotatable items → appends `.aiki/feedback.jsonl`. Workflow-aware (T11):
|
|
3
|
+
// idea-refinement → the judge's adjudicated contradictions, verdict correct/incorrect/unsure;
|
|
4
|
+
// code-review → the report's kept findings, verdict fixed/wontfix/false-positive (false-positive
|
|
5
|
+
// labels feed the bench PRECISION metric).
|
|
6
|
+
// Two entry paths: an interactive readline loop (needs a TTY) and non-interactive `--verdict <id>=<v>`
|
|
7
|
+
// (repeatable, used by tests/automation). Both funnel through the pure core in storage/feedback.ts.
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { scoreFindings } from '../orchestration/stages/cr-report.js';
|
|
10
|
+
import { readJsonArtifact, resolveRunId, runDir } from '../storage/runs-read.js';
|
|
11
|
+
import { appendFeedback, buildFeedbackEntries, FeedbackError, parseVerdictFlags, VERDICT_VOCAB } from '../storage/feedback.js';
|
|
12
|
+
/** Build the annotatable list for an idea-refinement run (adjudicated contradictions). */
|
|
13
|
+
function ideaItems(judge, map) {
|
|
14
|
+
return judge.adjudications.map((a) => {
|
|
15
|
+
const c = map?.contradictions.find((x) => x.id === a.id);
|
|
16
|
+
const dispute = c?.attacks[0]?.argument ?? '';
|
|
17
|
+
return { id: a.id, ruling: a.ruling, label: `[${a.ruling}] ${dispute || a.reasoning}` };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Build the annotatable list for a code-review run (kept findings from the report). */
|
|
21
|
+
function reviewItems(map, judge) {
|
|
22
|
+
return scoreFindings(map, judge)
|
|
23
|
+
.filter((s) => s.disposition === 'kept')
|
|
24
|
+
.map((s) => ({
|
|
25
|
+
id: s.finding.id,
|
|
26
|
+
ruling: `${s.finding.severity}/${s.finding.category}/${s.confidence}`,
|
|
27
|
+
label: `[${s.finding.severity} ${s.confidence}] ${s.finding.file}:${s.finding.line_start}-${s.finding.line_end} — ${s.finding.claim}`,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function ask(rl, q) {
|
|
31
|
+
return new Promise((res) => rl.question(q, (a) => res(a.trim())));
|
|
32
|
+
}
|
|
33
|
+
/** Interactive annotation loop. `keys` maps a single-char shortcut to a verdict. */
|
|
34
|
+
async function interactiveAnnotate(items, keys) {
|
|
35
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
const verdicts = new Map();
|
|
37
|
+
const legend = Object.entries(keys).map(([k, v]) => `[${k}]${v.slice(1)}`).join(' / ');
|
|
38
|
+
process.stdout.write(`\n ${items.length} item(s). For each: ${legend} / [s]kip / [q]uit\n`);
|
|
39
|
+
try {
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
process.stdout.write(`\n ${item.id} ${item.label}\n`);
|
|
42
|
+
const key = (await ask(rl, ` verdict? `)).toLowerCase();
|
|
43
|
+
if (key === 'q')
|
|
44
|
+
break;
|
|
45
|
+
if (key === 's' || key === '')
|
|
46
|
+
continue;
|
|
47
|
+
const verdict = keys[key];
|
|
48
|
+
if (!verdict) {
|
|
49
|
+
process.stdout.write(` ? unrecognized "${key}" — skipping\n`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const note = await ask(rl, ` note (enter to skip): `);
|
|
53
|
+
verdicts.set(item.id, { verdict, ...(note ? { note } : {}) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
rl.close();
|
|
58
|
+
}
|
|
59
|
+
return verdicts;
|
|
60
|
+
}
|
|
61
|
+
/** First-letter shortcut map for a workflow's vocab (correct→c, false-positive→f... unique-first-letter). */
|
|
62
|
+
function shortcutKeys(vocab) {
|
|
63
|
+
const keys = {};
|
|
64
|
+
for (const v of vocab) {
|
|
65
|
+
let k = v[0];
|
|
66
|
+
let i = 1;
|
|
67
|
+
while (keys[k] && i < v.length)
|
|
68
|
+
k = v[i++];
|
|
69
|
+
keys[k] = v;
|
|
70
|
+
}
|
|
71
|
+
return keys;
|
|
72
|
+
}
|
|
73
|
+
export async function resolve(runArg, opts = {}) {
|
|
74
|
+
const root = opts.root ?? '.aiki';
|
|
75
|
+
const match = await resolveRunId(runArg, root);
|
|
76
|
+
if (!match.ok) {
|
|
77
|
+
if (match.kind === 'none')
|
|
78
|
+
process.stderr.write('no runs found under .aiki/runs/\n');
|
|
79
|
+
else if (match.kind === 'no-match')
|
|
80
|
+
process.stderr.write(`no run matches "${match.arg}". Omit the id for the most recent run.\n`);
|
|
81
|
+
else
|
|
82
|
+
process.stderr.write(`"${match.arg}" is ambiguous — matches:\n${match.candidates.map((c) => ` ${c}`).join('\n')}\n`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
const dir = runDir(match.runId, root);
|
|
86
|
+
const meta = await readJsonArtifact(dir, 'meta.json');
|
|
87
|
+
const workflow = meta?.workflow ?? 'idea-refinement';
|
|
88
|
+
const judge = await readJsonArtifact(dir, '09-judge-report.json');
|
|
89
|
+
let items;
|
|
90
|
+
let itemType;
|
|
91
|
+
if (workflow === 'code-review') {
|
|
92
|
+
const map = await readJsonArtifact(dir, '07-review-map.json');
|
|
93
|
+
if (map && judge) {
|
|
94
|
+
items = reviewItems(map, judge);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Single-call bench arms (A/B) write no review-map — annotate the harness-persisted
|
|
98
|
+
// scored findings instead (raw/bench-findings.json, the precision denominator).
|
|
99
|
+
const bench = await readJsonArtifact(dir, 'raw/bench-findings.json');
|
|
100
|
+
if (!bench || bench.length === 0) {
|
|
101
|
+
process.stdout.write(` run ${match.runId} has no findings to annotate.\n`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
items = bench.map((f) => ({
|
|
105
|
+
id: f.id,
|
|
106
|
+
ruling: `${f.severity}/${f.category}/BENCH`,
|
|
107
|
+
label: `[${f.severity}] ${f.file}:${f.line_start}-${f.line_end} — ${f.claim}`,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
itemType = 'finding';
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (!judge || judge.adjudications.length === 0) {
|
|
114
|
+
process.stdout.write(` run ${match.runId} has no adjudicated disputes to annotate.\n`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
const map = await readJsonArtifact(dir, '07-disagreement-map.json');
|
|
118
|
+
items = ideaItems(judge, map);
|
|
119
|
+
itemType = 'adjudication';
|
|
120
|
+
}
|
|
121
|
+
if (items.length === 0) {
|
|
122
|
+
process.stdout.write(` run ${match.runId} has no items to annotate.\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const vocab = VERDICT_VOCAB[workflow];
|
|
126
|
+
let verdicts;
|
|
127
|
+
if (opts.verdict?.length) {
|
|
128
|
+
try {
|
|
129
|
+
verdicts = parseVerdictFlags(opts.verdict, vocab);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (!process.stdin.isTTY) {
|
|
137
|
+
process.stderr.write(`resolve is interactive — in a non-interactive shell pass --verdict <id>=<${vocab.join('|')}>\n`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
verdicts = await interactiveAnnotate(items, shortcutKeys(vocab));
|
|
142
|
+
}
|
|
143
|
+
const annItems = items.map((i) => ({ id: i.id, ruling: i.ruling }));
|
|
144
|
+
let entries;
|
|
145
|
+
try {
|
|
146
|
+
entries = buildFeedbackEntries(match.runId, workflow, annItems, verdicts, new Date(), itemType);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
process.stderr.write(`${e instanceof FeedbackError ? e.message : String(e)}\n`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
if (entries.length === 0) {
|
|
153
|
+
process.stdout.write(' no verdicts recorded.\n');
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
const path = await appendFeedback(entries);
|
|
157
|
+
process.stdout.write(` ✔ recorded ${entries.length} verdict(s) → ${path}\n`);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// `aiki resume <session-id>` (V6.3) — continue a killed/timed-out run from where it stopped, without
|
|
2
|
+
// re-spending the calls that already succeeded. It re-runs the pipeline into a FRESH run, replaying every
|
|
3
|
+
// completed (provider, prompt) from the old run's raw/ outputs; only the failed stage onward hits a model.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { run as runEngine } from '../orchestration/engine.js';
|
|
7
|
+
import { ConfigError, loadLayeredConfig } from '../config/config.js';
|
|
8
|
+
import { resolveRunsRoot } from '../storage/paths.js';
|
|
9
|
+
import { buildReplayCache } from '../storage/replay.js';
|
|
10
|
+
import { findSession } from '../storage/sessions.js';
|
|
11
|
+
import { readJsonArtifact, resolveRunId, runDir } from '../storage/runs-read.js';
|
|
12
|
+
export async function resumeCommand(runArg, opts = {}) {
|
|
13
|
+
if (!runArg) {
|
|
14
|
+
process.stderr.write('usage: aiki resume <session-id> (see `aiki sessions`)\n');
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const root = opts.root ?? '.aiki';
|
|
18
|
+
// Locate the run: prefer the global registry (finds it across locations), else the current root.
|
|
19
|
+
let oldId;
|
|
20
|
+
let oldDir;
|
|
21
|
+
let workflow;
|
|
22
|
+
let cwd;
|
|
23
|
+
const sess = await findSession(runArg);
|
|
24
|
+
if (sess && 'ambiguous' in sess) {
|
|
25
|
+
process.stderr.write(`"${runArg}" is ambiguous — matches:\n${sess.ambiguous.map((c) => ` ${c}`).join('\n')}\n`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
if (sess) {
|
|
29
|
+
oldId = sess.id;
|
|
30
|
+
workflow = sess.workflow;
|
|
31
|
+
oldDir = join(sess.runsRoot, 'runs', sess.id);
|
|
32
|
+
cwd = sess.cwd;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const match = await resolveRunId(runArg, root);
|
|
36
|
+
if (!match.ok) {
|
|
37
|
+
process.stderr.write(`no run matches "${runArg}" (checked the session registry and ${root}/runs).\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
oldId = match.runId;
|
|
41
|
+
oldDir = runDir(match.runId, root);
|
|
42
|
+
const meta = await readJsonArtifact(oldDir, 'meta.json');
|
|
43
|
+
if (!meta) {
|
|
44
|
+
process.stderr.write(`cannot read meta.json for ${oldId} — nothing to resume.\n`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
workflow = meta.workflow;
|
|
48
|
+
}
|
|
49
|
+
// Recover the original input the run was started with.
|
|
50
|
+
const inputFile = workflow === 'code-review' ? 'diff.patch' : 'idea.md';
|
|
51
|
+
let input;
|
|
52
|
+
try {
|
|
53
|
+
input = await readFile(join(oldDir, 'inputs', inputFile), 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
process.stderr.write(`cannot recover the input (inputs/${inputFile}) for ${oldId} — nothing to resume.\n`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
const replay = await buildReplayCache(oldDir);
|
|
60
|
+
if (replay.size === 0) {
|
|
61
|
+
process.stderr.write(`no completed calls found for ${oldId} — start a fresh run instead.\n`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
let cfg;
|
|
65
|
+
try {
|
|
66
|
+
cfg = await loadLayeredConfig();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
if (e instanceof ConfigError) {
|
|
70
|
+
process.stderr.write(`${e.message}\n`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
process.stdout.write(` resuming ${oldId} (${workflow}) — replaying ${replay.size} completed call(s); only the rest will hit a model.\n`);
|
|
76
|
+
const outcome = await runEngine(workflow, input, {
|
|
77
|
+
budget: cfg.budget,
|
|
78
|
+
deadlineMs: cfg.deadlineMs,
|
|
79
|
+
roleOverrides: cfg.roles,
|
|
80
|
+
cwd: workflow === 'code-review' ? cwd : undefined, // code-review reviewers run at the repo root
|
|
81
|
+
runsRoot: await resolveRunsRoot(),
|
|
82
|
+
replay,
|
|
83
|
+
resumedFrom: oldId,
|
|
84
|
+
providerModels: cfg.models,
|
|
85
|
+
});
|
|
86
|
+
if (outcome.ok) {
|
|
87
|
+
process.stdout.write(`\n ✔ resumed run ${outcome.runId} complete — ${outcome.callCount} new provider call(s)\n artifacts: ${outcome.dir}\n\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
process.stderr.write(`\n ✖ resumed run ${outcome.runId} failed [${outcome.error?.code}]: ${outcome.error?.message}\n` +
|
|
91
|
+
(outcome.dir ? ` partial artifacts: ${outcome.dir} — you can \`aiki resume ${outcome.runId}\` again\n` : '') +
|
|
92
|
+
'\n');
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// `aiki run <workflow> [input]` (§5) — headless run. idea-refinement takes inline text or a file path;
|
|
2
|
+
// code-review computes a git diff from --base/--head (or reads --diff) and reviews it at the repo root.
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { run as runEngine } from '../orchestration/engine.js';
|
|
6
|
+
import { ConfigError, loadLayeredConfig } from '../config/config.js';
|
|
7
|
+
import { computeDiff, detectDefaultBranch, GitError, repoToplevel } from '../orchestration/git.js';
|
|
8
|
+
import { resolveRunsRoot } from '../storage/paths.js';
|
|
9
|
+
import { openCouncilHtml } from '../council/open.js';
|
|
10
|
+
const WORKFLOWS = ['idea-refinement', 'code-review'];
|
|
11
|
+
/** Rough provider-call estimate for the run-cost preview (V5). Approximate — the real count varies with
|
|
12
|
+
* §14 repairs, cross-exam skips, and quorum. `opus` = the Claude/Opus subset (the metered-cost driver). */
|
|
13
|
+
export function estimateRun(workflow, opts = {}) {
|
|
14
|
+
if (workflow === 'code-review')
|
|
15
|
+
return { calls: 5, opus: opts.cheap ? 1 : 2 };
|
|
16
|
+
return { calls: 12, opus: 4 }; // idea-refinement (S0/S1/S3 on agy; S2 has claude; S7 + S9 + S9b run on the claude judge)
|
|
17
|
+
}
|
|
18
|
+
/** Thin y/N prompt (default yes). Only used on an interactive TTY. */
|
|
19
|
+
function confirm(question) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
rl.question(question, (a) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(!/^n/i.test(a.trim()));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Resolve an idea-refinement input: an existing file path → its contents, else the arg as inline text. */
|
|
29
|
+
async function resolveInput(arg) {
|
|
30
|
+
if (!arg)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
return await readFile(arg, 'utf8'); // path
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return arg; // inline text
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Build the code-review input: the unified diff + the repo-root cwd reviewers run in (§12.2). Returns
|
|
40
|
+
* a `done` code for the non-run outcomes (no changes / usage / git error) so the caller can exit. */
|
|
41
|
+
async function resolveCodeReview(opts) {
|
|
42
|
+
const repoRoot = await repoToplevel(process.cwd());
|
|
43
|
+
let diff;
|
|
44
|
+
if (opts.diff) {
|
|
45
|
+
try {
|
|
46
|
+
diff = await readFile(opts.diff, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
process.stderr.write(`cannot read --diff file "${opts.diff}"\n`);
|
|
50
|
+
return { done: 1 };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (!repoRoot) {
|
|
55
|
+
process.stderr.write('not inside a git repository — pass --diff <file> instead\n');
|
|
56
|
+
return { done: 1 };
|
|
57
|
+
}
|
|
58
|
+
const base = opts.base ?? await detectDefaultBranch(repoRoot);
|
|
59
|
+
if (!base) {
|
|
60
|
+
process.stderr.write('cannot detect default branch — pass --base <ref>, or --diff <file>\n');
|
|
61
|
+
return { done: 1 };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
diff = await computeDiff(base, opts.head ?? 'HEAD', repoRoot);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
process.stderr.write(`${e instanceof GitError ? e.message : String(e)}\n`);
|
|
68
|
+
return { done: 1 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!diff.trim()) {
|
|
72
|
+
process.stdout.write(' no changes to review.\n');
|
|
73
|
+
return { done: 0 };
|
|
74
|
+
}
|
|
75
|
+
return { text: diff, cwd: repoRoot ?? process.cwd() };
|
|
76
|
+
}
|
|
77
|
+
export async function runCommand(workflow, input, opts = {}) {
|
|
78
|
+
if (!WORKFLOWS.includes(workflow)) {
|
|
79
|
+
process.stderr.write(`unknown workflow "${workflow}". Available: ${WORKFLOWS.join(', ')}\n`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
let text;
|
|
83
|
+
let cwd;
|
|
84
|
+
if (workflow === 'code-review') {
|
|
85
|
+
const r = await resolveCodeReview(opts);
|
|
86
|
+
if ('done' in r)
|
|
87
|
+
return r.done;
|
|
88
|
+
({ text, cwd } = r);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const resolved = await resolveInput(input);
|
|
92
|
+
if (!resolved || !resolved.trim()) {
|
|
93
|
+
process.stderr.write(`no input. Usage: aiki run ${workflow} "<text>" | aiki run ${workflow} ./file.md\n`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
text = resolved;
|
|
97
|
+
}
|
|
98
|
+
// Precedence (§10/T9): --budget flag > config.budget > built-in default. roles/deadline/models are
|
|
99
|
+
// config-only (layered: global ~/.aiki base, project .aiki override).
|
|
100
|
+
let cfg;
|
|
101
|
+
try {
|
|
102
|
+
cfg = await loadLayeredConfig();
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (e instanceof ConfigError) {
|
|
106
|
+
process.stderr.write(`${e.message}\n`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
// --cheap = bench Arm E's Opus-thrift role swap (agy+codex reviewers, claude judge). code-review only;
|
|
112
|
+
// takes precedence over config roles for those two seats. Experimental — see BENCHMARK.md amendment E1.
|
|
113
|
+
let roleOverrides = cfg.roles;
|
|
114
|
+
if (opts.cheap) {
|
|
115
|
+
if (workflow === 'code-review')
|
|
116
|
+
roleOverrides = { ...cfg.roles, s4: ['agy', 'codex'], judge: 'claude' };
|
|
117
|
+
else
|
|
118
|
+
process.stderr.write('--cheap only applies to code-review; ignoring for this workflow.\n');
|
|
119
|
+
}
|
|
120
|
+
// Run-cost preview (V5): show the estimate; confirm interactively unless --yes or non-interactive.
|
|
121
|
+
const est = estimateRun(workflow, { cheap: opts.cheap });
|
|
122
|
+
const note = ` ≈${est.calls} provider call(s), ~${est.opus} on Claude/Opus (the metered part).`;
|
|
123
|
+
if (!opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
|
124
|
+
if (!(await confirm(`${note}\n Continue? [Y/n] `))) {
|
|
125
|
+
process.stdout.write(' cancelled.\n');
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
process.stdout.write(`${note}\n`);
|
|
131
|
+
}
|
|
132
|
+
const outcome = await runEngine(workflow, text, {
|
|
133
|
+
budget: opts.budget ?? cfg.budget,
|
|
134
|
+
deadlineMs: cfg.deadlineMs,
|
|
135
|
+
roleOverrides,
|
|
136
|
+
cwd, // code-review: repo root; idea-refinement: undefined → run dir
|
|
137
|
+
runsRoot: await resolveRunsRoot(), // hybrid: repo .aiki when in a repo, else ~/.aiki
|
|
138
|
+
providerModels: cfg.models, // V8: per-provider model → CLI --model
|
|
139
|
+
});
|
|
140
|
+
if (outcome.ok) {
|
|
141
|
+
process.stdout.write(`\n ✔ run ${outcome.runId} complete — ${outcome.callCount} provider call(s)\n artifacts: ${outcome.dir}\n`);
|
|
142
|
+
// Auto-open the readable report in the browser (interactive terminals only; skipped in pipes/CI).
|
|
143
|
+
if (process.stdout.isTTY) {
|
|
144
|
+
const html = await openCouncilHtml(outcome.runId, outcome.dir);
|
|
145
|
+
if (html)
|
|
146
|
+
process.stdout.write(` report: ${html} — opening in your browser…\n`);
|
|
147
|
+
}
|
|
148
|
+
process.stdout.write('\n');
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
process.stderr.write(`\n ✖ run ${outcome.runId} failed [${outcome.error?.code}]: ${outcome.error?.message}\n` +
|
|
152
|
+
(outcome.dir ? ` partial artifacts: ${outcome.dir}\n` : '') +
|
|
153
|
+
'\n');
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// `aiki sessions` (V6.3) — list runs from the global registry (~/.aiki/sessions.jsonl), newest first,
|
|
2
|
+
// across every location aiki was launched from. Failed/aborted ones are resumable via `aiki resume <id>`.
|
|
3
|
+
import { readSessions } from '../storage/sessions.js';
|
|
4
|
+
const MARK = { running: '●', ok: '✔', failed: '✖', aborted: '⊘' };
|
|
5
|
+
function ago(iso) {
|
|
6
|
+
const ms = Date.now() - Date.parse(iso);
|
|
7
|
+
if (Number.isNaN(ms))
|
|
8
|
+
return iso;
|
|
9
|
+
const m = Math.floor(ms / 60000);
|
|
10
|
+
if (m < 1)
|
|
11
|
+
return 'just now';
|
|
12
|
+
if (m < 60)
|
|
13
|
+
return `${m}m ago`;
|
|
14
|
+
const h = Math.floor(m / 60);
|
|
15
|
+
if (h < 24)
|
|
16
|
+
return `${h}h ago`;
|
|
17
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
18
|
+
}
|
|
19
|
+
export async function sessionsCommand(opts = {}) {
|
|
20
|
+
const all = await readSessions();
|
|
21
|
+
if (opts.json) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify(all, null, 2)}\n`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (all.length === 0) {
|
|
26
|
+
process.stdout.write('no sessions yet — run `aiki` or `aiki run <workflow> …` first.\n');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
const lines = all.slice(0, 30).map((s) => {
|
|
30
|
+
const resumable = s.status === 'failed' || s.status === 'aborted';
|
|
31
|
+
return ` ${MARK[s.status]} ${s.id} ${s.workflow.padEnd(16)} ${ago(s.startedAt).padStart(9)}${resumable ? ' ← aiki resume ' + s.id : ''}`;
|
|
32
|
+
});
|
|
33
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|