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,95 @@
|
|
|
1
|
+
// Read-side helpers for stored runs under `.aiki/runs/` (T9). The write side is RunWriter (runs.ts);
|
|
2
|
+
// this is the counterpart used by `aiki show` and `aiki resolve`.
|
|
3
|
+
//
|
|
4
|
+
// Run ids are timestamp-prefixed (`20260704-1312-idea-refinement-8c44`) so a plain lexical sort is
|
|
5
|
+
// chronological — the last element is the most recent run.
|
|
6
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
/**
|
|
9
|
+
* Pure run-id resolution (unit-tested without fs). Rules (grilled 2026-07-04):
|
|
10
|
+
* - no arg → the most recent run (lexical-max, since ids are timestamp-prefixed);
|
|
11
|
+
* - exact dir-name match wins;
|
|
12
|
+
* - else unique substring/suffix match (so `8c44` finds the full id);
|
|
13
|
+
* - multiple substring matches → ambiguous (return candidates); none → no-match.
|
|
14
|
+
*/
|
|
15
|
+
export function matchRunId(ids, arg) {
|
|
16
|
+
if (ids.length === 0)
|
|
17
|
+
return { ok: false, kind: 'none' };
|
|
18
|
+
const sorted = [...ids].sort();
|
|
19
|
+
if (!arg)
|
|
20
|
+
return { ok: true, runId: sorted[sorted.length - 1] };
|
|
21
|
+
if (ids.includes(arg))
|
|
22
|
+
return { ok: true, runId: arg };
|
|
23
|
+
const candidates = sorted.filter((id) => id.includes(arg));
|
|
24
|
+
if (candidates.length === 1)
|
|
25
|
+
return { ok: true, runId: candidates[0] };
|
|
26
|
+
if (candidates.length > 1)
|
|
27
|
+
return { ok: false, kind: 'ambiguous', arg, candidates };
|
|
28
|
+
return { ok: false, kind: 'no-match', arg };
|
|
29
|
+
}
|
|
30
|
+
/** List stored run ids (directory names under `<root>/runs/`). Empty if the dir doesn't exist. */
|
|
31
|
+
export async function listRuns(root = '.aiki') {
|
|
32
|
+
try {
|
|
33
|
+
const entries = await readdir(join(root, 'runs'), { withFileTypes: true });
|
|
34
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Resolve a run-id arg to a concrete run directory + id (listRuns + matchRunId). */
|
|
41
|
+
export async function resolveRunId(arg, root = '.aiki') {
|
|
42
|
+
return matchRunId(await listRuns(root), arg);
|
|
43
|
+
}
|
|
44
|
+
export function runDir(runId, root = '.aiki') {
|
|
45
|
+
return join(root, 'runs', runId);
|
|
46
|
+
}
|
|
47
|
+
/** Read a run's final report, or null if it was never written (aborted/partial run). */
|
|
48
|
+
export async function readFinalReport(dir) {
|
|
49
|
+
try {
|
|
50
|
+
return await readFile(join(dir, 'final-report.md'), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Parse a JSON artifact from a run dir, or null if absent/unparseable. */
|
|
57
|
+
export async function readJsonArtifact(dir, name) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(await readFile(join(dir, name), 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Recursively list every artifact file in a run dir as sorted relative paths (for `show --raw`). */
|
|
66
|
+
export async function listArtifacts(dir) {
|
|
67
|
+
const out = [];
|
|
68
|
+
async function walk(d) {
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
const full = join(d, e.name);
|
|
78
|
+
if (e.isDirectory())
|
|
79
|
+
await walk(full);
|
|
80
|
+
else
|
|
81
|
+
out.push(relative(dir, full));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await walk(dir);
|
|
85
|
+
return out.sort();
|
|
86
|
+
}
|
|
87
|
+
/** True if a run directory exists on disk. */
|
|
88
|
+
export async function runExists(dir) {
|
|
89
|
+
try {
|
|
90
|
+
return (await stat(dir)).isDirectory();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Artifact writer for a single run's `.aiki/runs/<id>/` folder (§15).
|
|
2
|
+
//
|
|
3
|
+
// Guarantees (§14, §24 T4 acceptance):
|
|
4
|
+
// - Ordered writes: numbered stage artifacts must be written in forward order. A write for a
|
|
5
|
+
// stage earlier than the furthest-reached stage is refused (`OutOfOrderWriteError`). This is
|
|
6
|
+
// §14's "the artifact writer refuses out-of-order writes".
|
|
7
|
+
// - Immutable artifacts: a stage file, once written, cannot be rewritten (crash-forensics
|
|
8
|
+
// integrity). `meta.json` is the sole exception — it is finalized/updated (§16 aborted:true).
|
|
9
|
+
// - Crash-safe writes: every file is written to `<path>.tmp` then atomically `rename`d into
|
|
10
|
+
// place, so a crash mid-write never leaves a truncated/invalid artifact on disk.
|
|
11
|
+
// - Schema boundary: artifacts with a core schema (§14) are zod-validated BEFORE hitting disk;
|
|
12
|
+
// an invalid payload throws and writes nothing.
|
|
13
|
+
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { ActionPlan, DisagreementMap, IntentContract, JudgeReport, ReviewMap, RoleOutput, RunBrief, RunMeta, VerificationSet } from '../schemas/index.js';
|
|
16
|
+
export class OutOfOrderWriteError extends Error {
|
|
17
|
+
constructor(slot, ord, maxOrd) {
|
|
18
|
+
super(`out-of-order write: "${slot}" (stage ${ord}) after stage ${maxOrd} already written`);
|
|
19
|
+
this.name = 'OutOfOrderWriteError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class DuplicateWriteError extends Error {
|
|
23
|
+
constructor(relPath) {
|
|
24
|
+
super(`artifact already written (immutable): ${relPath}`);
|
|
25
|
+
this.name = 'DuplicateWriteError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** JSON stage slots. Composites without a T4 core schema (misunderstanding-guard, drift, claims)
|
|
29
|
+
* are written as-is; their schemas land with S2/S5/S6 (T5–T6). */
|
|
30
|
+
const JSON_SLOTS = {
|
|
31
|
+
'run-brief': { ord: 0.5, path: '00b-run-brief.json', schema: RunBrief },
|
|
32
|
+
'intent-contract': { ord: 1, path: '01-intent-contract.json', schema: IntentContract },
|
|
33
|
+
'misunderstanding-guard': { ord: 2, path: '02-misunderstanding-guard.json', schema: null },
|
|
34
|
+
'drift-report': { ord: 5, path: '05-drift-report.json', schema: null },
|
|
35
|
+
claims: { ord: 6, path: '06-claims.json', schema: null },
|
|
36
|
+
'disagreement-map': { ord: 7, path: '07-disagreement-map.json', schema: DisagreementMap },
|
|
37
|
+
// code-review's stage-7 artifact (ord 7, distinct path). A run writes one of {disagreement-map,
|
|
38
|
+
// review-map} depending on its workflow, so the shared ord never collides within a run.
|
|
39
|
+
'review-map': { ord: 7, path: '07-review-map.json', schema: ReviewMap },
|
|
40
|
+
verifications: { ord: 8, path: '08-verifications.json', schema: VerificationSet },
|
|
41
|
+
'judge-report': { ord: 9, path: '09-judge-report.json', schema: JudgeReport },
|
|
42
|
+
'action-plan': { ord: 9.5, path: '09b-action-plan.json', schema: ActionPlan },
|
|
43
|
+
};
|
|
44
|
+
/** Text (markdown) stage slots (§15). */
|
|
45
|
+
const TEXT_SLOTS = {
|
|
46
|
+
original: { ord: 0, path: '00-original.md' },
|
|
47
|
+
'final-report': { ord: 10, path: 'final-report.md' },
|
|
48
|
+
};
|
|
49
|
+
const PROMPTS_DIR_ORD = 3; // 03-prompts/
|
|
50
|
+
const ROLE_OUTPUTS_DIR_ORD = 4; // 04-role-outputs/
|
|
51
|
+
/**
|
|
52
|
+
* Writes one run's artifacts under `<root>/runs/<runId>/`. One instance per run; not concurrency
|
|
53
|
+
* safe for the same run (a run is single-threaded through its stages). `root` defaults to `.aiki`.
|
|
54
|
+
*/
|
|
55
|
+
export class RunWriter {
|
|
56
|
+
runId;
|
|
57
|
+
dir;
|
|
58
|
+
maxOrd = -1;
|
|
59
|
+
written = new Set(); // relPaths of immutable artifacts already on disk
|
|
60
|
+
constructor(runId, root = '.aiki') {
|
|
61
|
+
this.runId = runId;
|
|
62
|
+
this.dir = join(root, 'runs', runId);
|
|
63
|
+
}
|
|
64
|
+
/** Create the run directory. Idempotent. */
|
|
65
|
+
async init() {
|
|
66
|
+
await mkdir(this.dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
// ── ordered stage artifacts ───────────────────────────────────────────────
|
|
69
|
+
/** Write a JSON stage artifact. Validates against its core schema when one exists (§14). */
|
|
70
|
+
async writeJson(slot, data) {
|
|
71
|
+
const def = JSON_SLOTS[slot];
|
|
72
|
+
const payload = def.schema ? def.schema.parse(data) : data; // throws on invalid → nothing written
|
|
73
|
+
this.reserve(def.ord, def.path);
|
|
74
|
+
return this.atomicWrite(def.path, JSON.stringify(payload, null, 2));
|
|
75
|
+
}
|
|
76
|
+
/** Write a markdown stage artifact (00-original.md / final-report.md). */
|
|
77
|
+
async writeText(slot, text) {
|
|
78
|
+
const def = TEXT_SLOTS[slot];
|
|
79
|
+
this.reserve(def.ord, def.path);
|
|
80
|
+
return this.atomicWrite(def.path, text);
|
|
81
|
+
}
|
|
82
|
+
/** Write the exact final prompt sent to a provider for a stage → 03-prompts/<name> (§15). */
|
|
83
|
+
async writePrompt(name, text) {
|
|
84
|
+
const rel = join('03-prompts', name);
|
|
85
|
+
this.reserve(PROMPTS_DIR_ORD, rel);
|
|
86
|
+
return this.atomicWrite(rel, text);
|
|
87
|
+
}
|
|
88
|
+
/** Write one validated S4 role output → 04-role-outputs/<name>.json. Validates RoleOutput (§14). */
|
|
89
|
+
async writeRoleOutput(name, data) {
|
|
90
|
+
const payload = RoleOutput.parse(data); // engine attaches the `workflow` discriminator first (S4)
|
|
91
|
+
const rel = join('04-role-outputs', name.endsWith('.json') ? name : `${name}.json`);
|
|
92
|
+
this.reserve(ROLE_OUTPUTS_DIR_ORD, rel);
|
|
93
|
+
return this.atomicWrite(rel, JSON.stringify(payload, null, 2));
|
|
94
|
+
}
|
|
95
|
+
// ── unordered artifacts ───────────────────────────────────────────────────
|
|
96
|
+
/** Copy an input verbatim → inputs/<name> (diff.patch, source docs). Unordered; overwritable. */
|
|
97
|
+
async writeInput(name, content) {
|
|
98
|
+
return this.atomicWrite(join('inputs', name), content);
|
|
99
|
+
}
|
|
100
|
+
/** Dump an untouched provider stdout/stderr → raw/<name> (e.g. s4-claude.out). Unordered. */
|
|
101
|
+
async writeRaw(name, content) {
|
|
102
|
+
return this.atomicWrite(join('raw', name), content);
|
|
103
|
+
}
|
|
104
|
+
/** Write/finalize meta.json (§15, §16). Validated against RunMeta; overwritable (updated at
|
|
105
|
+
* finalize / on abort). Not subject to stage ordering. */
|
|
106
|
+
async writeMeta(meta) {
|
|
107
|
+
const payload = RunMeta.parse(meta);
|
|
108
|
+
return this.atomicWrite('meta.json', JSON.stringify(payload, null, 2));
|
|
109
|
+
}
|
|
110
|
+
// ── internals ─────────────────────────────────────────────────────────────
|
|
111
|
+
/** Enforce forward-only ordering + one-shot immutability for stage artifacts. */
|
|
112
|
+
reserve(ord, relPath) {
|
|
113
|
+
if (ord < this.maxOrd)
|
|
114
|
+
throw new OutOfOrderWriteError(relPath, ord, this.maxOrd);
|
|
115
|
+
if (this.written.has(relPath))
|
|
116
|
+
throw new DuplicateWriteError(relPath);
|
|
117
|
+
this.maxOrd = Math.max(this.maxOrd, ord);
|
|
118
|
+
this.written.add(relPath);
|
|
119
|
+
}
|
|
120
|
+
/** Atomic write: temp file + rename, so a crash never leaves a partial artifact (§24 T4). */
|
|
121
|
+
async atomicWrite(relPath, content) {
|
|
122
|
+
const full = join(this.dir, relPath);
|
|
123
|
+
await mkdir(dirname(full), { recursive: true });
|
|
124
|
+
const tmp = `${full}.tmp`;
|
|
125
|
+
await writeFile(tmp, content, 'utf8');
|
|
126
|
+
await rename(tmp, full);
|
|
127
|
+
return full;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Global session registry (V6.3) — `~/.aiki/sessions.jsonl`, append-only, one JSON object per line.
|
|
2
|
+
// Records every run REGARDLESS of where it was launched, so `aiki sessions` can list them and
|
|
3
|
+
// `aiki resume` can locate a run that lives under a different project's `.aiki`. Status updates append
|
|
4
|
+
// a fresh full line; readers keep the last line per id (last-write-wins).
|
|
5
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { homeAikiRoot } from './paths.js';
|
|
9
|
+
export const SessionStatus = z.enum(['running', 'ok', 'failed', 'aborted']);
|
|
10
|
+
export const SessionEntry = z
|
|
11
|
+
.object({
|
|
12
|
+
id: z.string().min(1),
|
|
13
|
+
workflow: z.string(),
|
|
14
|
+
cwd: z.string(), // launch dir (code-review needs it as the reviewer repo root on resume)
|
|
15
|
+
runsRoot: z.string(), // absolute .aiki root the run lives under (repo vs ~/.aiki)
|
|
16
|
+
startedAt: z.string(), // ISO
|
|
17
|
+
status: SessionStatus,
|
|
18
|
+
resumedFrom: z.string().optional(),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
function registryPath() {
|
|
22
|
+
return join(homeAikiRoot(), 'sessions.jsonl');
|
|
23
|
+
}
|
|
24
|
+
export async function recordSession(entry) {
|
|
25
|
+
await mkdir(homeAikiRoot(), { recursive: true });
|
|
26
|
+
await appendFile(registryPath(), `${JSON.stringify(SessionEntry.parse(entry))}\n`);
|
|
27
|
+
}
|
|
28
|
+
/** All sessions, newest first, deduped by id (the last line for an id wins — that's its latest status). */
|
|
29
|
+
export async function readSessions() {
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = await readFile(registryPath(), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const byId = new Map();
|
|
38
|
+
for (const line of raw.split('\n')) {
|
|
39
|
+
if (!line.trim())
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
const e = SessionEntry.parse(JSON.parse(line));
|
|
43
|
+
byId.set(e.id, e); // last line for this id wins
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* skip a malformed/legacy line */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Newest first by startedAt.
|
|
50
|
+
return [...byId.values()].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
51
|
+
}
|
|
52
|
+
/** Re-append the entry with a new status (keeps the append-only file honest). No-op if id unknown. */
|
|
53
|
+
export async function updateSessionStatus(id, status) {
|
|
54
|
+
const cur = (await readSessions()).find((s) => s.id === id);
|
|
55
|
+
if (!cur)
|
|
56
|
+
return;
|
|
57
|
+
await recordSession({ ...cur, status });
|
|
58
|
+
}
|
|
59
|
+
/** Locate a session by exact id or a unique suffix/substring (mirrors resolveRunId's matching). */
|
|
60
|
+
export async function findSession(idArg) {
|
|
61
|
+
const all = await readSessions();
|
|
62
|
+
const exact = all.find((s) => s.id === idArg);
|
|
63
|
+
if (exact)
|
|
64
|
+
return exact;
|
|
65
|
+
const hits = all.filter((s) => s.id.includes(idArg));
|
|
66
|
+
if (hits.length === 1)
|
|
67
|
+
return hits[0];
|
|
68
|
+
if (hits.length > 1)
|
|
69
|
+
return { ambiguous: hits.map((s) => s.id) };
|
|
70
|
+
return null;
|
|
71
|
+
}
|