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.
Files changed (78) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/dist/bench/arms.js +104 -0
  5. package/dist/bench/harness.js +251 -0
  6. package/dist/bench/results.js +70 -0
  7. package/dist/bench/scoring/seeded-bugs.js +31 -0
  8. package/dist/cli/bench.js +50 -0
  9. package/dist/cli/config.js +51 -0
  10. package/dist/cli/doctor.js +115 -0
  11. package/dist/cli/index.js +129 -0
  12. package/dist/cli/models.js +51 -0
  13. package/dist/cli/providers.js +31 -0
  14. package/dist/cli/resolve.js +159 -0
  15. package/dist/cli/resume.js +94 -0
  16. package/dist/cli/run.js +155 -0
  17. package/dist/cli/sessions.js +35 -0
  18. package/dist/cli/show.js +73 -0
  19. package/dist/config/config.js +102 -0
  20. package/dist/config/smoke-cache.js +65 -0
  21. package/dist/council/open.js +26 -0
  22. package/dist/council/view.js +873 -0
  23. package/dist/orchestration/cluster.js +83 -0
  24. package/dist/orchestration/context.js +277 -0
  25. package/dist/orchestration/engine.js +92 -0
  26. package/dist/orchestration/git.js +133 -0
  27. package/dist/orchestration/jsonStage.js +32 -0
  28. package/dist/orchestration/skills.js +39 -0
  29. package/dist/orchestration/stages/cr-ladder.js +63 -0
  30. package/dist/orchestration/stages/cr-map.js +62 -0
  31. package/dist/orchestration/stages/cr-report.js +83 -0
  32. package/dist/orchestration/stages/cr-s4-review.js +69 -0
  33. package/dist/orchestration/stages/cr-s8-crossexam.js +104 -0
  34. package/dist/orchestration/stages/cr-s9-judge.js +89 -0
  35. package/dist/orchestration/stages/s0-grill.js +79 -0
  36. package/dist/orchestration/stages/s1-intent.js +25 -0
  37. package/dist/orchestration/stages/s10-render.js +198 -0
  38. package/dist/orchestration/stages/s2-misread.js +76 -0
  39. package/dist/orchestration/stages/s3-prompts.js +55 -0
  40. package/dist/orchestration/stages/s4-analyze.js +50 -0
  41. package/dist/orchestration/stages/s5-drift.js +40 -0
  42. package/dist/orchestration/stages/s6-claims.js +56 -0
  43. package/dist/orchestration/stages/s7-disagreement.js +134 -0
  44. package/dist/orchestration/stages/s8-verify.js +56 -0
  45. package/dist/orchestration/stages/s9-judge.js +152 -0
  46. package/dist/orchestration/stages/s9b-plan.js +192 -0
  47. package/dist/providers/adapter-core.js +131 -0
  48. package/dist/providers/adapters.js +9 -0
  49. package/dist/providers/agy.js +29 -0
  50. package/dist/providers/claude.js +56 -0
  51. package/dist/providers/codex.js +35 -0
  52. package/dist/providers/detect.js +21 -0
  53. package/dist/providers/probe.js +43 -0
  54. package/dist/providers/profiles.js +38 -0
  55. package/dist/providers/profiles.json +5 -0
  56. package/dist/providers/smoke.js +26 -0
  57. package/dist/providers/spawn.js +152 -0
  58. package/dist/providers/types.js +17 -0
  59. package/dist/schemas/index.js +374 -0
  60. package/dist/skills/.gitkeep +0 -0
  61. package/dist/skills/code-review/judge.md +23 -0
  62. package/dist/skills/code-review/reviewer.md +38 -0
  63. package/dist/skills/idea-refinement/analyst.md +45 -0
  64. package/dist/skills/idea-refinement/planner.md +25 -0
  65. package/dist/storage/feedback.js +111 -0
  66. package/dist/storage/paths.js +20 -0
  67. package/dist/storage/replay.js +0 -0
  68. package/dist/storage/runs-read.js +95 -0
  69. package/dist/storage/runs.js +129 -0
  70. package/dist/storage/sessions.js +71 -0
  71. package/dist/tui/app.js +444 -0
  72. package/dist/tui/format.js +27 -0
  73. package/dist/tui/index.js +8 -0
  74. package/dist/tui/smart-entry.js +106 -0
  75. package/dist/tui/timeline.js +91 -0
  76. package/dist/workflows/code-review.js +76 -0
  77. package/dist/workflows/idea-refinement.js +105 -0
  78. package/package.json +64 -0
