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,63 @@
1
+ // V4 escalation ladder — deterministic coverage-hole detection (pure). See BENCHMARK.md amendment L1.
2
+ //
3
+ // The ladder hunts cheap first (agy + codex, = Arm E's tier 1) and escalates a Claude call ONLY when it
4
+ // is likely to matter: (a) a disputed finding → the Claude judge (Arm E already does this); (b) a COVERAGE
5
+ // HOLE → one targeted Claude review over the risky hunks. This file owns (b)'s trigger: a pure function
6
+ // over the diff + tier-1 findings, so it's fully unit-tested without any model call. RISK_DEFS are FROZEN
7
+ // by the L1 pre-registration — do not tune them without a new amendment.
8
+ import { parseDiffFiles } from '../git.js';
9
+ /** FROZEN for L1 (BENCHMARK.md amendment L1): risk classes + their globs / keywords / covering categories. */
10
+ export const RISK_DEFS = [
11
+ {
12
+ id: 'auth',
13
+ label: 'auth / access control',
14
+ categories: ['SECURITY'],
15
+ fileRe: /(auth|login|session|token|permission|acl|oauth|jwt|guard|middleware)/i,
16
+ keywordRe: /\b(authenticate|authoriz|password|jwt|bcrypt|session|cookie|csrf|isadmin|req\.user|\.role\b|permission)\b/i,
17
+ },
18
+ {
19
+ id: 'crypto',
20
+ label: 'cryptography',
21
+ categories: ['SECURITY'],
22
+ fileRe: /(crypto|encrypt|cipher|hash|sign)/i,
23
+ keywordRe: /\b(encrypt|decrypt|createhash|randombytes|createcipher|hmac|nonce|\biv\b|math\.random)\b/i,
24
+ },
25
+ {
26
+ id: 'payment',
27
+ label: 'payments / money',
28
+ categories: ['CORRECTNESS', 'SECURITY'],
29
+ fileRe: /(payment|billing|charge|invoice|checkout|stripe|order|price)/i,
30
+ keywordRe: /\b(charge|refund|amount|currency|\bprice\b|subtotal|\btotal\b|discount|\btax\b|balance)\b/i,
31
+ },
32
+ {
33
+ id: 'async',
34
+ label: 'async / concurrency',
35
+ categories: ['CONCURRENCY'],
36
+ fileRe: /(worker|queue|scheduler|\bjob\b|concurrent)/i,
37
+ keywordRe: /\b(async|await|promise\.all|promise\.race|settimeout|setinterval|mutex|\block\b|concurrent|parallel)\b/i,
38
+ },
39
+ ];
40
+ /**
41
+ * A risk the diff touches (by file glob or added-line keyword) that tier-1 flagged NOTHING of the right
42
+ * category inside → escalate a targeted hunt. Deterministic + pure (BENCHMARK.md L1 §"Coverage hole").
43
+ */
44
+ export function detectCoverageHoles(diff, findings) {
45
+ const files = parseDiffFiles(diff);
46
+ const added = diff
47
+ .split('\n')
48
+ .filter((l) => l.startsWith('+') && !l.startsWith('+++'))
49
+ .join('\n');
50
+ const holes = [];
51
+ for (const r of RISK_DEFS) {
52
+ const riskFiles = files.filter((f) => r.fileRe.test(f));
53
+ const triggered = riskFiles.length > 0 || r.keywordRe.test(added);
54
+ if (!triggered)
55
+ continue;
56
+ // Scope = the risk-glob files if any, else the whole diff (keyword-only trigger).
57
+ const scope = riskFiles.length ? riskFiles : files;
58
+ const covered = findings.some((f) => r.categories.includes(f.category) && scope.includes(f.file));
59
+ if (!covered)
60
+ holes.push({ risk: r.id, label: r.label, categories: r.categories, files: scope });
61
+ }
62
+ return holes;
63
+ }
@@ -0,0 +1,62 @@
1
+ // code-review disagreement map (§12.2, T10) — the deterministic analog of idea's S6/S7. Pure.
2
+ //
3
+ // Two consensus paths (grilled 2026-07-04):
4
+ // 1. §487 matcher — both reviewers INDEPENDENTLY flagged the same defect (same file + overlapping
5
+ // lines + same category). Line-anchored, so tractable where idea's prose consensus wasn't.
6
+ // 2. cross-exam CONFIRM — the other reviewer agreed a single-authored finding is a genuine defect.
7
+ // A cross-exam REFUTE → disputed (adjudicated by S9). UNCERTAIN / unexamined → single-reviewer.
8
+ // Findings are reindexed to stable global ids (G1..) because per-reviewer ids ("F1") collide.
9
+ const SEV_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
10
+ /** §487 matcher (BENCHMARK.md §3): same defect iff same file, overlapping lines, same category. Also
11
+ * the seeded-bug scorer's match (T11) — a seeded bug is just a `FindingLoc`. */
12
+ export function sameFinding(a, b) {
13
+ return a.file === b.file && a.category === b.category && a.line_start <= b.line_end && b.line_start <= a.line_end;
14
+ }
15
+ export function buildReviewMap(reviewers, cross) {
16
+ const consensus = [];
17
+ const disputed = [];
18
+ const single = [];
19
+ const [a, b] = reviewers;
20
+ const mergedA = new Set();
21
+ const mergedB = new Set();
22
+ // Path 1 — §487 independent-consensus merge.
23
+ if (a && b) {
24
+ for (const fa of a.findings) {
25
+ const match = b.findings.find((fb) => !mergedB.has(fb) && sameFinding(fa, fb));
26
+ if (!match)
27
+ continue;
28
+ mergedA.add(fa);
29
+ mergedB.add(match);
30
+ const rep = SEV_RANK[match.severity] < SEV_RANK[fa.severity] ? match : fa; // keep the higher-severity representative
31
+ consensus.push({ finding: rep, reviewers: [a.provider, b.provider], cross_verdict: 'NONE' });
32
+ }
33
+ }
34
+ // Path 2 — single-authored findings, classified by the OTHER reviewer's cross-exam verdict.
35
+ const classify = (author, skip) => {
36
+ for (const f of author.findings) {
37
+ if (skip.has(f))
38
+ continue;
39
+ const cv = cross.get(`${author.provider}/${f.id}`);
40
+ const anno = { finding: f, reviewers: [author.provider], cross_verdict: cv?.verdict ?? 'NONE' };
41
+ if (cv?.verdict === 'CONFIRM')
42
+ consensus.push(anno);
43
+ else if (cv?.verdict === 'REFUTE')
44
+ disputed.push({ ...anno, refutation: cv.note || cv.evidence });
45
+ else
46
+ single.push(anno); // UNCERTAIN or never examined
47
+ }
48
+ };
49
+ if (a)
50
+ classify(a, mergedA);
51
+ if (b)
52
+ classify(b, mergedB);
53
+ // Reindex to stable, unique global ids so S9 can reference disputed items and S10 can match them.
54
+ let n = 0;
55
+ const reindex = (arr) => arr.map((af) => ({ ...af, finding: { ...af.finding, id: `G${++n}` } }));
56
+ return {
57
+ consensus: reindex(consensus),
58
+ disputed: reindex(disputed),
59
+ single_reviewer: reindex(single),
60
+ per_reviewer: reviewers.map((r) => ({ provider: r.provider, raised: r.raised, kept: r.findings.length, dropped: r.dropped.length })),
61
+ };
62
+ }
@@ -0,0 +1,83 @@
1
+ // code-review S10 — report rendering (§12.2, T10). Pure → final-report.md, a decision brief for the
2
+ // reviewer: verdict → findings table (P0/P1 first) → disagreement map → per-reviewer stats → raw links.
3
+ //
4
+ // Confidence + false-positive exclusion are DERIVED here (one source of truth, §624), NOT taken from the
5
+ // judge: consensus→HIGH, single-reviewer→MEDIUM, disputed+UPHOLD/UNRESOLVED→LOW (kept),
6
+ // disputed+REJECT→false positive (excluded from the findings table, listed under Rejected).
7
+ import { DISPLAY_NAME } from '../../providers/types.js';
8
+ const SEV_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
9
+ const disp = (id) => DISPLAY_NAME[id];
10
+ const attrib = (ps) => ps.map(disp).join(' + ');
11
+ /** Pure: assign each finding its derived confidence + disposition from the map + judge rulings. */
12
+ export function scoreFindings(map, judge) {
13
+ const ruling = new Map(judge.adjudications.map((a) => [a.id, a.ruling]));
14
+ const out = [];
15
+ for (const af of map.consensus)
16
+ out.push({ finding: af.finding, reviewers: af.reviewers, confidence: 'HIGH', disposition: 'kept' });
17
+ for (const af of map.single_reviewer)
18
+ out.push({ finding: af.finding, reviewers: af.reviewers, confidence: 'MEDIUM', disposition: 'kept' });
19
+ for (const af of map.disputed) {
20
+ const r = ruling.get(af.finding.id) ?? 'UNRESOLVED';
21
+ out.push({ finding: af.finding, reviewers: af.reviewers, confidence: 'LOW', disposition: r === 'REJECT' ? 'rejected' : 'kept', ruling: r });
22
+ }
23
+ return out;
24
+ }
25
+ const bySeverity = (a, b) => SEV_RANK[a.finding.severity] - SEV_RANK[b.finding.severity];
26
+ export function renderReviewReport(ctx, map, judge) {
27
+ const scored = scoreFindings(map, judge);
28
+ const kept = scored.filter((s) => s.disposition === 'kept').sort(bySeverity);
29
+ const rejected = scored.filter((s) => s.disposition === 'rejected');
30
+ const flags = [...ctx.flags];
31
+ const L = [];
32
+ L.push(`# Code Review — ${ctx.runId}`, '');
33
+ L.push(`- Reviewers: ${map.per_reviewer.map((r) => disp(r.provider)).join(', ')} · Judge: ${disp(ctx.roles.judge)} · calls: ${ctx.calls.length}/${ctx.budget.limit}`);
34
+ if (flags.length)
35
+ L.push(`- ⚠ Flags: ${flags.join(', ')}`);
36
+ L.push('');
37
+ L.push('## Verdict', '', judge.verdict, '');
38
+ const p0p1 = kept.filter((s) => s.finding.severity === 'P0' || s.finding.severity === 'P1');
39
+ L.push(`## Findings (${kept.length} kept${p0p1.length ? `, ${p0p1.length} P0/P1` : ''})`, '');
40
+ if (kept.length) {
41
+ L.push('| Sev | Location | Conf | Category | Finding | Reviewers |', '|---|---|---|---|---|---|');
42
+ for (const s of kept) {
43
+ const f = s.finding;
44
+ L.push(`| ${f.severity} | ${f.file}:${f.line_start}-${f.line_end} | ${s.confidence} | ${f.category} | ${f.claim} | ${attrib(s.reviewers)} |`);
45
+ }
46
+ }
47
+ else {
48
+ L.push('_No defects held after review._');
49
+ }
50
+ L.push('');
51
+ L.push('## Disagreement map', '');
52
+ L.push(`**Consensus (both reviewers or cross-confirmed):** ${map.consensus.length}`);
53
+ L.push(`**Single-reviewer:** ${map.single_reviewer.length}`);
54
+ L.push(`**Disputed → adjudicated:** ${map.disputed.length}`);
55
+ for (const d of map.disputed) {
56
+ const r = judge.adjudications.find((a) => a.id === d.finding.id);
57
+ L.push(`- **${d.finding.id}** ${d.finding.file}:${d.finding.line_start}-${d.finding.line_end}${r ? ` → ${r.ruling}` : ''}: ${d.finding.claim}`);
58
+ if (d.refutation)
59
+ L.push(` - refutation: ${d.refutation}`);
60
+ if (r)
61
+ L.push(` - judge: ${r.reasoning}`);
62
+ }
63
+ L.push('');
64
+ if (rejected.length) {
65
+ L.push(`## Rejected (adjudicated false positives): ${rejected.length}`, '');
66
+ for (const s of rejected)
67
+ L.push(`- ${s.finding.file}:${s.finding.line_start}-${s.finding.line_end} — ${s.finding.claim}`);
68
+ L.push('');
69
+ }
70
+ L.push('## Per-reviewer stats', '', '| Reviewer | Raised | Kept (valid) | Dropped (bad file:line) |', '|---|---|---|---|');
71
+ for (const r of map.per_reviewer)
72
+ L.push(`| ${disp(r.provider)} | ${r.raised} | ${r.kept} | ${r.dropped} |`);
73
+ L.push('');
74
+ L.push('## Dissent', '');
75
+ for (const d of judge.dissent)
76
+ L.push(`- ${d}`);
77
+ L.push('', `_Confidence: ${judge.confidence_notes}_`, '');
78
+ L.push('## Raw artifacts', '', '- reviewer findings: `04-role-outputs/`', '- cross-exam: `08-verifications.json`', '- disagreement map: `07-review-map.json`', '- judge report: `09-judge-report.json`', '- diff: `inputs/diff.patch`', '');
79
+ return L.join('\n');
80
+ }
81
+ export async function s10ReviewRender(ctx, map, judge) {
82
+ await ctx.writer.writeText('final-report', renderReviewReport(ctx, map, judge));
83
+ }
@@ -0,0 +1,69 @@
1
+ // code-review S4 — parallel blind review (§12.2, T10). Each reviewer (claude + codex) independently
2
+ // reviews the diff at repo-root cwd and returns findings. Immediately after, a DETERMINISTIC file:line
3
+ // validator (§605) drops any finding whose file isn't in the diff / present at HEAD, or whose line
4
+ // range is out of bounds — BEFORE the cross-exam sees it. Dropping is per-finding (keep the valid ones,
5
+ // §grill 2026-07-04); a reviewer ending with 0 valid findings is legal, not a failure.
6
+ import { readFile } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { CodeReviewRoleOutputModel } from '../../schemas/index.js';
9
+ import { isFatal, StageError } from '../context.js';
10
+ import { jsonCall } from '../jsonStage.js';
11
+ /**
12
+ * Pure file:line validator (§12.2/§605). A finding is valid iff its file appears in the diff AND is
13
+ * present at HEAD (has a line count) AND 1 ≤ line_start ≤ line_end ≤ lineCount.
14
+ */
15
+ export function filterValidFindings(findings, diffFiles, lineCounts) {
16
+ const valid = [];
17
+ const dropped = [];
18
+ for (const f of findings) {
19
+ const lc = lineCounts.get(f.file);
20
+ const ok = diffFiles.has(f.file) && lc !== undefined && f.line_start >= 1 && f.line_end >= f.line_start && f.line_end <= lc;
21
+ (ok ? valid : dropped).push(f);
22
+ }
23
+ return { valid, dropped };
24
+ }
25
+ /** Line count per file at HEAD (reads the repo tree). A missing/unreadable file is simply absent. */
26
+ export async function countLines(repoRoot, files) {
27
+ const out = new Map();
28
+ await Promise.all(files.map(async (f) => {
29
+ try {
30
+ out.set(f, (await readFile(join(repoRoot, f), 'utf8')).split('\n').length);
31
+ }
32
+ catch {
33
+ /* file not at HEAD (e.g. deleted) → left out → findings on it are rejected */
34
+ }
35
+ }));
36
+ return out;
37
+ }
38
+ async function reviewOne(ctx, seat, prompt, diffSet, lineCounts) {
39
+ const model = await jsonCall(ctx, ctx.handle(seat), `S4-${seat}`, prompt, CodeReviewRoleOutputModel);
40
+ // Persist the RAW reviewer output (all findings) for forensics; downstream uses only the valid set.
41
+ await ctx.writer.writeRoleOutput(seat, { workflow: 'code-review', ...model });
42
+ const { valid, dropped } = filterValidFindings(model.findings, diffSet, lineCounts);
43
+ return { provider: seat, findings: valid, dropped, raised: model.findings.length };
44
+ }
45
+ export async function s4Review(ctx, prompt, diffFiles) {
46
+ const seats = ctx.roles.s4;
47
+ const lineCounts = await countLines(ctx.cwd, diffFiles); // ctx.cwd = repo root for code-review
48
+ const diffSet = new Set(diffFiles);
49
+ const settled = await Promise.allSettled(seats.map((seat) => reviewOne(ctx, seat, prompt, diffSet, lineCounts)));
50
+ const survivors = [];
51
+ const dropped = [];
52
+ for (let i = 0; i < settled.length; i++) {
53
+ const r = settled[i];
54
+ if (r.status === 'fulfilled')
55
+ survivors.push(r.value);
56
+ else if (isFatal(r.reason))
57
+ throw r.reason; // budget/deadline/abort → abort the run
58
+ else
59
+ dropped.push(`${seats[i]}:${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
60
+ }
61
+ if (survivors.length === 0) {
62
+ throw new StageError('S4', 'QUORUM', `no reviewers survived (dropped: ${dropped.join('; ')})`);
63
+ }
64
+ // Exactly one reviewer → no cross-exam is possible (nothing to examine against). The run still
65
+ // produces single-reviewer findings; flag the reduced diversity (§8).
66
+ if (survivors.length < 2)
67
+ ctx.addFlag('low_diversity');
68
+ return survivors;
69
+ }
@@ -0,0 +1,104 @@
1
+ // code-review S8 — mutual cross-exam (§12.2, T10; teeth added V1). Each reviewer receives the OTHER
2
+ // reviewer's findings ANONYMIZED ("the other reviewer") and rules CONFIRM / REFUTE / UNCERTAIN per
3
+ // finding, with its own evidence. The prompt forces an adversarial pass: rank weakest-first, actively
4
+ // try to refute the weakest with file:line evidence, REFUTE only with evidence else UNCERTAIN. A
5
+ // rubber stamp (all CONFIRM, no weakest-first justification) triggers ONE sharper re-ask (mirrors S9);
6
+ // if it still rubber-stamps, raise synthesis_suspect. Two exams (skip a side whose target has 0
7
+ // findings, §grill). Verdict is finding-centric: CONFIRM = genuine defect, REFUTE = false positive.
8
+ import { VerificationSet } from '../../schemas/index.js';
9
+ import { isFatal } from '../context.js';
10
+ import { jsonCall } from '../jsonStage.js';
11
+ const S8_PROMPT = `ROLE: Senior code reviewer performing a peer cross-examination. Below are findings from
12
+ ANOTHER reviewer (source withheld). Your job is to CHALLENGE them, not rubber-stamp them.
13
+
14
+ Do ALL of the following, in order:
15
+ 1. Rank the findings from WEAKEST (most likely a false positive) to strongest.
16
+ 2. Take the weakest finding — and any other you doubt — and actively try to REFUTE it: open the
17
+ referenced file:lines yourself and look for a concrete reason the reported defect is NOT real
18
+ (guarded upstream, unreachable path, misread line, wrong category, already-correct code).
19
+ 3. Decide each verdict:
20
+ - REFUTE only when you have concrete file:line evidence it is a false positive.
21
+ - If you doubt a finding but cannot prove it false, use UNCERTAIN and state the specific doubt.
22
+ - CONFIRM only a finding you genuinely could not weaken.
23
+
24
+ Output ONLY JSON:
25
+ {"verifications": [{"target_id": "<finding id>", "verdict": "CONFIRM|REFUTE|UNCERTAIN",
26
+ "evidence": "<your own file:line reasoning>", "note": "<≤2 sentences>"}]}
27
+ CONFIRM = a real defect worth reporting. REFUTE = a false positive / not a genuine issue. UNCERTAIN =
28
+ cannot determine. If — and ONLY if — every finding survives your attack and you CONFIRM them all, you
29
+ MUST set "all_confirmed_justification" naming the single weakest finding and the file:line reason it
30
+ still holds. JSON only, no prose outside it.
31
+ FINDINGS TO EXAMINE: {{FINDINGS_JSON}}`;
32
+ /** Anonymized view of a reviewer's findings for the examiner (no provider attribution, no self_confidence). */
33
+ function anonymize(author) {
34
+ return author.findings.map((f) => ({ id: f.id, file: f.file, line_start: f.line_start, line_end: f.line_end, severity: f.severity, category: f.category, claim: f.claim, evidence: f.evidence }));
35
+ }
36
+ async function callExam(ctx, examiner, label, prompt) {
37
+ try {
38
+ // Examiner runs at repo-root cwd (default) so it can investigate the code; it's a verified read-only reviewer.
39
+ return await jsonCall(ctx, ctx.handle(examiner), label, prompt, VerificationSet);
40
+ }
41
+ catch (e) {
42
+ if (isFatal(e))
43
+ throw e; // budget/deadline/abort → abort the run
44
+ return null; // examiner down / bad output → treat as "no cross-exam" (findings stay single-reviewer)
45
+ }
46
+ }
47
+ /** Rubber stamp = confirmed everything without the mandatory weakest-first justification. The teeth we
48
+ * want are a REFUTE/UNCERTAIN (a genuine pushback) OR, if all really do hold, the justification. */
49
+ function isRubberStamp(graded, vset) {
50
+ return graded.length > 0 && graded.every((v) => v.verdict === 'CONFIRM') && !vset.all_confirmed_justification;
51
+ }
52
+ /** One cross-exam: initial call, then — on a rubber stamp — one sharper re-ask (mirrors S9's retry).
53
+ * Returns the accepted verification set (the pushed-back re-ask if it improved, else the original) and
54
+ * whether it is STILL a rubber stamp so the caller can flag `synthesis_suspect`. */
55
+ async function examine(ctx, examiner, author) {
56
+ const prompt = S8_PROMPT.replace('{{FINDINGS_JSON}}', JSON.stringify(anonymize(author), null, 2));
57
+ const vset = await callExam(ctx, examiner, `S8-${examiner}`, prompt);
58
+ if (!vset)
59
+ return null;
60
+ const known = new Set(author.findings.map((f) => f.id));
61
+ const graded = vset.verifications.filter((v) => known.has(v.target_id));
62
+ if (!isRubberStamp(graded, vset))
63
+ return { vset, graded, rubberStamp: false };
64
+ // Rubber stamp → one sharper re-ask. Accept it only if it actually pushed back (a non-CONFIRM verdict)
65
+ // or supplied the missing weakest-first justification; otherwise keep the original and flag.
66
+ const fix = `${prompt}\n\n---\nYour previous cross-examination CONFIRMED every finding and gave no weakest-first ` +
67
+ `justification — that reads as a rubber stamp, not a peer cross-examination. Re-examine: rank the ` +
68
+ `findings weakest-first, open the file:lines of the single weakest, and either REFUTE it with concrete ` +
69
+ `evidence or mark it UNCERTAIN with the specific doubt. If every finding truly survives, set ` +
70
+ `"all_confirmed_justification" naming the weakest and the file:line reason it holds. Output ONLY the corrected JSON.`;
71
+ const retry = await callExam(ctx, examiner, `S8-${examiner}-repair`, fix);
72
+ if (retry) {
73
+ const rg = retry.verifications.filter((v) => known.has(v.target_id));
74
+ if (!isRubberStamp(rg, retry))
75
+ return { vset: retry, graded: rg, rubberStamp: false };
76
+ }
77
+ return { vset, graded, rubberStamp: true };
78
+ }
79
+ export async function s8CrossExam(ctx, reviewers) {
80
+ const byKey = new Map();
81
+ const all = [];
82
+ // Cross-exam is defined for exactly two reviewers examining each other. With <2 (a single survivor)
83
+ // there is nothing to cross-examine; write an empty artifact and leave every finding single-reviewer.
84
+ if (reviewers.length === 2) {
85
+ const [a, b] = reviewers;
86
+ for (const [examiner, author] of [[a, b], [b, a]]) {
87
+ if (author.findings.length === 0)
88
+ continue; // skip-empty (nothing to examine)
89
+ const result = await examine(ctx, examiner.provider, author);
90
+ if (!result)
91
+ continue;
92
+ const { graded, rubberStamp } = result;
93
+ // A rubber stamp that survived the sharper re-ask stays flagged (still no pushback, no justification).
94
+ if (rubberStamp)
95
+ ctx.addFlag('synthesis_suspect');
96
+ for (const v of graded) {
97
+ byKey.set(`${author.provider}/${v.target_id}`, { verdict: v.verdict, note: v.note, evidence: v.evidence, examiner: examiner.provider });
98
+ all.push({ ...v, target_id: `${author.provider}/${v.target_id}` }); // namespace for the artifact (ids collide across reviewers)
99
+ }
100
+ }
101
+ }
102
+ // NB: the 08-verifications artifact is written by the workflow AFTER 07-review-map (ascending order).
103
+ return { byKey, verifications: all };
104
+ }
@@ -0,0 +1,89 @@
1
+ // code-review S9 — judge adjudication (§12.2, T10). The judge (agy/Gemini — it authored NO finding, so
2
+ // it can adjudicate cleanly) rules on the DISPUTED findings only (those one reviewer flagged as a false
3
+ // positive in cross-exam). Ruling polarity is finding-centric here (NOT idea S9's attack-centric one):
4
+ // UPHOLD = genuine defect, keep it · REJECT = false positive, drop it · UNRESOLVED = undecided.
5
+ //
6
+ // SAFETY (grilled 2026-07-04): the judge runs with cwd = the RUN DIR, never the repo — it only sees
7
+ // findings text in its prompt, so agy's unverified --sandbox write-blocking can't touch the repo. Zero
8
+ // disputes → no judge call; synthesize a deterministic verdict.
9
+ import { JudgeReportModel } from '../../schemas/index.js';
10
+ import { isFatal } from '../context.js';
11
+ import { jsonCall } from '../jsonStage.js';
12
+ import { adjudicationScopeViolations } from './s9-judge.js';
13
+ import { loadSkill } from '../skills.js';
14
+ const S9_PROMPT = `ROLE: Judge on a code review. Two independent reviewers disagreed on the findings below:
15
+ one reviewer reported each as a defect; another flagged it as a likely FALSE POSITIVE (see "refutation").
16
+ For EACH disputed finding, rule from the evidence and refutation given (you have findings text only):
17
+ - UPHOLD = it IS a genuine defect (keep it),
18
+ - REJECT = it is a false positive (drop it),
19
+ - UNRESOLVED = genuinely undecided.{{SKILL}}
20
+ Output ONLY JSON matching the judge schema:
21
+ - adjudications: for EACH disputed id → {id, ruling: UPHOLD|REJECT|UNRESOLVED, reasoning ≤3 sentences, evidence_cited}.
22
+ - verdict: ≤80 words — overall assessment of the change (roughly how many real defects, worst severity).
23
+ - dissent: ≥1 item — the strongest argument against your own verdict. Empty dissent is invalid.
24
+ - confidence_notes: which findings you hold HIGH/MEDIUM/LOW and why.
25
+ DISPUTED FINDINGS: {{DISPUTED_JSON}}`;
26
+ /**
27
+ * Fill the judge template: {{DISPUTED_JSON}} → the disputes, {{SKILL}} → the judge playbook (or nothing).
28
+ * An empty skill collapses the slot, so the prompt is byte-for-byte the pre-skill baseline.
29
+ */
30
+ export function buildJudgePrompt(disputes, skill) {
31
+ return S9_PROMPT.replace('{{DISPUTED_JSON}}', JSON.stringify(disputes, null, 2)).replace('{{SKILL}}', skill ? `\n\n${skill}` : '');
32
+ }
33
+ export async function s9ReviewJudge(ctx, map) {
34
+ const disputeIds = map.disputed.map((d) => d.finding.id);
35
+ // No disputes → nothing to adjudicate; synthesize a verdict deterministically (save the call).
36
+ if (disputeIds.length === 0) {
37
+ const kept = map.consensus.length + map.single_reviewer.length;
38
+ const report = {
39
+ adjudications: [],
40
+ verdict: `${kept} finding(s) reported, none disputed by the reviewers.`,
41
+ dissent: ['(no disputed findings — nothing to contest)'],
42
+ confidence_notes: 'Consensus findings HIGH; single-reviewer findings MEDIUM.',
43
+ };
44
+ await ctx.writer.writeJson('judge-report', report);
45
+ return report;
46
+ }
47
+ const disputes = map.disputed.map((d) => ({
48
+ id: d.finding.id,
49
+ file: d.finding.file,
50
+ lines: `${d.finding.line_start}-${d.finding.line_end}`,
51
+ severity: d.finding.severity,
52
+ category: d.finding.category,
53
+ claim: d.finding.claim,
54
+ evidence: d.finding.evidence,
55
+ refutation: d.refutation ?? '',
56
+ }));
57
+ const basePrompt = buildJudgePrompt(disputes, loadSkill('code-review', 'judge'));
58
+ // Judge runs on cwd = run dir (NOT the repo) — sidesteps agy's unverified sandbox.
59
+ const judge = ctx.handle(ctx.roles.judge);
60
+ const opts = { cwd: ctx.writer.dir };
61
+ let report = await jsonCall(ctx, judge, 'S9', basePrompt, JudgeReportModel, opts);
62
+ // Anti-scope + mandatory-dissent guard → one targeted re-ask (mirrors idea S9).
63
+ let violations = adjudicationScopeViolations(report, disputeIds);
64
+ if (violations.length || report.dissent.length === 0) {
65
+ const fix = `${basePrompt}\n\n---\nYour previous output had problems:\n` +
66
+ (violations.length ? `- adjudications must reference ONLY these disputed ids [${disputeIds.join(', ')}]; not: ${violations.join(', ')}\n` : '') +
67
+ (report.dissent.length === 0 ? `- dissent must contain at least one item.\n` : '') +
68
+ `Output ONLY the corrected JSON.`;
69
+ try {
70
+ report = await jsonCall(ctx, judge, 'S9-repair', fix, JudgeReportModel, opts);
71
+ violations = adjudicationScopeViolations(report, disputeIds);
72
+ }
73
+ catch (e) {
74
+ if (isFatal(e))
75
+ throw e; // keep the first report on a non-fatal repair failure
76
+ }
77
+ }
78
+ const inScope = report.adjudications.filter((a) => new Set(disputeIds).has(a.id));
79
+ if (inScope.length !== report.adjudications.length)
80
+ ctx.addFlag('synthesis_suspect');
81
+ let dissent = report.dissent;
82
+ if (dissent.length === 0) {
83
+ ctx.addFlag('synthesis_suspect');
84
+ dissent = ['(none produced — flagged synthesis_suspect)'];
85
+ }
86
+ const final = { ...report, adjudications: inScope, dissent, recommendation: undefined, conditions: undefined };
87
+ await ctx.writer.writeJson('judge-report', final);
88
+ return final;
89
+ }
@@ -0,0 +1,79 @@
1
+ // S0 - contextual grill / intent preflight. One cheap front-loaded model call generates
2
+ // 3-4 context-specific questions; interactive surfaces answer them before the full council runs.
3
+ import { RunBrief, RunBriefDraft } from '../../schemas/index.js';
4
+ import { jsonCall } from '../jsonStage.js';
5
+ const S0_PROMPT = `You are the intent preflight analyst for aiki, a professional multi-model council.
6
+ Read the user's idea below. Do not evaluate the idea yet. Instead, produce a concise run brief and
7
+ exactly 3 or 4 context-specific questions that would materially improve the later council's verdict.
8
+
9
+ Output ONLY JSON matching this shape:
10
+ {
11
+ "subject": "<short subject>",
12
+ "decision_frame": "<what decision the user seems to need, or null>",
13
+ "evaluation_lens": "<lens the council should use, or null>",
14
+ "target_user": "<target user stated or implied, or null>",
15
+ "constraints": ["<explicit constraints>"],
16
+ "claims_to_test": ["<load-bearing claims worth testing>"],
17
+ "evidence_supplied": ["<evidence or proof already supplied>"],
18
+ "missing_axes": ["<important missing context>"],
19
+ "questions": [
20
+ {
21
+ "id": "Q1",
22
+ "axis": "decision_frame|evaluation_lens|target_user|success_bar|non_negotiables|risk_context|evidence|alternatives|scope",
23
+ "question": "<one direct question>",
24
+ "why_it_matters": "<one sentence>",
25
+ "suggested_answers": ["<2-5 short options>"]
26
+ }
27
+ ]
28
+ }
29
+
30
+ Rules:
31
+ - Ask only questions whose answers could change the analysis or verdict.
32
+ - Make the questions specific to this idea, not generic startup intake.
33
+ - Prefer decision frame, target user, success bar, constraints, evidence, and risk context.
34
+ - Do not ask more than 4 questions.
35
+
36
+ USER IDEA:
37
+ {{RAW_INPUT}}`;
38
+ export function defaultGrillAnswers(brief) {
39
+ return brief.questions.map((q) => ({
40
+ question_id: q.id,
41
+ answer: 'Use best judgment from the supplied prompt.',
42
+ source: 'default',
43
+ }));
44
+ }
45
+ function normalizeAnswers(brief, answers) {
46
+ const byQuestion = new Map((answers ?? []).map((a) => [a.question_id, a]));
47
+ return brief.questions.map((q) => {
48
+ const found = byQuestion.get(q.id);
49
+ const answer = found?.answer.trim();
50
+ if (found && answer)
51
+ return { question_id: q.id, answer, source: found.source };
52
+ return { question_id: q.id, answer: 'Use best judgment from the supplied prompt.', source: 'default' };
53
+ });
54
+ }
55
+ export async function s0Grill(ctx, rawInput) {
56
+ const analyst = ctx.handle(ctx.roles.analyst);
57
+ const draft = await jsonCall(ctx, analyst, 'S0', S0_PROMPT.replace('{{RAW_INPUT}}', rawInput), RunBriefDraft);
58
+ const answers = normalizeAnswers(draft, ctx.events?.grill ? await ctx.events.grill(draft) : defaultGrillAnswers(draft));
59
+ const brief = RunBrief.parse({ ...draft, answers });
60
+ await ctx.writer.writeJson('run-brief', brief);
61
+ return brief;
62
+ }
63
+ export function renderGrilledInput(rawInput, brief) {
64
+ const sections = [
65
+ `Subject: ${brief.subject}`,
66
+ brief.decision_frame ? `Decision frame: ${brief.decision_frame}` : null,
67
+ brief.evaluation_lens ? `Evaluation lens: ${brief.evaluation_lens}` : null,
68
+ brief.target_user ? `Target user: ${brief.target_user}` : null,
69
+ brief.constraints.length ? `Constraints: ${brief.constraints.join('; ')}` : null,
70
+ brief.claims_to_test.length ? `Claims to test: ${brief.claims_to_test.join('; ')}` : null,
71
+ brief.evidence_supplied.length ? `Evidence supplied: ${brief.evidence_supplied.join('; ')}` : null,
72
+ brief.missing_axes.length ? `Missing context: ${brief.missing_axes.join('; ')}` : null,
73
+ ].filter((line) => line !== null);
74
+ const answers = brief.questions.map((q) => {
75
+ const answer = brief.answers.find((a) => a.question_id === q.id);
76
+ return `- ${q.question}\n Answer: ${answer?.answer ?? 'Use best judgment from the supplied prompt.'}`;
77
+ });
78
+ return `${rawInput.trim()}\n\n---\nAiki intent preflight\n${sections.join('\n')}\n\nAnswers:\n${answers.join('\n')}\n`;
79
+ }
@@ -0,0 +1,25 @@
1
+ // S1 — intent contract (§9, §13). Analyst normalizes the raw request into a typed IntentContract.
2
+ // Failure handling (§9): invalid JSON → one repair retry (in jsonCall) → StageError('BAD_OUTPUT'),
3
+ // which aborts the run with a message.
4
+ import { IntentContract } from '../../schemas/index.js';
5
+ import { jsonCall } from '../jsonStage.js';
6
+ // §13 S1 prompt, verbatim.
7
+ const S1_PROMPT = `You are the intake analyst for a professional multi-model orchestration system.
8
+ Read the user's request below. Produce ONLY a JSON object matching this schema, nothing else:
9
+
10
+ {"task": "<one-paragraph normalized restatement>",
11
+ "task_type": "idea-refinement|code-review|other",
12
+ "constraints": ["<explicit constraints stated by the user>"],
13
+ "unknowns": ["<things the request leaves unspecified>"],
14
+ "success_criteria": ["<what a good final output must contain>"]}
15
+
16
+ Rules: do not answer the request. Do not add constraints the user did not state.
17
+ USER REQUEST:
18
+ {{RAW_INPUT}}`;
19
+ export async function s1Intent(ctx, rawInput) {
20
+ const analyst = ctx.handle(ctx.roles.analyst);
21
+ const prompt = S1_PROMPT.replace('{{RAW_INPUT}}', rawInput);
22
+ const contract = await jsonCall(ctx, analyst, 'S1', prompt, IntentContract);
23
+ await ctx.writer.writeJson('intent-contract', contract);
24
+ return contract;
25
+ }