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,56 @@
|
|
|
1
|
+
// S8 — verifier loop (§9, §13). The verifier (codex) independently checks each dispute before the
|
|
2
|
+
// judge sees it. Idea-refinement runs a SINGLE pass (the "max 2 iterations" cap is the code-review
|
|
3
|
+
// reviewer cross-exam, §313, landing at T10). Disputed items are ANONYMIZED — the verifier sees the
|
|
4
|
+
// claim + the argument(s) against it with NO provider labels (§313/§624 anti-self-preference), since
|
|
5
|
+
// codex authored some S4 claims. Zero disputes → skip the call entirely. A verifier failure is
|
|
6
|
+
// graceful: every item is marked UNCERTAIN ("unverified") and passed to the judge, never an abort (§259).
|
|
7
|
+
import { VerificationSet } from '../../schemas/index.js';
|
|
8
|
+
import { isFatal } from '../context.js';
|
|
9
|
+
import { jsonCall } from '../jsonStage.js';
|
|
10
|
+
const S8_PROMPT = `ROLE: Verifier. Below are disputed claims, each with the argument(s) against it, from
|
|
11
|
+
anonymous sources. For EACH item, independently judge whether the argument defeats the claim.
|
|
12
|
+
Output ONLY JSON:
|
|
13
|
+
{"verifications": [{"target_id": "<id>", "verdict": "CONFIRM|REFUTE|UNCERTAIN",
|
|
14
|
+
"evidence": "<your own independent reasoning>", "note": "<≤2 sentences>"}]}
|
|
15
|
+
CONFIRM = the argument holds (the claim is genuinely doubtful). REFUTE = the argument fails (the claim
|
|
16
|
+
stands). Rules: you MUST issue at least one REFUTE, or set "all_confirmed_justification" explaining why
|
|
17
|
+
every claim survives. JSON only, no prose outside it.
|
|
18
|
+
ITEMS: {{DISPUTED_ITEMS_JSON}}`;
|
|
19
|
+
export async function s8Verify(ctx, map) {
|
|
20
|
+
const disputes = map.contradictions;
|
|
21
|
+
if (disputes.length === 0) {
|
|
22
|
+
const empty = { verifications: [] };
|
|
23
|
+
await ctx.writer.writeJson('verifications', empty);
|
|
24
|
+
return empty;
|
|
25
|
+
}
|
|
26
|
+
// Anonymized disputed items: claim text + argument text only, no provider attribution.
|
|
27
|
+
const claimById = new Map();
|
|
28
|
+
for (const c of [...map.consensus, ...map.unique])
|
|
29
|
+
claimById.set(c.id, c.statement);
|
|
30
|
+
const items = disputes.map((d) => ({
|
|
31
|
+
id: d.id,
|
|
32
|
+
claim: d.claim_ids.map((cid) => claimById.get(cid) ?? cid).join(' / '),
|
|
33
|
+
arguments_against: d.attacks.map((a) => a.argument),
|
|
34
|
+
}));
|
|
35
|
+
const prompt = S8_PROMPT.replace('{{DISPUTED_ITEMS_JSON}}', JSON.stringify(items, null, 2));
|
|
36
|
+
try {
|
|
37
|
+
const vset = await jsonCall(ctx, ctx.handle(ctx.roles.verifier), 'S8', prompt, VerificationSet);
|
|
38
|
+
await ctx.writer.writeJson('verifications', vset);
|
|
39
|
+
return vset;
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
if (isFatal(e))
|
|
43
|
+
throw e; // budget/deadline/abort → abort the run
|
|
44
|
+
// Verifier down / bad output: mark every dispute unverified, pass to the judge as low-confidence.
|
|
45
|
+
const vset = {
|
|
46
|
+
verifications: disputes.map((d) => ({
|
|
47
|
+
target_id: d.id,
|
|
48
|
+
verdict: 'UNCERTAIN',
|
|
49
|
+
evidence: '(verifier unavailable — unverified)',
|
|
50
|
+
note: '',
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
await ctx.writer.writeJson('verifications', vset);
|
|
54
|
+
return vset;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// S9 — judge synthesis (§9, §13). The judge (claude) adjudicates DISPUTED items only; consensus is
|
|
2
|
+
// already settled (S7 grouping) and passes through untouched. Two guards beyond the schema:
|
|
3
|
+
// 1. Anti-blending (§260, the §602 acceptance test): the judge may reference ONLY disputed ids in
|
|
4
|
+
// `adjudications` — never a consensus id. `adjudicationScopeViolations` (pure) detects this; the
|
|
5
|
+
// stage re-asks once, then filters any still-invalid adjudications out and flags the run.
|
|
6
|
+
// 2. Mandatory dissent (§260): empty dissent → re-ask once → else flag `synthesis_suspect` and
|
|
7
|
+
// inject a placeholder so the report still renders (continue, not abort).
|
|
8
|
+
// Confidence is NOT taken from the judge here — the held/failed/unverified audit + HIGH/MED/LOW labels
|
|
9
|
+
// are derived deterministically at S10 (§624: confidence from cross-model confirmation, not self-report).
|
|
10
|
+
import { JudgeReportModel } from '../../schemas/index.js';
|
|
11
|
+
import { isFatal } from '../context.js';
|
|
12
|
+
import { jsonCall } from '../jsonStage.js';
|
|
13
|
+
const S9_PROMPT = `ROLE: Judge. You adjudicate ONLY the disputed items below. Consensus items are already
|
|
14
|
+
settled; do not restate, edit, re-litigate, or reference them in your adjudications.
|
|
15
|
+
Apply this rubric strictly: {{RUBRIC_JSON}}
|
|
16
|
+
|
|
17
|
+
You are the CHAIRMAN of the panel. Write for a decision-maker who did not see the debate — be clear,
|
|
18
|
+
specific, and professional. No hedging mush, no restating the question back.
|
|
19
|
+
|
|
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
|
+
UPHOLD = the argument defeats the claim; REJECT = the claim survives; UNRESOLVED = genuinely undecided.
|
|
23
|
+
- verdict: 2-5 sentences — your clear recommendation (proceed / proceed-if / don't) and the single most
|
|
24
|
+
important reason. Grounded ONLY in adjudicated + consensus claims.
|
|
25
|
+
- recommendation: one of PROCEED, PROCEED_WITH_CONDITIONS, PIVOT, STOP.
|
|
26
|
+
PIVOT = the core idea is unsound but an adjacent version may work. STOP = load-bearing assumptions failed
|
|
27
|
+
with no credible repair. PROCEED_WITH_CONDITIONS means the idea is viable only if specific checks pass.
|
|
28
|
+
- conditions: required ONLY for PROCEED_WITH_CONDITIONS; each condition must be checkable, not a vibe.
|
|
29
|
+
- key_points: 4-8 bullets — the reasoning a decision-maker needs, in plain language. Cover: what decided
|
|
30
|
+
it, the decisive trade-offs, where the analysts DISAGREED and whose side you took and why, and the one
|
|
31
|
+
thing that would most change the verdict. Each bullet a full standalone point, not a fragment.
|
|
32
|
+
- dissent: ≥1 item — the strongest argument AGAINST your own verdict. Empty dissent is invalid.
|
|
33
|
+
- confidence_notes: which conclusions are HIGH/MEDIUM/LOW and why.
|
|
34
|
+
DISPUTED ITEMS + VERIFICATION: {{DISPUTES_JSON}}
|
|
35
|
+
CONSENSUS (context only, read-only): {{CONSENSUS_JSON}}`;
|
|
36
|
+
/** Pure anti-blending validator: adjudication ids that are NOT disputed items (the judge trying to
|
|
37
|
+
* touch consensus). Empty = clean. This is the heart of the §602 "rejects consensus edits" test. */
|
|
38
|
+
export function adjudicationScopeViolations(report, disputeIds) {
|
|
39
|
+
const allowed = new Set(disputeIds);
|
|
40
|
+
return report.adjudications.map((a) => a.id).filter((id) => !allowed.has(id));
|
|
41
|
+
}
|
|
42
|
+
/** Idea S9 semantic guard: the model-facing schema permits missing recommendation fields so S9 can
|
|
43
|
+
* re-ask once instead of losing a usable judge report to one absent enum. */
|
|
44
|
+
export function recommendationIssues(report) {
|
|
45
|
+
const issues = [];
|
|
46
|
+
const hasConditions = (report.conditions?.length ?? 0) > 0;
|
|
47
|
+
if (!report.recommendation)
|
|
48
|
+
issues.push('recommendation is required');
|
|
49
|
+
if (report.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
|
|
50
|
+
issues.push('conditions are required for PROCEED_WITH_CONDITIONS');
|
|
51
|
+
}
|
|
52
|
+
if (hasConditions && report.recommendation !== 'PROCEED_WITH_CONDITIONS') {
|
|
53
|
+
issues.push('conditions are only valid with PROCEED_WITH_CONDITIONS');
|
|
54
|
+
}
|
|
55
|
+
return issues;
|
|
56
|
+
}
|
|
57
|
+
/** §272 2-provider demotion (pure). When the judge is also an S4 author (only happens with 2
|
|
58
|
+
* providers), any adjudication on a dispute whose contested claim was authored SOLELY by the judge's
|
|
59
|
+
* provider is forced to `UNRESOLVED` — the judge may not confirm its own claim; it stays for the
|
|
60
|
+
* human. A no-op with 3 providers, where the judge (claude) authored no S4 claim. */
|
|
61
|
+
export function demoteSelfAuthored(adjudications, map, judgeProvider) {
|
|
62
|
+
const claimProviders = new Map();
|
|
63
|
+
for (const c of [...map.consensus, ...map.unique])
|
|
64
|
+
claimProviders.set(c.id, c.providers);
|
|
65
|
+
const disputeClaims = new Map(map.contradictions.map((d) => [d.id, d.claim_ids]));
|
|
66
|
+
return adjudications.map((a) => {
|
|
67
|
+
const cids = disputeClaims.get(a.id) ?? [];
|
|
68
|
+
const soleJudge = cids.length > 0 && cids.every((cid) => {
|
|
69
|
+
const ps = claimProviders.get(cid) ?? [];
|
|
70
|
+
return ps.length === 1 && ps[0] === judgeProvider;
|
|
71
|
+
});
|
|
72
|
+
return soleJudge && a.ruling !== 'UNRESOLVED' ? { ...a, ruling: 'UNRESOLVED' } : a;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const sevRank = (s) => (s === 'HIGH' ? 0 : s === 'MED' ? 1 : 2);
|
|
76
|
+
function fallbackConditions(map, adjudications) {
|
|
77
|
+
const claimById = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c.statement]));
|
|
78
|
+
const rulingById = new Map(adjudications.map((a) => [a.id, a.ruling]));
|
|
79
|
+
const riskConditions = map.contradictions
|
|
80
|
+
.filter((d) => rulingById.get(d.id) === 'UPHOLD')
|
|
81
|
+
.sort((a, b) => Math.min(...a.attacks.map((x) => sevRank(x.severity))) - Math.min(...b.attacks.map((x) => sevRank(x.severity))))
|
|
82
|
+
.map((d) => {
|
|
83
|
+
const claim = d.claim_ids.map((id) => claimById.get(id) ?? id).join(' / ');
|
|
84
|
+
return `Proceed only if you can validate that ${claim}.`;
|
|
85
|
+
});
|
|
86
|
+
const blindSpotConditions = map.blind_spots.map((b) => `Proceed only after examining the ${b} gap.`);
|
|
87
|
+
return [...riskConditions, ...blindSpotConditions, 'Proceed only after one cheap test confirms the core user need.'].slice(0, 6);
|
|
88
|
+
}
|
|
89
|
+
export async function s9Judge(ctx, contract, map, verifications, rubric) {
|
|
90
|
+
const claimById = new Map();
|
|
91
|
+
for (const c of [...map.consensus, ...map.unique])
|
|
92
|
+
claimById.set(c.id, c.statement);
|
|
93
|
+
const verdictById = new Map(verifications.verifications.map((v) => [v.target_id, v.verdict]));
|
|
94
|
+
const disputes = map.contradictions.map((d) => ({
|
|
95
|
+
id: d.id,
|
|
96
|
+
claim: d.claim_ids.map((cid) => claimById.get(cid) ?? cid).join(' / '),
|
|
97
|
+
arguments_against: d.attacks.map((a) => a.argument),
|
|
98
|
+
verifier_verdict: verdictById.get(d.id) ?? 'UNVERIFIED',
|
|
99
|
+
}));
|
|
100
|
+
const consensus = map.consensus.map((c) => ({ id: c.id, statement: c.statement }));
|
|
101
|
+
const disputeIds = map.contradictions.map((d) => d.id);
|
|
102
|
+
const basePrompt = S9_PROMPT.replace('{{RUBRIC_JSON}}', JSON.stringify(rubric.map((r) => r.label)))
|
|
103
|
+
.replace('{{DISPUTES_JSON}}', JSON.stringify(disputes, null, 2))
|
|
104
|
+
.replace('{{CONSENSUS_JSON}}', JSON.stringify(consensus, null, 2))
|
|
105
|
+
// reference the contract task so the verdict stays anchored to what the user asked
|
|
106
|
+
.concat(`\nTASK: ${contract.task}`);
|
|
107
|
+
const judge = ctx.handle(ctx.roles.judge);
|
|
108
|
+
let report = await jsonCall(ctx, judge, 'S9', basePrompt, JudgeReportModel);
|
|
109
|
+
// Semantic guards beyond the schema → one targeted re-ask.
|
|
110
|
+
let violations = adjudicationScopeViolations(report, disputeIds);
|
|
111
|
+
let recIssues = recommendationIssues(report);
|
|
112
|
+
if (violations.length || report.dissent.length === 0 || recIssues.length) {
|
|
113
|
+
const fix = `${basePrompt}\n\n---\nYour previous output had problems:\n` +
|
|
114
|
+
(violations.length ? `- adjudications must reference ONLY these disputed ids [${disputeIds.join(', ')}]; not: ${violations.join(', ')}\n` : '') +
|
|
115
|
+
(report.dissent.length === 0 ? `- dissent must contain at least one item.\n` : '') +
|
|
116
|
+
(recIssues.length ? `- recommendation/conditions problem: ${recIssues.join('; ')}.\n` : '') +
|
|
117
|
+
`Output ONLY the corrected JSON.`;
|
|
118
|
+
try {
|
|
119
|
+
report = await jsonCall(ctx, judge, 'S9-repair', fix, JudgeReportModel);
|
|
120
|
+
violations = adjudicationScopeViolations(report, disputeIds);
|
|
121
|
+
recIssues = recommendationIssues(report);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (isFatal(e))
|
|
125
|
+
throw e; // keep the first report on a non-fatal repair failure
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Enforce after the re-ask: drop any still-out-of-scope adjudications; guarantee non-empty dissent.
|
|
129
|
+
const inScope = report.adjudications.filter((a) => new Set(disputeIds).has(a.id));
|
|
130
|
+
if (inScope.length !== report.adjudications.length)
|
|
131
|
+
ctx.addFlag('synthesis_suspect');
|
|
132
|
+
// §272: if the judge authored a contested claim (2-provider only), it can't confirm it → UNRESOLVED.
|
|
133
|
+
const adjudications = demoteSelfAuthored(inScope, map, ctx.roles.judge);
|
|
134
|
+
let dissent = report.dissent;
|
|
135
|
+
if (dissent.length === 0) {
|
|
136
|
+
ctx.addFlag('synthesis_suspect');
|
|
137
|
+
dissent = ['(none produced — flagged synthesis_suspect)'];
|
|
138
|
+
}
|
|
139
|
+
let recommendation = report.recommendation;
|
|
140
|
+
let conditions = report.conditions;
|
|
141
|
+
if (recIssues.length) {
|
|
142
|
+
ctx.addFlag('synthesis_suspect');
|
|
143
|
+
recommendation = 'PROCEED_WITH_CONDITIONS';
|
|
144
|
+
conditions = fallbackConditions(map, adjudications);
|
|
145
|
+
}
|
|
146
|
+
else if (recommendation !== 'PROCEED_WITH_CONDITIONS') {
|
|
147
|
+
conditions = undefined;
|
|
148
|
+
}
|
|
149
|
+
const final = { ...report, adjudications, dissent, recommendation, conditions };
|
|
150
|
+
await ctx.writer.writeJson('judge-report', final);
|
|
151
|
+
return final;
|
|
152
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// S9b — idea validation plan. This is the one report-v3 model call after the judge: it turns the
|
|
2
|
+
// adjudicated risks, blind spots, and open questions into anchored validation actions. Rendering stays
|
|
3
|
+
// deterministic; if the planner fails or produces unanchored actions, we write a flagged fallback plan.
|
|
4
|
+
import { ActionPlan } from '../../schemas/index.js';
|
|
5
|
+
import { BudgetExceeded, isFatal } from '../context.js';
|
|
6
|
+
import { jsonCall } from '../jsonStage.js';
|
|
7
|
+
import { loadSkill } from '../skills.js';
|
|
8
|
+
import { mergeOpenQuestions } from './s10-render.js';
|
|
9
|
+
const S9B_PROMPT = `ROLE: Validation planner. You write the next actions for a decision-maker after an
|
|
10
|
+
idea council has already debated and judged the idea. Do not write a build roadmap. Write only validation
|
|
11
|
+
actions that test unsettled risks, blind spots, or open questions. Cheapest decisive test first.{{SKILL}}
|
|
12
|
+
|
|
13
|
+
Output ONLY JSON matching the ActionPlan schema:
|
|
14
|
+
- actions: 1-7 ordered actions, each imperative and concrete.
|
|
15
|
+
- validates MUST anchor to one of:
|
|
16
|
+
- a dispute id from upheld_risks, e.g. "D3"
|
|
17
|
+
- a blind spot label as "blind:<label>"
|
|
18
|
+
- an open-question prefix as "Q:<question prefix>"
|
|
19
|
+
- why ties the action to the risk, blind spot, or question.
|
|
20
|
+
- kill_signal is the result that should stop or reshape the idea.
|
|
21
|
+
- sequencing_note explains why this order is cheapest and decisive.
|
|
22
|
+
|
|
23
|
+
CONTEXT: {{CONTEXT_JSON}}`;
|
|
24
|
+
function clip(s, n) {
|
|
25
|
+
const t = s.trim();
|
|
26
|
+
return t.length > n ? `${t.slice(0, n - 1).trimEnd()}...` : t;
|
|
27
|
+
}
|
|
28
|
+
function sevRank(s) {
|
|
29
|
+
return s === 'HIGH' ? 0 : s === 'MED' ? 1 : 2;
|
|
30
|
+
}
|
|
31
|
+
function norm(s) {
|
|
32
|
+
return s.trim().toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function questionAnchor(q) {
|
|
35
|
+
return `Q:${clip(q, 90)}`;
|
|
36
|
+
}
|
|
37
|
+
function blindAnchor(b) {
|
|
38
|
+
return `blind:${b}`;
|
|
39
|
+
}
|
|
40
|
+
function upheldRisks(map, judgeReport) {
|
|
41
|
+
const claimById = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c.statement]));
|
|
42
|
+
const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
|
|
43
|
+
return map.contradictions
|
|
44
|
+
.map((d) => {
|
|
45
|
+
const adj = rulingById.get(d.id);
|
|
46
|
+
if (adj?.ruling !== 'UPHOLD')
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
id: d.id,
|
|
50
|
+
assumption: d.claim_ids.map((id) => claimById.get(id) ?? id).join(' / '),
|
|
51
|
+
severity: [...d.attacks].sort((a, b) => sevRank(a.severity) - sevRank(b.severity))[0]?.severity ?? 'MED',
|
|
52
|
+
reasoning: adj.reasoning,
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
.filter((r) => r !== null)
|
|
56
|
+
.sort((a, b) => sevRank(a.severity) - sevRank(b.severity));
|
|
57
|
+
}
|
|
58
|
+
export function buildActionPlannerPrompt(input, skill) {
|
|
59
|
+
return S9B_PROMPT
|
|
60
|
+
.replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
|
|
61
|
+
.replace('{{CONTEXT_JSON}}', JSON.stringify(input, null, 2));
|
|
62
|
+
}
|
|
63
|
+
export function validAnchor(anchor, anchors) {
|
|
64
|
+
const a = anchor.trim();
|
|
65
|
+
if (anchors.disputeIds.includes(a))
|
|
66
|
+
return true;
|
|
67
|
+
if (a.toLowerCase().startsWith('blind:')) {
|
|
68
|
+
const label = norm(a.slice('blind:'.length));
|
|
69
|
+
return anchors.blindSpots.some((b) => {
|
|
70
|
+
const n = norm(b);
|
|
71
|
+
return n.startsWith(label) || label.startsWith(n);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (a.toLowerCase().startsWith('q:')) {
|
|
75
|
+
const prefix = norm(a.slice(2));
|
|
76
|
+
return anchors.openQuestions.some((q) => {
|
|
77
|
+
const n = norm(q);
|
|
78
|
+
return n.startsWith(prefix) || prefix.startsWith(n);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
export function anchoredActionPlan(plan, anchors) {
|
|
84
|
+
const actions = plan.actions
|
|
85
|
+
.filter((a) => validAnchor(a.validates, anchors))
|
|
86
|
+
.sort((a, b) => a.order - b.order)
|
|
87
|
+
.map((a, i) => ({ ...a, order: i + 1 }));
|
|
88
|
+
if (actions.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
return { actions, sequencing_note: plan.sequencing_note };
|
|
91
|
+
}
|
|
92
|
+
export function fallbackActionPlan(contract, map, judgeReport, openQuestions) {
|
|
93
|
+
const risks = upheldRisks(map, judgeReport);
|
|
94
|
+
const actions = [];
|
|
95
|
+
for (const r of risks) {
|
|
96
|
+
actions.push({
|
|
97
|
+
order: actions.length + 1,
|
|
98
|
+
action: `Test whether "${clip(r.assumption, 120)}" is true with the smallest realistic sample.`,
|
|
99
|
+
why: `The chair upheld this as a load-bearing ${r.severity} risk.`,
|
|
100
|
+
validates: r.id,
|
|
101
|
+
effort: 'S',
|
|
102
|
+
kill_signal: 'The assumption fails in the sample or only works outside the intended use case.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
for (const b of map.blind_spots) {
|
|
106
|
+
actions.push({
|
|
107
|
+
order: actions.length + 1,
|
|
108
|
+
action: `Answer the ${b} gap with one concrete evidence source.`,
|
|
109
|
+
why: 'No analyst examined this dimension enough to rely on it.',
|
|
110
|
+
validates: blindAnchor(b),
|
|
111
|
+
effort: 'S',
|
|
112
|
+
kill_signal: 'The answer exposes a hard blocker or makes the target user/use case incoherent.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const q of openQuestions) {
|
|
116
|
+
actions.push({
|
|
117
|
+
order: actions.length + 1,
|
|
118
|
+
action: `Resolve this question: ${clip(q, 140)}`,
|
|
119
|
+
why: 'The analysts identified it as an answer that could change the verdict.',
|
|
120
|
+
validates: questionAnchor(q),
|
|
121
|
+
effort: 'S',
|
|
122
|
+
kill_signal: 'The answer contradicts the value proposition or removes the target user.',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (actions.length === 0) {
|
|
126
|
+
const q = `What evidence would change the verdict for ${clip(contract.task, 80)}?`;
|
|
127
|
+
actions.push({
|
|
128
|
+
order: 1,
|
|
129
|
+
action: 'Run one cheap test of the core user need before investing more.',
|
|
130
|
+
why: 'The council produced no unsettled anchored item, so validate the core demand directly.',
|
|
131
|
+
validates: questionAnchor(q),
|
|
132
|
+
effort: 'S',
|
|
133
|
+
kill_signal: 'Target users do not recognize the problem or would not switch from existing alternatives.',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return ActionPlan.parse({
|
|
137
|
+
actions: actions.slice(0, 7).map((a, i) => ({ ...a, order: i + 1 })),
|
|
138
|
+
sequencing_note: 'Start with upheld risks, then blind spots, then open questions; stop when a kill signal fires.',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export async function s9bPlan(ctx, contract, seats, map, judgeReport) {
|
|
142
|
+
const openQuestions = mergeOpenQuestions(seats);
|
|
143
|
+
const risks = upheldRisks(map, judgeReport);
|
|
144
|
+
const anchors = {
|
|
145
|
+
disputeIds: risks.map((r) => r.id),
|
|
146
|
+
blindSpots: map.blind_spots,
|
|
147
|
+
openQuestions,
|
|
148
|
+
};
|
|
149
|
+
const fallback = async (flag) => {
|
|
150
|
+
ctx.addFlag(flag);
|
|
151
|
+
const plan = fallbackActionPlan(contract, map, judgeReport, openQuestions);
|
|
152
|
+
await ctx.writer.writeJson('action-plan', plan);
|
|
153
|
+
return plan;
|
|
154
|
+
};
|
|
155
|
+
if (ctx.budget.limit - ctx.budget.used < 2)
|
|
156
|
+
return fallback('plan_skipped');
|
|
157
|
+
const prompt = buildActionPlannerPrompt({
|
|
158
|
+
task: contract.task,
|
|
159
|
+
recommendation: judgeReport.recommendation,
|
|
160
|
+
conditions: judgeReport.conditions,
|
|
161
|
+
upheld_risks: risks,
|
|
162
|
+
blind_spots: map.blind_spots,
|
|
163
|
+
open_questions: openQuestions,
|
|
164
|
+
}, loadSkill('idea-refinement', 'planner'));
|
|
165
|
+
try {
|
|
166
|
+
const first = await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-plan', prompt, ActionPlan);
|
|
167
|
+
const anchored = anchoredActionPlan(first, anchors);
|
|
168
|
+
if (anchored) {
|
|
169
|
+
await ctx.writer.writeJson('action-plan', anchored);
|
|
170
|
+
return anchored;
|
|
171
|
+
}
|
|
172
|
+
if (ctx.budget.limit - ctx.budget.used < 1)
|
|
173
|
+
return fallback('plan_fallback');
|
|
174
|
+
const repair = `${prompt}\n\n---\nYour previous plan had no actions with valid anchors.\n` +
|
|
175
|
+
`Valid dispute ids: ${anchors.disputeIds.join(', ') || '(none)'}\n` +
|
|
176
|
+
`Valid blind spots: ${anchors.blindSpots.join(' | ') || '(none)'}\n` +
|
|
177
|
+
`Valid open questions: ${anchors.openQuestions.join(' | ') || '(none)'}\n` +
|
|
178
|
+
`Output ONLY corrected JSON with every action anchored to one of those values.`;
|
|
179
|
+
const repaired = await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-anchor-repair', repair, ActionPlan);
|
|
180
|
+
const repairedAnchored = anchoredActionPlan(repaired, anchors);
|
|
181
|
+
if (repairedAnchored) {
|
|
182
|
+
await ctx.writer.writeJson('action-plan', repairedAnchored);
|
|
183
|
+
return repairedAnchored;
|
|
184
|
+
}
|
|
185
|
+
return fallback('plan_fallback');
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
if (isFatal(e) && !(e instanceof BudgetExceeded))
|
|
189
|
+
throw e;
|
|
190
|
+
return fallback('plan_fallback');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawnCapture } from './spawn.js';
|
|
2
|
+
const CREDENTIAL_RE = /KEY|TOKEN|SECRET/i;
|
|
3
|
+
/** Inherit the user's env minus anything credential-looking (§7.2, §19 defense in depth). */
|
|
4
|
+
export function filterEnv(base = process.env) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (const [k, v] of Object.entries(base)) {
|
|
7
|
+
if (!CREDENTIAL_RE.test(k))
|
|
8
|
+
out[k] = v;
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
const AUTH_RE = /\b(login|log in|unauthorized|expired|authenticat|not authenticated|ineligible|unsupported client|no longer supported|sign in|please run)\b/i;
|
|
13
|
+
const QUOTA_RE = /\b(rate limit|quota|429|resource[_ ]exhausted|too many requests|usage limit|out of)\b/i;
|
|
14
|
+
/**
|
|
15
|
+
* Map a raw process result to a ProviderError, or 'OK' (§7.2 taxonomy). Order is deliberate.
|
|
16
|
+
* Exit 0 short-circuits to OK — we do NOT scan stderr on success. Some CLIs (codex) write a
|
|
17
|
+
* full session transcript (prompt + result) to stderr, so pattern-matching it on success would
|
|
18
|
+
* false-positive AUTH/QUOTA on innocent content. AUTH/QUOTA are failure modes: only relevant
|
|
19
|
+
* when the process did not exit cleanly.
|
|
20
|
+
*/
|
|
21
|
+
export function classify(raw) {
|
|
22
|
+
if (raw.notFound)
|
|
23
|
+
return 'NOT_FOUND';
|
|
24
|
+
if (raw.timedOut)
|
|
25
|
+
return 'TIMEOUT';
|
|
26
|
+
if (raw.code === 0)
|
|
27
|
+
return 'OK';
|
|
28
|
+
const err = raw.stderr ?? '';
|
|
29
|
+
if (AUTH_RE.test(err))
|
|
30
|
+
return 'AUTH';
|
|
31
|
+
if (QUOTA_RE.test(err))
|
|
32
|
+
return 'QUOTA';
|
|
33
|
+
return 'CRASH';
|
|
34
|
+
}
|
|
35
|
+
function tryParse(s) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(s);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** First balanced {...} object in text, ignoring braces inside strings. */
|
|
44
|
+
function firstBalancedObject(text) {
|
|
45
|
+
const start = text.indexOf('{');
|
|
46
|
+
if (start < 0)
|
|
47
|
+
return undefined;
|
|
48
|
+
let depth = 0;
|
|
49
|
+
let inStr = false;
|
|
50
|
+
let esc = false;
|
|
51
|
+
for (let i = start; i < text.length; i++) {
|
|
52
|
+
const c = text[i];
|
|
53
|
+
if (inStr) {
|
|
54
|
+
if (esc)
|
|
55
|
+
esc = false;
|
|
56
|
+
else if (c === '\\')
|
|
57
|
+
esc = true;
|
|
58
|
+
else if (c === '"')
|
|
59
|
+
inStr = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (c === '"')
|
|
63
|
+
inStr = true;
|
|
64
|
+
else if (c === '{')
|
|
65
|
+
depth++;
|
|
66
|
+
else if (c === '}') {
|
|
67
|
+
depth--;
|
|
68
|
+
if (depth === 0)
|
|
69
|
+
return text.slice(start, i + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* §14 JSON location within already-de-enveloped model text:
|
|
76
|
+
* whole-parse → fenced ```json block → first balanced {...}. Returns undefined if none parse.
|
|
77
|
+
*/
|
|
78
|
+
export function extractJson(text) {
|
|
79
|
+
const whole = tryParse(text.trim());
|
|
80
|
+
if (whole !== undefined)
|
|
81
|
+
return whole;
|
|
82
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
83
|
+
if (fence?.[1]) {
|
|
84
|
+
const p = tryParse(fence[1].trim());
|
|
85
|
+
if (p !== undefined)
|
|
86
|
+
return p;
|
|
87
|
+
}
|
|
88
|
+
const bal = firstBalancedObject(text);
|
|
89
|
+
if (bal) {
|
|
90
|
+
const p = tryParse(bal);
|
|
91
|
+
if (p !== undefined)
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Shared adapter runner: build argv → spawn (fd-capture) → classify → de-envelope → extract JSON.
|
|
98
|
+
* Exactly one retry, only for TIMEOUT | BAD_OUTPUT | CRASH (§7.2). AUTH/QUOTA/NOT_FOUND fail fast.
|
|
99
|
+
*/
|
|
100
|
+
export async function runAdapter(spec, req, flags, deps = {}) {
|
|
101
|
+
const spawnFn = deps.spawn ?? spawnCapture;
|
|
102
|
+
const attempt = async () => {
|
|
103
|
+
const args = spec.buildArgs(req, flags);
|
|
104
|
+
const raw = await spawnFn(spec.id, args, { cwd: req.cwd, timeoutMs: req.timeoutMs, env: filterEnv(), signal: req.signal });
|
|
105
|
+
const cls = classify(raw);
|
|
106
|
+
if (cls !== 'OK') {
|
|
107
|
+
return { ok: false, error: cls, stderrTail: raw.stderr, durationMs: raw.durationMs };
|
|
108
|
+
}
|
|
109
|
+
const envErr = spec.envelopeError?.(raw.stdout);
|
|
110
|
+
if (envErr) {
|
|
111
|
+
return { ok: false, error: envErr, stderrTail: raw.stderr, durationMs: raw.durationMs };
|
|
112
|
+
}
|
|
113
|
+
const text = spec.extractText(raw.stdout);
|
|
114
|
+
let json;
|
|
115
|
+
if (req.expectJson) {
|
|
116
|
+
json = extractJson(text);
|
|
117
|
+
if (json === undefined) {
|
|
118
|
+
return { ok: false, error: 'BAD_OUTPUT', stderrTail: raw.stderr, durationMs: raw.durationMs };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, text, json, durationMs: raw.durationMs, providerMeta: spec.meta?.(raw.stdout) };
|
|
122
|
+
};
|
|
123
|
+
const first = await attempt();
|
|
124
|
+
if (first.ok)
|
|
125
|
+
return first;
|
|
126
|
+
// Don't burn a retry (a fresh child) when the run is being aborted (Ctrl+C, T8).
|
|
127
|
+
if (!req.signal?.aborted && (first.error === 'TIMEOUT' || first.error === 'BAD_OUTPUT' || first.error === 'CRASH')) {
|
|
128
|
+
return attempt(); // exactly one retry
|
|
129
|
+
}
|
|
130
|
+
return first;
|
|
131
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { runAdapter } from './adapter-core.js';
|
|
2
|
+
/**
|
|
3
|
+
* Antigravity CLI (`agy`, runs Gemini 3.1 Pro) — replaces the discontinued gemini CLI.
|
|
4
|
+
* Invocation (verified live, T2):
|
|
5
|
+
* agy -p "<prompt>" [--sandbox]
|
|
6
|
+
* No JSON-output flag: `-p` prints the model's response as raw text, so when we ask for JSON
|
|
7
|
+
* the response *is* the JSON → §14 extraction parses it directly (no envelope to strip).
|
|
8
|
+
* Read-only is best-effort via `--sandbox` (terminal restrictions); write-blocking is
|
|
9
|
+
* UNVERIFIED — see docs/PROVIDER_NOTES.md, to be pinned down at T10 (code-review).
|
|
10
|
+
* NEVER pass --dangerously-skip-permissions (§19).
|
|
11
|
+
*/
|
|
12
|
+
const agySpec = {
|
|
13
|
+
id: 'agy',
|
|
14
|
+
buildArgs(req, flags) {
|
|
15
|
+
const args = ['-p', req.prompt];
|
|
16
|
+
if (req.readOnly !== false && flags.readOnlyFlag === 'sandbox')
|
|
17
|
+
args.push('--sandbox');
|
|
18
|
+
if (flags.model)
|
|
19
|
+
args.push('--model', flags.model); // V8: verified `agy --model <id>` (ids may contain spaces — one argv elem)
|
|
20
|
+
return args;
|
|
21
|
+
},
|
|
22
|
+
extractText(stdout) {
|
|
23
|
+
return stdout; // raw text; §14 extraction handles whole/fenced/balanced JSON
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export const agy = {
|
|
27
|
+
id: 'agy',
|
|
28
|
+
run: (req, flags, deps) => runAdapter(agySpec, req, flags, deps),
|
|
29
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { runAdapter } from './adapter-core.js';
|
|
2
|
+
function envelope(stdout) {
|
|
3
|
+
try {
|
|
4
|
+
const v = JSON.parse(stdout);
|
|
5
|
+
return v && typeof v === 'object' ? v : undefined;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Claude Code (`claude`). Invocation per §7.3:
|
|
13
|
+
* claude -p "<prompt>" [--output-format json] [--permission-mode plan]
|
|
14
|
+
* `--output-format json` returns an envelope: { type, subtype, is_error, result, session_id,
|
|
15
|
+
* total_cost_usd, usage, ... } — the model's text lives in `.result` (verified live, T2).
|
|
16
|
+
*/
|
|
17
|
+
const claudeSpec = {
|
|
18
|
+
id: 'claude',
|
|
19
|
+
buildArgs(req, flags) {
|
|
20
|
+
const args = ['-p', req.prompt];
|
|
21
|
+
if (req.expectJson && flags.jsonOutput)
|
|
22
|
+
args.push('--output-format', 'json');
|
|
23
|
+
if (req.readOnly !== false && flags.readOnlyFlag === 'plan')
|
|
24
|
+
args.push('--permission-mode', 'plan');
|
|
25
|
+
if (flags.model)
|
|
26
|
+
args.push('--model', flags.model); // V8: verified `claude --model <alias|name>`
|
|
27
|
+
return args;
|
|
28
|
+
},
|
|
29
|
+
extractText(stdout) {
|
|
30
|
+
const env = envelope(stdout);
|
|
31
|
+
if (env && typeof env.result === 'string')
|
|
32
|
+
return env.result;
|
|
33
|
+
return stdout; // plain mode (no --output-format) or unexpected shape
|
|
34
|
+
},
|
|
35
|
+
envelopeError(stdout) {
|
|
36
|
+
const env = envelope(stdout);
|
|
37
|
+
if (env?.is_error === true)
|
|
38
|
+
return 'CRASH'; // model-level failure signalled in the envelope
|
|
39
|
+
return undefined;
|
|
40
|
+
},
|
|
41
|
+
meta(stdout) {
|
|
42
|
+
const env = envelope(stdout);
|
|
43
|
+
if (!env)
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
session_id: env.session_id,
|
|
47
|
+
subtype: env.subtype,
|
|
48
|
+
total_cost_usd: env.total_cost_usd,
|
|
49
|
+
usage: env.usage,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
export const claude = {
|
|
54
|
+
id: 'claude',
|
|
55
|
+
run: (req, flags, deps) => runAdapter(claudeSpec, req, flags, deps),
|
|
56
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { runAdapter } from './adapter-core.js';
|
|
2
|
+
/**
|
|
3
|
+
* Codex CLI (`codex`). Invocation (verified live, T3):
|
|
4
|
+
* codex exec [-s read-only] "<prompt>" (cwd set via spawn's cwd option)
|
|
5
|
+
*
|
|
6
|
+
* Output split (verified): stdout carries ONLY the model's final message; the full session
|
|
7
|
+
* transcript (session id, echoed prompt, "tokens used") goes to stderr. So stdout *is* the
|
|
8
|
+
* result text → §14 extraction parses it directly, no envelope. (We deliberately avoid
|
|
9
|
+
* `--json` JSONL: plain stdout is already clean and needs no event-stream parsing.)
|
|
10
|
+
*
|
|
11
|
+
* Because codex mirrors the prompt + result into stderr, error classification must not scan
|
|
12
|
+
* stderr on success — adapter-core.classify short-circuits on exit 0 for exactly this reason.
|
|
13
|
+
*
|
|
14
|
+
* T10 note: `codex exec` may need a git-repo/writable-cwd check for arbitrary review dirs;
|
|
15
|
+
* verify when wiring code-review. NEVER pass --dangerously-bypass-approvals-and-sandbox (§19).
|
|
16
|
+
*/
|
|
17
|
+
const codexSpec = {
|
|
18
|
+
id: 'codex',
|
|
19
|
+
buildArgs(req, flags) {
|
|
20
|
+
const args = ['exec'];
|
|
21
|
+
if (req.readOnly !== false && flags.readOnlyFlag === 'sandbox')
|
|
22
|
+
args.push('-s', 'read-only');
|
|
23
|
+
if (flags.model)
|
|
24
|
+
args.push('--model', flags.model); // V8: verified `codex exec --model <id>` (before the prompt)
|
|
25
|
+
args.push(req.prompt);
|
|
26
|
+
return args;
|
|
27
|
+
},
|
|
28
|
+
extractText(stdout) {
|
|
29
|
+
return stdout; // clean final message; §14 extraction handles whole/fenced/balanced JSON
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export const codex = {
|
|
33
|
+
id: 'codex',
|
|
34
|
+
run: (req, flags, deps) => runAdapter(codexSpec, req, flags, deps),
|
|
35
|
+
};
|