@@ -0,0 +1,21 @@
1
+ import { runCommand } from './spawn.js';
2
+ const DETECT_TIMEOUT_MS = 5000;
3
+ const INSTALL_HINT = {
4
+ claude: "not on PATH — install: npm i -g @anthropic-ai/claude-code",
5
+ codex: "not on PATH — install: npm i -g @openai/codex",
6
+ agy: "not on PATH — install the Antigravity CLI (agy) and log in",
7
+ };
8
+ /** Pull a semver-ish token out of a `--version` line (formats vary per CLI). */
9
+ export function parseVersion(stdout) {
10
+ const m = stdout.match(/\d+\.\d+\.\d+[\w.-]*/);
11
+ return m ? m[0] : undefined;
12
+ }
13
+ /** Detection: PATH lookup + `--version`, 5s timeout, no model calls (§8). */
14
+ export async function detect(id, run = runCommand) {
15
+ const r = await run(id, ['--version'], DETECT_TIMEOUT_MS);
16
+ if (r.notFound) {
17
+ return { id, status: 'NOT_INSTALLED', hint: INSTALL_HINT[id] };
18
+ }
19
+ const version = parseVersion(r.stdout) ?? parseVersion(r.stderr);
20
+ return { id, status: 'READY', version };
21
+ }
@@ -0,0 +1,43 @@
1
+ import { captureFull } from './spawn.js';
2
+ const PROBE_TIMEOUT_MS = 5000;
3
+ // Which binary + args to ask for --help, per provider (codex flags live under `exec`).
4
+ const HELP_INVOCATION = {
5
+ claude: { bin: 'claude', args: ['--help'] },
6
+ codex: { bin: 'codex', args: ['exec', '--help'] },
7
+ agy: { bin: 'agy', args: ['--help'] },
8
+ };
9
+ /**
10
+ * Parse a --help dump into a FlagProfile. Pure — the seam T2 tests directly.
11
+ * Flag names verified against installed CLIs at build time (see docs/PROVIDER_NOTES.md).
12
+ */
13
+ export function parseFlagProfile(id, help) {
14
+ const has = (re) => re.test(help);
15
+ switch (id) {
16
+ case 'claude':
17
+ return {
18
+ id,
19
+ jsonOutput: has(/--output-format/),
20
+ readOnlyFlag: has(/--permission-mode/) ? 'plan' : 'none',
21
+ };
22
+ case 'codex':
23
+ return {
24
+ id,
25
+ jsonOutput: has(/--json\b/),
26
+ readOnlyFlag: has(/--sandbox/) ? 'sandbox' : 'none',
27
+ };
28
+ case 'agy':
29
+ // agy has no JSON-output flag; `-p` returns raw text (the JSON we ask for) → §14 extraction.
30
+ // Read-only is best-effort via `--sandbox` (terminal restrictions); see docs/PROVIDER_NOTES.md.
31
+ return {
32
+ id,
33
+ jsonOutput: false,
34
+ readOnlyFlag: has(/--sandbox/) ? 'sandbox' : 'none',
35
+ };
36
+ }
37
+ }
38
+ /** Flag probe: `<bin> --help`, regex-match §7.3 flags, no model calls (§8). */
39
+ export async function probeFlags(id, capture = captureFull) {
40
+ const { bin, args } = HELP_INVOCATION[id];
41
+ const help = await capture(id, bin, args, PROBE_TIMEOUT_MS);
42
+ return parseFlagProfile(id, help);
43
+ }
@@ -0,0 +1,38 @@
1
+ // Static capability profiles (§7.4), merged with live detection + flag probe into the
2
+ // machine-readable profile that `aiki providers --json` prints (§5).
3
+ //
4
+ // Ordinals (1–3) are hand-maintained and exist ONLY to drive default role assignment (§10).
5
+ // They are NOT a benchmark output — do not build dynamic scoring in v1 (§7.4).
6
+ //
7
+ // CAVEAT (revisit at T5, per .agent/STATE.md): the plan's §7.4 table describes the OLD `gemini`
8
+ // CLI. Our third provider is `agy` (Antigravity / Gemini 3.1 Pro) — strong AND quota-metered,
9
+ // not the old free-tier gemini. We kept §7.4's ordinals here to avoid silently changing default
10
+ // role assignment before T5, but corrected `cost` to `quota-metered` (using `free-tier-generous`
11
+ // would contradict a decided fact). The ordinals + their role impact are re-decided at T5.
12
+ import { createRequire } from 'node:module';
13
+ import { DISPLAY_NAME, PROVIDER_IDS } from './types.js';
14
+ import { detect } from './detect.js';
15
+ import { probeFlags } from './probe.js';
16
+ // Read the JSON via require: keeps `rootDir` = src clean (no dist copy step needed for an import
17
+ // assertion) and works under NodeNext without pulling the file into the TS program.
18
+ const require = createRequire(import.meta.url);
19
+ const STATIC_PROFILES = require('./profiles.json');
20
+ /**
21
+ * Resolve every provider's machine profile: detection (PATH + --version) and flag probe (--help),
22
+ * merged with the static capability ordinals. No model calls (§8) — this is fast and safe.
23
+ */
24
+ export async function resolveProfiles() {
25
+ return Promise.all(PROVIDER_IDS.map(async (id) => {
26
+ const det = await detect(id);
27
+ const installed = det.status === 'READY';
28
+ const flags = installed ? await probeFlags(id) : null;
29
+ return {
30
+ id,
31
+ displayName: DISPLAY_NAME[id],
32
+ installed,
33
+ version: det.version ?? null,
34
+ flags: flags ? { jsonOutput: flags.jsonOutput, readOnlyFlag: flags.readOnlyFlag } : null,
35
+ capability: STATIC_PROFILES[id],
36
+ };
37
+ }));
38
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "claude": { "reasoning": 3, "codeNav": 3, "jsonReliability": 3, "cost": "quota-metered", "contextExplore": 3 },
3
+ "codex": { "reasoning": 2, "codeNav": 3, "jsonReliability": 2, "cost": "quota-metered", "contextExplore": 2 },
4
+ "agy": { "reasoning": 2, "codeNav": 2, "jsonReliability": 2, "cost": "quota-metered", "contextExplore": 2 }
5
+ }
@@ -0,0 +1,26 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { ADAPTERS } from './adapters.js';
3
+ const SMOKE_TIMEOUT_MS = 60_000;
4
+ function nonce() {
5
+ return randomBytes(4).toString('hex'); // 8 hex chars
6
+ }
7
+ /**
8
+ * One cheap model call per provider (§8): ask it to echo a random nonce as JSON.
9
+ * Pass = process exits ok within timeout AND extracted JSON echoes the nonce.
10
+ */
11
+ export async function smokeTest(id, flags, deps = {}) {
12
+ const adapter = ADAPTERS[id];
13
+ const n = nonce();
14
+ const prompt = `Reply with ONLY this JSON and nothing else: {"ok": true, "echo": "${n}"}`;
15
+ const res = await adapter.run({ prompt, cwd: process.cwd(), timeoutMs: SMOKE_TIMEOUT_MS, expectJson: true, readOnly: true }, flags, deps);
16
+ if (!res.ok) {
17
+ return { ok: false, error: res.error, nonce: n, durationMs: res.durationMs, detail: res.stderrTail.slice(-160) };
18
+ }
19
+ const echoed = res.json?.echo;
20
+ return {
21
+ ok: echoed === n,
22
+ nonce: n,
23
+ echoed: typeof echoed === 'string' ? echoed : undefined,
24
+ durationMs: res.durationMs,
25
+ };
26
+ }
@@ -0,0 +1,152 @@
1
+ import { execa } from 'execa';
2
+ import { spawn } from 'node:child_process';
3
+ import { openSync, closeSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ const STDERR_TAIL_CAP = 8000;
7
+ let seq = 0;
8
+ /**
9
+ * Real process runner backing detection and flag probes (metadata calls only).
10
+ * `shell: false` (execa default) — argv array, no shell interpolation (§7.2 injection safety).
11
+ * Adapter run() in T2 adds env filtering, process-tree kill, and retry on top of this shape.
12
+ */
13
+ export const runCommand = async (bin, args, timeoutMs) => {
14
+ try {
15
+ const r = await execa(bin, args, { timeout: timeoutMs, reject: false });
16
+ return {
17
+ code: r.exitCode ?? null,
18
+ stdout: r.stdout ?? '',
19
+ stderr: r.stderr ?? '',
20
+ timedOut: r.timedOut ?? false,
21
+ notFound: false,
22
+ };
23
+ }
24
+ catch (e) {
25
+ const err = e;
26
+ return {
27
+ code: err.exitCode ?? null,
28
+ stdout: err.stdout ?? '',
29
+ stderr: err.stderr ?? err.message ?? '',
30
+ timedOut: err.timedOut ?? false,
31
+ notFound: err.code === 'ENOENT',
32
+ };
33
+ }
34
+ };
35
+ /**
36
+ * Capture a command's full output by redirecting the child's stdout/stderr straight
37
+ * to a temp file (true fd redirect, not a pipe). Some CLIs (claude) call process.exit()
38
+ * which truncates async *pipe* writes at ~8KB; execa's own pipe/`{file}` capture hits
39
+ * this. Inheriting a file fd lets the child write the full ~16KB --help. Fixed internal
40
+ * argv only — no user input reaches here.
41
+ */
42
+ export function captureFull(id, bin, args, timeoutMs) {
43
+ const path = join(tmpdir(), `aiki-probe-${id}-${process.pid}.txt`);
44
+ const fd = openSync(path, 'w');
45
+ return new Promise((resolve) => {
46
+ const child = spawn(bin, args, { stdio: ['ignore', fd, fd] });
47
+ const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
48
+ const done = () => {
49
+ clearTimeout(timer);
50
+ resolve();
51
+ };
52
+ child.on('close', done);
53
+ child.on('error', done); // ENOENT etc → empty file, probe reports drift
54
+ }).then(() => {
55
+ closeSync(fd);
56
+ try {
57
+ return readFileSync(path, 'utf8');
58
+ }
59
+ catch {
60
+ return '';
61
+ }
62
+ finally {
63
+ rmSync(path, { force: true });
64
+ }
65
+ });
66
+ }
67
+ /**
68
+ * Run a provider CLI and capture its full output for adapter run() (§7.2).
69
+ * - stdout → temp file via inherited fd (true redirect; avoids claude's 8KB pipe truncation).
70
+ * - stderr → pipe, kept as a tail (for error classification / stderrTail).
71
+ * - detached process group so a timeout kills the whole tree, not just the direct child.
72
+ * - env is supplied filtered by the caller (adapter-core strips /KEY|TOKEN|SECRET/i).
73
+ */
74
+ export const spawnCapture = (bin, args, { cwd, timeoutMs, env, signal }) => {
75
+ const started = Date.now();
76
+ const outPath = join(tmpdir(), `aiki-run-${process.pid}-${seq++}.out`);
77
+ const fd = openSync(outPath, 'w');
78
+ return new Promise((resolve) => {
79
+ let stderr = '';
80
+ let timedOut = false;
81
+ let notFound = false;
82
+ let settled = false;
83
+ const child = spawn(bin, args, { cwd, env, detached: true, stdio: ['ignore', fd, 'pipe'] });
84
+ // SIGKILL the whole detached process group. Shared by the timeout and the Ctrl+C abort (T8).
85
+ const killGroup = () => {
86
+ try {
87
+ if (child.pid)
88
+ process.kill(-child.pid, 'SIGKILL');
89
+ }
90
+ catch {
91
+ try {
92
+ child.kill('SIGKILL');
93
+ }
94
+ catch {
95
+ /* already gone */
96
+ }
97
+ }
98
+ };
99
+ const timer = setTimeout(() => {
100
+ timedOut = true;
101
+ killGroup();
102
+ }, timeoutMs);
103
+ // Ctrl+C: kill the in-flight child immediately so no orphaned metered call survives (§472, T8).
104
+ const onAbort = () => killGroup();
105
+ if (signal) {
106
+ if (signal.aborted)
107
+ killGroup();
108
+ else
109
+ signal.addEventListener('abort', onAbort, { once: true });
110
+ }
111
+ const finish = (code, signal2) => {
112
+ if (settled)
113
+ return;
114
+ settled = true;
115
+ clearTimeout(timer);
116
+ signal?.removeEventListener('abort', onAbort);
117
+ try {
118
+ closeSync(fd);
119
+ }
120
+ catch {
121
+ /* already closed */
122
+ }
123
+ let stdout = '';
124
+ try {
125
+ stdout = readFileSync(outPath, 'utf8');
126
+ }
127
+ catch {
128
+ /* empty */
129
+ }
130
+ rmSync(outPath, { force: true });
131
+ resolve({
132
+ code,
133
+ signal: signal2 ?? null,
134
+ stdout,
135
+ stderr: stderr.length > STDERR_TAIL_CAP ? stderr.slice(-STDERR_TAIL_CAP) : stderr,
136
+ timedOut,
137
+ notFound,
138
+ durationMs: Date.now() - started,
139
+ });
140
+ };
141
+ child.stderr?.on('data', (d) => {
142
+ stderr += d.toString();
143
+ if (stderr.length > STDERR_TAIL_CAP * 2)
144
+ stderr = stderr.slice(-STDERR_TAIL_CAP);
145
+ });
146
+ child.on('error', (e) => {
147
+ notFound = e.code === 'ENOENT';
148
+ finish(null, null);
149
+ });
150
+ child.on('close', (code, signal) => finish(code, signal));
151
+ });
152
+ };
@@ -0,0 +1,17 @@
1
+ // Provider layer types.
2
+ //
3
+ // NOTE: the third provider is `agy` (Antigravity CLI, runs Gemini 3.1 Pro). It replaces the
4
+ // discontinued `gemini` CLI referenced throughout the plan — wherever the plan says "gemini",
5
+ // it now means `agy`. See docs/PROVIDER_NOTES.md.
6
+ export const PROVIDER_IDS = ['claude', 'codex', 'agy'];
7
+ /**
8
+ * User-facing display names. Internally (ids, artifacts, meta.json, logs) we always use the
9
+ * true id — `agy` — for audit accuracy. The UI shows the familiar model name instead: users
10
+ * know "Gemini", not the Antigravity binary. Command/binary references (e.g. "run `agy`") must
11
+ * still use the real id.
12
+ */
13
+ export const DISPLAY_NAME = {
14
+ claude: 'Claude',
15
+ codex: 'Codex',
16
+ agy: 'Gemini',
17
+ };