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
package/dist/cli/show.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// `aiki show <run-id>` (§5) — print a stored run's final report; `--raw` lists artifact files.
|
|
2
|
+
// A partial/aborted run (no final-report.md) falls back to a short summary from meta.json (grilled
|
|
3
|
+
// 2026-07-04). Run-id arg is resolved by suffix/substring (no arg → latest).
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { listArtifacts, readFinalReport, readJsonArtifact, resolveRunId, runDir } from '../storage/runs-read.js';
|
|
6
|
+
import { writeCouncilHtml } from '../council/view.js';
|
|
7
|
+
import { openInBrowser } from '../council/open.js';
|
|
8
|
+
/** Emit the resolution error for a failed run-id match. */
|
|
9
|
+
function reportMatchError(match) {
|
|
10
|
+
if (match.kind === 'none') {
|
|
11
|
+
process.stderr.write('no runs found under .aiki/runs/ — run one first (`aiki run idea-refinement …`)\n');
|
|
12
|
+
}
|
|
13
|
+
else if (match.kind === 'no-match') {
|
|
14
|
+
process.stderr.write(`no run matches "${match.arg}". Omit the id for the most recent run.\n`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
process.stderr.write(`"${match.arg}" is ambiguous — matches:\n${match.candidates.map((c) => ` ${c}`).join('\n')}\n`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Short summary for a run with no final report (aborted/partial). */
|
|
21
|
+
function partialSummary(runId, meta, artifacts) {
|
|
22
|
+
const stages = artifacts.filter((f) => /^\d\d?[-/]/.test(f)).map((f) => f.split('/')[0]);
|
|
23
|
+
const uniqueStages = [...new Set(stages)];
|
|
24
|
+
const lines = [` run ${runId} — incomplete (no final report)`];
|
|
25
|
+
if (meta) {
|
|
26
|
+
lines.push(` workflow: ${meta.workflow}`);
|
|
27
|
+
lines.push(` exit status: ${meta.exit_status}${meta.aborted ? ' (aborted:true)' : ''}`);
|
|
28
|
+
lines.push(` calls: ${meta.call_count}/${meta.budget.limit}`);
|
|
29
|
+
if (meta.flags?.length)
|
|
30
|
+
lines.push(` flags: ${meta.flags.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push(' meta.json absent — run likely crashed before finalize.');
|
|
34
|
+
}
|
|
35
|
+
lines.push(` stages on disk: ${uniqueStages.join(', ') || '(none)'}`);
|
|
36
|
+
lines.push(` → inspect partial artifacts: aiki show ${runId} --raw`);
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
export async function show(runArg, opts = {}) {
|
|
40
|
+
const root = opts.root ?? '.aiki';
|
|
41
|
+
const match = await resolveRunId(runArg, root);
|
|
42
|
+
if (!match.ok) {
|
|
43
|
+
reportMatchError(match);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const dir = runDir(match.runId, root);
|
|
47
|
+
if (opts.html) {
|
|
48
|
+
const path = await writeCouncilHtml(match.runId, dir);
|
|
49
|
+
if (!path) {
|
|
50
|
+
process.stderr.write(`cannot render council view for ${match.runId}: missing or unreadable meta.json\n`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const abs = resolve(path);
|
|
54
|
+
process.stdout.write(`${abs}\n`);
|
|
55
|
+
if (opts.open)
|
|
56
|
+
openInBrowser(abs);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (opts.raw) {
|
|
60
|
+
const files = await listArtifacts(dir);
|
|
61
|
+
process.stdout.write(`${dir}\n${files.map((f) => ` ${f}`).join('\n')}\n`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const report = await readFinalReport(dir);
|
|
65
|
+
if (report !== null) {
|
|
66
|
+
process.stdout.write(report.endsWith('\n') ? report : `${report}\n`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
// No final report → partial/aborted run: summarize instead of failing.
|
|
70
|
+
const [meta, artifacts] = await Promise.all([readJsonArtifact(dir, 'meta.json'), listArtifacts(dir)]);
|
|
71
|
+
process.stdout.write(`\n${partialSummary(match.runId, meta, artifacts)}\n\n`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// `.aiki/config.json` loading (T9, §5/§273/§443). Project-local, human-edited config that pins
|
|
2
|
+
// roles + budget/deadline. Precedence is always: run flag > config > built-in default.
|
|
3
|
+
//
|
|
4
|
+
// Design decisions (grilled 2026-07-04, see .agent/STATE.md):
|
|
5
|
+
// - `roles` is a FLAT GLOBAL Partial<RoleMap> — applied as roleOverrides for every workflow via the
|
|
6
|
+
// existing resolveRoles(workflow, available, overrides) seam. (§273 "config pins roles globally".)
|
|
7
|
+
// - Missing file → defaults (not an error). Present but invalid (bad JSON or schema) → HARD-FAIL with a
|
|
8
|
+
// message naming the file + the exact problem. We never silently ignore a config the user believes is
|
|
9
|
+
// active (schema-boundary rule, CLAUDE.md).
|
|
10
|
+
// - The 6h smoke cache is NOT here — it lives in a separate tool-owned .aiki/smoke-cache.json so `doctor`
|
|
11
|
+
// never rewrites this human-edited file (deviation from the plan's literal "cached in config.json").
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { ProviderIdSchema } from '../schemas/index.js';
|
|
16
|
+
import { DEFAULT_BUDGET, DEFAULT_DEADLINE_MS } from '../orchestration/context.js';
|
|
17
|
+
import { homeAikiRoot } from '../storage/paths.js';
|
|
18
|
+
/** Partial role pin — any subset of the RoleMap roles. `.strict()` so a typo'd key hard-fails. */
|
|
19
|
+
const ConfigRoles = z
|
|
20
|
+
.object({
|
|
21
|
+
analyst: ProviderIdSchema.optional(),
|
|
22
|
+
judge: ProviderIdSchema.optional(),
|
|
23
|
+
verifier: ProviderIdSchema.optional(),
|
|
24
|
+
s4: z.array(ProviderIdSchema).min(1).optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
/** Per-provider model override (V8). Each CLI runs its own model families, so model choice is per
|
|
28
|
+
* provider; the id is passed verbatim to the CLI as `--model <id>` (see docs/PROVIDER_NOTES.md). */
|
|
29
|
+
const ProviderModels = z
|
|
30
|
+
.object({
|
|
31
|
+
claude: z.string().min(1).optional(),
|
|
32
|
+
codex: z.string().min(1).optional(),
|
|
33
|
+
agy: z.string().min(1).optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
/** The `.aiki/config.json` schema. `.strict()` → an unknown top-level key is a hard-fail (typo guard). */
|
|
37
|
+
export const AikiConfig = z
|
|
38
|
+
.object({
|
|
39
|
+
roles: ConfigRoles.optional(),
|
|
40
|
+
models: ProviderModels.optional(),
|
|
41
|
+
budget: z.number().int().positive().optional(),
|
|
42
|
+
deadlineMs: z.number().int().positive().optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
export class ConfigError extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'ConfigError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Load `.aiki/config.json`. Missing → `{}` (defaults). Present-but-invalid (unparseable JSON or a
|
|
53
|
+
* value that fails the zod schema) → throws `ConfigError` naming the file + the precise problem.
|
|
54
|
+
*/
|
|
55
|
+
export async function loadConfig(root = '.aiki') {
|
|
56
|
+
const path = join(root, 'config.json');
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = await readFile(path, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {}; // missing file = use defaults (not an error)
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
throw new ConfigError(`${path}: invalid JSON — ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
const res = AikiConfig.safeParse(parsed);
|
|
72
|
+
if (!res.success) {
|
|
73
|
+
const issue = res.error.issues[0];
|
|
74
|
+
const where = issue?.path.length ? `config.${issue.path.join('.')}` : 'config';
|
|
75
|
+
throw new ConfigError(`${path}: ${where} — ${issue?.message ?? 'invalid config'}`);
|
|
76
|
+
}
|
|
77
|
+
return res.data;
|
|
78
|
+
}
|
|
79
|
+
/** Merge the loaded config over built-in defaults for display (`aiki config`). Roles = pins only. */
|
|
80
|
+
export function effectiveConfig(cfg) {
|
|
81
|
+
return {
|
|
82
|
+
budget: cfg.budget ?? DEFAULT_BUDGET,
|
|
83
|
+
deadlineMs: cfg.deadlineMs ?? DEFAULT_DEADLINE_MS,
|
|
84
|
+
roles: cfg.roles ?? {},
|
|
85
|
+
models: cfg.models ?? {},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Merge a global (base) config with a project (override): project wins per field; roles/models KEYS merge. */
|
|
89
|
+
export function mergeConfig(base, over) {
|
|
90
|
+
const merged = { ...base, ...over };
|
|
91
|
+
if (base.roles || over.roles)
|
|
92
|
+
merged.roles = { ...base.roles, ...over.roles };
|
|
93
|
+
if (base.models || over.models)
|
|
94
|
+
merged.models = { ...base.models, ...over.models };
|
|
95
|
+
return merged;
|
|
96
|
+
}
|
|
97
|
+
/** Layered config: global `~/.aiki/config.json` (base) overlaid by the project `.aiki/config.json`. What
|
|
98
|
+
* the CLI actually uses (V8) — a user can set defaults once in the global file and override per project. */
|
|
99
|
+
export async function loadLayeredConfig() {
|
|
100
|
+
const [global, project] = await Promise.all([loadConfig(homeAikiRoot()), loadConfig('.aiki')]);
|
|
101
|
+
return mergeConfig(global, project);
|
|
102
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `.aiki/smoke-cache.json` — the §242 6h smoke-test cache. Tool-owned (doctor writes it), kept SEPARATE
|
|
2
|
+
// from the human-edited .aiki/config.json so doctor never clobbers hand-edits (grilled 2026-07-04).
|
|
3
|
+
//
|
|
4
|
+
// Unlike config.json, a corrupt/missing cache is NOT a hard error — it's disposable, so we silently
|
|
5
|
+
// treat it as empty and re-run the smoke. An entry is stale when it's older than 6h OR the provider's
|
|
6
|
+
// detected version has changed since it was cached (an upgrade should re-prove the provider).
|
|
7
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ProviderIdSchema } from '../schemas/index.js';
|
|
11
|
+
export const SMOKE_TTL_MS = 6 * 60 * 60 * 1000; // §242
|
|
12
|
+
const SmokeCacheEntrySchema = z.object({
|
|
13
|
+
ok: z.boolean(),
|
|
14
|
+
error: z.string().optional(),
|
|
15
|
+
durationMs: z.number().nonnegative(),
|
|
16
|
+
version: z.string().nullable(), // detected --version when cached; version change ⇒ stale
|
|
17
|
+
at: z.string(), // ISO-8601 of when the smoke ran
|
|
18
|
+
});
|
|
19
|
+
const SmokeCacheSchema = z.record(ProviderIdSchema, SmokeCacheEntrySchema);
|
|
20
|
+
/** Pure: is a cached entry still usable? Fresh = same detected version AND within the TTL. */
|
|
21
|
+
export function isFresh(entry, detectedVersion, nowMs, ttlMs = SMOKE_TTL_MS) {
|
|
22
|
+
if (entry.version !== detectedVersion)
|
|
23
|
+
return false;
|
|
24
|
+
const at = Date.parse(entry.at);
|
|
25
|
+
if (Number.isNaN(at))
|
|
26
|
+
return false;
|
|
27
|
+
return nowMs - at < ttlMs;
|
|
28
|
+
}
|
|
29
|
+
/** Pure: build a cache entry from a fresh smoke result. */
|
|
30
|
+
export function toEntry(smoke, version, at = new Date()) {
|
|
31
|
+
return {
|
|
32
|
+
ok: smoke.ok,
|
|
33
|
+
...(smoke.error ? { error: smoke.error } : {}),
|
|
34
|
+
durationMs: smoke.durationMs,
|
|
35
|
+
version,
|
|
36
|
+
at: at.toISOString(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Reconstruct a Smoke (for rendering) from a cached entry. */
|
|
40
|
+
export function entryToSmoke(entry) {
|
|
41
|
+
return {
|
|
42
|
+
ok: entry.ok,
|
|
43
|
+
...(entry.error ? { error: entry.error } : {}),
|
|
44
|
+
nonce: 'cached',
|
|
45
|
+
durationMs: entry.durationMs,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Read the cache; missing or corrupt → `{}` (disposable, never throws). */
|
|
49
|
+
export async function readSmokeCache(root = '.aiki') {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = SmokeCacheSchema.safeParse(JSON.parse(await readFile(join(root, 'smoke-cache.json'), 'utf8')));
|
|
52
|
+
return parsed.success ? parsed.data : {};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Atomically write the cache (temp + rename). */
|
|
59
|
+
export async function writeSmokeCache(cache, root = '.aiki') {
|
|
60
|
+
await mkdir(root, { recursive: true });
|
|
61
|
+
const full = join(root, 'smoke-cache.json');
|
|
62
|
+
const tmp = `${full}.tmp`;
|
|
63
|
+
await writeFile(tmp, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
64
|
+
await rename(tmp, full);
|
|
65
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Open a finished run's council HTML in the OS browser. Shared by `aiki show --open`, the headless
|
|
2
|
+
// `aiki run` success path, and the TUI. Best-effort: auto-open is a convenience, never a run failure.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { writeCouncilHtml } from './view.js';
|
|
6
|
+
/** Open a file in the OS default handler. Detached + unref so aiki can exit immediately. */
|
|
7
|
+
export function openInBrowser(path) {
|
|
8
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
9
|
+
const child = spawn(cmd, [path], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' });
|
|
10
|
+
child.on('error', () => process.stderr.write(`could not auto-open — open it manually: ${path}\n`));
|
|
11
|
+
child.unref();
|
|
12
|
+
}
|
|
13
|
+
/** Render a finished run's council HTML and open it. Never throws — returns the absolute path, or null. */
|
|
14
|
+
export async function openCouncilHtml(runId, dir) {
|
|
15
|
+
try {
|
|
16
|
+
const path = await writeCouncilHtml(runId, dir);
|
|
17
|
+
if (!path)
|
|
18
|
+
return null;
|
|
19
|
+
const abs = resolve(path);
|
|
20
|
+
openInBrowser(abs);
|
|
21
|
+
return abs;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|