aiki-cli 0.3.2 → 0.3.5
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 +68 -0
- package/README.md +64 -4
- package/dist/bench/idea-v3-bench.js +104 -5
- package/dist/bench/idea-v3-rating.js +158 -3
- package/dist/cli/bench.js +5 -5
- package/dist/cli/index.js +12 -2
- package/dist/cli/resume.js +20 -0
- package/dist/cli/run.js +75 -4
- package/dist/cli/serve.js +48 -0
- package/dist/config/config.js +7 -2
- package/dist/council/view.js +13 -4
- package/dist/orchestration/auto-profile.js +97 -0
- package/dist/orchestration/claim-groups.js +56 -0
- package/dist/orchestration/context.js +66 -3
- package/dist/orchestration/decision-dossier.js +42 -10
- package/dist/orchestration/decision-graph.js +2 -2
- package/dist/orchestration/engine.js +8 -4
- package/dist/orchestration/evidence-origin.js +17 -0
- package/dist/orchestration/jsonStage.js +47 -5
- package/dist/orchestration/modes.js +4 -2
- package/dist/orchestration/preflight.js +19 -0
- package/dist/orchestration/quick-analysis.js +31 -6
- package/dist/orchestration/sanitize-paths.js +10 -0
- package/dist/orchestration/stages/s10-render.js +31 -7
- package/dist/orchestration/stages/s4-analyze.js +7 -1
- package/dist/orchestration/stages/s4b-challenge.js +97 -0
- package/dist/orchestration/stages/s5-drift.js +13 -3
- package/dist/orchestration/stages/s8-verify.js +44 -7
- package/dist/orchestration/stages/s9-judge.js +20 -5
- package/dist/orchestration/stages/s9b-plan.js +44 -15
- package/dist/orchestration/url-sources.js +21 -0
- package/dist/providers/adapter-core.js +1 -1
- package/dist/providers/claude.js +18 -0
- package/dist/schemas/index.js +46 -0
- package/dist/serve/flight-deck.js +830 -0
- package/dist/serve/followup.js +50 -0
- package/dist/serve/frames.js +168 -0
- package/dist/serve/gates.js +72 -0
- package/dist/serve/projections.js +283 -0
- package/dist/serve/server.js +219 -0
- package/dist/serve/threads.js +145 -0
- package/dist/serve-ui/Five.png +0 -0
- package/dist/serve-ui/app.js +820 -0
- package/dist/serve-ui/index.html +171 -0
- package/dist/serve-ui/workspace.css +662 -0
- package/dist/storage/runs.js +2 -1
- package/dist/workflows/idea-refinement.js +94 -16
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased — v7 adaptive deliberation
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`aiki run idea-refinement --mode auto`** — a deterministic quick-vs-council routing decision made
|
|
7
|
+
before any provider call. Escalates to `council` when the prompt supplies URLs or an evidence pack,
|
|
8
|
+
hits a regulated/financial/security topic, requests deliverables beyond a decision, uses research
|
|
9
|
+
wording, or runs long (>120 words); otherwise `quick`. Prints the resolved mode + reason and persists
|
|
10
|
+
it to `meta.auto_decision`. No learned routing (§22): the rules are fixed and inspectable. `meta.mode`
|
|
11
|
+
stays `quick|council`; legacy metadata still parses and explicit modes keep their existing artifacts.
|
|
12
|
+
- **One-call auto fast path (v7 Phase C).** A short DECISION-only request with a clear decision verb
|
|
13
|
+
and subject skips the two model preflight readings and builds the same typed contract deterministically.
|
|
14
|
+
Ambiguous prompts keep the existing quick path; explicit quick/council runs are unchanged. Reports label
|
|
15
|
+
the result: `Single-pass analysis; council escalation was not required.`
|
|
16
|
+
- **Structural auto escalation (v7 Phase D).** Validated analyst output now triggers a deterministic
|
|
17
|
+
failover for unsupported decisive claims, model-knowledge STOP/PIVOT claims, failed calculations,
|
|
18
|
+
contradictory supplied evidence, or load-bearing disagreement. Auto runs use 1–4 calls: two existing
|
|
19
|
+
preflight readings, one primary analysis, and at most one slim claim-only challenger call. The receipt
|
|
20
|
+
records every trigger; explicit quick/council paths remain unchanged.
|
|
21
|
+
- **Per-call token accounting (v7 Phase A).** Every call records normalized input/output token usage —
|
|
22
|
+
provider-reported for Claude, a labeled `chars/4` estimate otherwise — summed into `meta.usage_totals`
|
|
23
|
+
and shown as a `Tokens:` line in the dossier and terminal summary. Purely additive; behavior unchanged.
|
|
24
|
+
|
|
25
|
+
## Unreleased — v6 council integrity
|
|
26
|
+
|
|
27
|
+
Root-cause pass driven by exact replay of run `20260717-1219` (`plan/AIKI-v6-council-integrity-plan.md`);
|
|
28
|
+
every fix is pinned by tests on the real payloads that failed live (`test/fixtures/f740-*`).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **A parseable plan is never discarded again.** The planner's complete feature backlog and
|
|
32
|
+
implementation plan were replaced by "deliverables unavailable" over a 13-character headline
|
|
33
|
+
overflow with no repair budget. `jsonCall` now applies deterministic salvage before failing when
|
|
34
|
+
repair is unavailable; lossy coercion clips over-max strings at a word boundary; empty optional
|
|
35
|
+
strings are dropped losslessly (they carry no information). Partial planner answers are kept and
|
|
36
|
+
flagged `plan_partial` instead of vanishing.
|
|
37
|
+
- **The chair's judgment is no longer overruled by code.** The `PARTIAL/UNVERIFIABLE ⇒ must be
|
|
38
|
+
UNRESOLVED` rule (which flattened 5 nuanced rulings and forced a 125s Opus repair) is deleted;
|
|
39
|
+
ruling HOLDS against positively CONTRADICTED evidence remains fatal.
|
|
40
|
+
- **S4 schema-ceremony repairs are free now.** Empty rationale strings validate via lossless
|
|
41
|
+
coercion (was a 267s paid repair); genuinely lossy defects salvage without a call when the budget
|
|
42
|
+
is tight (repairs stay gated behind a 3-call reserved tail: chair + planner + one tail repair).
|
|
43
|
+
- Chair prose is no longer regex-mangled ("confirmed-unverified" stays the chair's own words);
|
|
44
|
+
enum rewrites apply to UPPERCASE leaks only. A `NOT_COMPUTABLE` payback block is omitted instead
|
|
45
|
+
of announced. Rendered Markdown/HTML never contain local filesystem usernames.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **Local council workspace:** `aiki serve` opens a loopback-only three-pane browser workspace with cached
|
|
49
|
+
provider status, decision history, server-enforced file/URL/spend gates, live SSE stage and call telemetry,
|
|
50
|
+
cancellation, path-free verdict cards, and responsive desktop/mobile layouts.
|
|
51
|
+
- Completed decisions accept one-call follow-up questions labeled `no council`; failed or cancelled runs with
|
|
52
|
+
cached calls can be resumed after an explicit spend review. Settings atomically persist provider model pins
|
|
53
|
+
and role assignments without exposing credentials, local paths, or arbitrary JSON to the browser.
|
|
54
|
+
- A dependency-free hosted replay, real workspace screenshot, and timed three-minute demo script provide a
|
|
55
|
+
safe submission path when judges do not have the three provider CLIs installed.
|
|
56
|
+
- **Semantic claim join at zero extra calls:** the S8 verifier may emit `claim_groups` over an
|
|
57
|
+
anonymous all-claims index; validated groups (unknown ids and single-provider groups dropped)
|
|
58
|
+
overlay CONSENSUS/DISAGREEMENT states through the existing state machine. Cross-provider
|
|
59
|
+
paraphrases — the reason every report said "0 consensus · 0 disputes" — now join.
|
|
60
|
+
- **Evidence origin honesty:** every card is classified EXTERNAL / USER_MATERIAL / MODEL_KNOWLEDGE;
|
|
61
|
+
confidence's evidence-quality term counts external only; the user's own material renders as
|
|
62
|
+
"consistent with your materials (not independently checked)", never VERIFIED; the Sources section
|
|
63
|
+
states how many independent external sources the run actually had.
|
|
64
|
+
- **Blocked-source permission prompt:** when a supplied URL cannot be read, an interactive run now
|
|
65
|
+
asks right in the terminal before any paid call — `Proceed without it? [y/N]` — the same
|
|
66
|
+
allow/deny pattern provider CLIs use for internet or file access. Deny prints how to proceed
|
|
67
|
+
(paste the page text into the prompt, or `--allow-blocked-sources`); headless runs fail fast
|
|
68
|
+
with the same instructions. (The previous behavior burned a full council budget around a 403'd
|
|
69
|
+
page.)
|
|
70
|
+
|
|
3
71
|
## 0.3.2 — 2026-07-17
|
|
4
72
|
|
|
5
73
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img alt="Node ≥ 20" src="https://img.shields.io/badge/node-%E2%89%A5%2020-brightgreen.svg">
|
|
13
13
|
<img alt="Local-first, no API keys" src="https://img.shields.io/badge/local--first-no%20API%20keys-informational.svg">
|
|
14
14
|
<img alt="Read-only orchestration" src="https://img.shields.io/badge/orchestration-read--only-success.svg">
|
|
15
|
-
<img alt="Tests" src="https://img.shields.io/badge/tests-
|
|
15
|
+
<img alt="Tests" src="https://img.shields.io/badge/tests-579%20passing-success.svg">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
---
|
|
@@ -30,7 +30,21 @@ It does two jobs, well:
|
|
|
30
30
|
aiki is **not** a general assistant. Trivia and chat are routed away, not answered — a council adds cost, not
|
|
31
31
|
accuracy, when there's one right answer.
|
|
32
32
|
|
|
33
|
-
**Jump to:** [Why](#why) · [What's new](#whats-new-in-030) · [How it works](#how-aiki-works-no-apis) · [Benchmarks](#benchmarks) · [Requirements](#requirements) · [Install](#install) · [Quickstart](#quickstart) · [The two workflows](#the-two-workflows) · [Example](#example-a-real-idea-run) · [Configuration](#configuration) · [Sessions & resume](#sessions--resume) · [Safety](#safety-model) · [Costs & limits](#costs--limits)
|
|
33
|
+
**Jump to:** [Workspace](#local-council-workspace) · [Why](#why) · [What's new](#whats-new-in-030) · [How it works](#how-aiki-works-no-apis) · [Benchmarks](#benchmarks) · [Requirements](#requirements) · [Install](#install) · [Quickstart](#quickstart) · [The two workflows](#the-two-workflows) · [Example](#example-a-real-idea-run) · [Configuration](#configuration) · [Sessions & resume](#sessions--resume) · [Safety](#safety-model) · [Costs & limits](#costs--limits)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Local council workspace
|
|
38
|
+
|
|
39
|
+
Open the live local workspace with:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm i -g aiki-cli && aiki serve
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`aiki serve` opens a loopback-only browser workspace with decision history, explicit file/URL/spend approval,
|
|
46
|
+
live council stages, a verdict card, one-call follow-ups, and per-role model settings. It makes no provider
|
|
47
|
+
calls until you approve and convene a run.
|
|
34
48
|
|
|
35
49
|
---
|
|
36
50
|
|
|
@@ -52,6 +66,11 @@ stop copy-pasting between them by hand.
|
|
|
52
66
|
remaining coverage gaps before the chair.
|
|
53
67
|
- **Two bounded behaviors.** Use `quick` for one structured analyst or `council` for the full multi-model
|
|
54
68
|
decision council with source investigation. `research` remains accepted as an alias for `council`.
|
|
69
|
+
`--mode auto` deterministically selects the entry policy from the prompt (URLs, an evidence pack, a
|
|
70
|
+
regulated/financial/security topic, requested deliverables, research wording, or a long input require
|
|
71
|
+
preflight) and prints the one-line reason — never a learned choice. A clear, short decision question starts
|
|
72
|
+
with one call; structural gaps in the typed output add the two readings and, only when useful, one targeted
|
|
73
|
+
challenger call (1–4 total). Use explicit `council` when you want the full multi-model council every time.
|
|
55
74
|
- **A decision dossier, not an essay.** Reports lead with the recommendation, verified evidence coverage,
|
|
56
75
|
decisive facts, first action, strongest counter-case, and critical unknowns. Financial and threshold-heavy
|
|
57
76
|
decisions can include graph-anchored numbers, payback, option commitments, and a go/no-go tripwire.
|
|
@@ -180,7 +199,7 @@ Upgrade an existing installation:
|
|
|
180
199
|
|
|
181
200
|
```bash
|
|
182
201
|
npm install -g aiki-cli@latest
|
|
183
|
-
aiki --version # 0.3.
|
|
202
|
+
aiki --version # 0.3.4
|
|
184
203
|
```
|
|
185
204
|
|
|
186
205
|
From source:
|
|
@@ -197,9 +216,46 @@ npm link # puts `aiki` on your PATH (or run directly: node dist/cl
|
|
|
197
216
|
|
|
198
217
|
```bash
|
|
199
218
|
cd /path/to/your/project
|
|
200
|
-
aiki
|
|
219
|
+
aiki serve # opens the local council workspace in your browser
|
|
220
|
+
aiki # or open the interactive terminal home screen
|
|
201
221
|
```
|
|
202
222
|
|
|
223
|
+
## The serve workspace
|
|
224
|
+
|
|
225
|
+
`aiki serve` is the recommended way to use Aiki. It starts a **loopback-only** web workspace (bound to
|
|
226
|
+
`127.0.0.1`, never exposed to your network) and opens it in your browser. **No provider is called until you
|
|
227
|
+
type a request and approve the run** — opening the workspace is free.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
aiki serve # binds the first free port in 4173–4183 and opens the browser
|
|
231
|
+
aiki serve --port 4180 # pin a specific port
|
|
232
|
+
aiki serve --no-open # start the server but don't auto-open the browser
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**What you'll see and do:**
|
|
236
|
+
|
|
237
|
+
1. **Sessions rail (left).** Every past run, newest first. Click one to reopen its full decision brief;
|
|
238
|
+
runs that were killed or timed out are flagged **Resume**.
|
|
239
|
+
2. **Composer (center).** Describe an idea to stress-test, or ask for a code review, and press **Enter**.
|
|
240
|
+
Plain text never spends anything on its own — you always get a convene step first.
|
|
241
|
+
3. **Approval gates (inline).** Before any paid work, Aiki asks — per action — to read a **file**, fetch a
|
|
242
|
+
**URL**, or **spend** provider calls. Each gate offers **allow once**, **allow for this session**, or
|
|
243
|
+
**deny**. Nothing runs until you approve.
|
|
244
|
+
4. **Live council.** A pipeline timeline shows each stage; the provider seat cards (Claude, Codex, Gemini)
|
|
245
|
+
light up as they work, with a running **call counter and budget bar** so you always see the spend.
|
|
246
|
+
5. **Verdict card.** When the council finishes: a plain-language banner (go / proceed-with-conditions /
|
|
247
|
+
stop / inconclusive), a **confidence meter**, the anchored validation plan, expandable reasoning and
|
|
248
|
+
evidence, and a **Copy summary** button.
|
|
249
|
+
6. **Cost receipt.** Per-provider call counts plus the token/cost receipt for the run, against your budget.
|
|
250
|
+
7. **Follow-ups.** After a run completes you can ask a follow-up question — answered in **exactly one
|
|
251
|
+
read-only call, no new council** — or **Re-convene** to run a fresh full council on a revised question.
|
|
252
|
+
8. **Settings.** Choose the model each provider uses and override per-role assignments (judge, verifier,
|
|
253
|
+
analyst, the two review seats, the follow-up responder). It shows the effective value vs. your local
|
|
254
|
+
override and never exposes credentials, keys, or paths.
|
|
255
|
+
|
|
256
|
+
Everything the workspace shows is backed by the same schema-validated, on-disk audit trail as the headless
|
|
257
|
+
CLI, so nothing on screen can drift from what was actually stored.
|
|
258
|
+
|
|
203
259
|
## Use Aiki For
|
|
204
260
|
|
|
205
261
|
- Code review: "review this branch", "check this patch", "find bugs in this diff".
|
|
@@ -234,6 +290,7 @@ Plain text is never charged silently — you get a confirm step before any run s
|
|
|
234
290
|
```bash
|
|
235
291
|
aiki run idea-refinement "a fridge-photo-to-recipe app for busy parents"
|
|
236
292
|
aiki run idea-refinement "an early idea" --mode quick # one structured analyst; no council claim
|
|
293
|
+
aiki run idea-refinement "Should I use Postgres or MySQL?" --mode auto # starts at 1 call; structural gaps may escalate to 4
|
|
237
294
|
aiki run idea-refinement ./idea.md
|
|
238
295
|
aiki run idea-refinement ./idea.md --mode council --evidence ./research/ # grounded, source-verifying council
|
|
239
296
|
aiki run code-review --base main # review this branch vs main
|
|
@@ -361,6 +418,9 @@ This is the part that makes aiki trustworthy to point at a real repo:
|
|
|
361
418
|
**3 calls in quick** or **8–10 in the full council** (`research` is the same behavior; schema repairs can add
|
|
362
419
|
calls within the mode-aware budget); code review is about **5**. `aiki run` shows the mode, range, budget, and reserved
|
|
363
420
|
chair/planner calls before asking to confirm (skip with `--yes`).
|
|
421
|
+
- **Every run reports token usage.** The decision report and the serve cost receipt show input/output tokens
|
|
422
|
+
and reported cost per run — provider-reported where the CLI returns it, a labeled estimate otherwise — so
|
|
423
|
+
you see the real spend, not just the call count.
|
|
364
424
|
- **Not a general assistant.** Questions and "explore my whole codebase" requests are redirected, not answered
|
|
365
425
|
— aiki reviews a *diff* and vets a *stated idea*.
|
|
366
426
|
- **Analysis, not advice.** Every report is a decision aid. Verify before acting.
|
|
@@ -3,15 +3,18 @@ import { access, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/pro
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { executeRun } from '../orchestration/engine.js';
|
|
6
|
+
import { buildTaskProfile, resolveAutoMode } from '../orchestration/auto-profile.js';
|
|
6
7
|
import { jsonCall } from '../orchestration/jsonStage.js';
|
|
7
8
|
import { makeRunId, resolveRoles, RunCtx, setupProviders } from '../orchestration/context.js';
|
|
8
9
|
import { EvidencePack } from '../orchestration/evidence-pack.js';
|
|
10
|
+
import { requestedOutputsFor } from '../orchestration/preflight.js';
|
|
11
|
+
import { extractPublicUrls } from '../orchestration/url-sources.js';
|
|
9
12
|
import { IdeaV3CaseManifest } from './scoring/decision-insights.js';
|
|
10
13
|
import { RunWriter } from '../storage/runs.js';
|
|
11
14
|
import { runIdeaRefinement } from '../workflows/idea-refinement.js';
|
|
12
|
-
export const IDEA_V3_ARM_IDS = ['B', 'C', 'D2', 'R'];
|
|
15
|
+
export const IDEA_V3_ARM_IDS = ['A', 'B', 'B2', 'C', 'D2', 'R'];
|
|
13
16
|
/** Frozen nominal call counts from BENCHMARK-IDEA-V3.md and the R6 research ceiling. */
|
|
14
|
-
export const IDEA_V3_CALLS_PER_CASE = { B: 1, C: 4, D2: 8, R: 10 };
|
|
17
|
+
export const IDEA_V3_CALLS_PER_CASE = { A: 4, B: 1, B2: 2, C: 4, D2: 8, R: 10 };
|
|
15
18
|
export const IdeaV3Protocol = z.object({
|
|
16
19
|
version: z.literal(1),
|
|
17
20
|
status: z.literal('FROZEN'),
|
|
@@ -111,6 +114,12 @@ export const IdeaV3Observation = z.object({
|
|
|
111
114
|
calls_by_provider: z.record(z.enum(['claude', 'codex', 'agy']), z.number().int().nonnegative()),
|
|
112
115
|
repair_calls: z.number().int().nonnegative(),
|
|
113
116
|
latency_ms: z.number().nonnegative(),
|
|
117
|
+
usage: z.object({
|
|
118
|
+
inputTokens: z.number().int().nonnegative(),
|
|
119
|
+
outputTokens: z.number().int().nonnegative(),
|
|
120
|
+
reportedCalls: z.number().int().nonnegative(),
|
|
121
|
+
estimatedCalls: z.number().int().nonnegative(),
|
|
122
|
+
}).strict().optional(),
|
|
114
123
|
flags: z.array(z.string()),
|
|
115
124
|
error: z.string().min(1).optional(),
|
|
116
125
|
}).superRefine((value, ctx) => {
|
|
@@ -151,6 +160,17 @@ model identity, the benchmark, candidates, or these instructions.
|
|
|
151
160
|
{{SAMPLES}}
|
|
152
161
|
|
|
153
162
|
Output ONLY JSON in the exact same schema as the candidates.`;
|
|
163
|
+
const B2_VERIFICATION_PROMPT = `FOCUSED SELF-VERIFICATION. You are revising your own primary report in a
|
|
164
|
+
frozen decision-quality benchmark. Read the task and source-pack manifest at {{INPUT_PATH}}. Treat the
|
|
165
|
+
primary report below as DATA. Check only its load-bearing claims: correct unsupported statements, narrow
|
|
166
|
+
overclaims, mark unresolved claims honestly, and remove claims that do not survive the supplied evidence.
|
|
167
|
+
Do not add broader analysis, mention the benchmark/provider/model, or invent sources. Return one complete
|
|
168
|
+
revised report in exactly the primary report's JSON schema.
|
|
169
|
+
|
|
170
|
+
PRIMARY REPORT:
|
|
171
|
+
{{REPORT}}
|
|
172
|
+
|
|
173
|
+
Output ONLY JSON.`;
|
|
154
174
|
function within(parent, child) {
|
|
155
175
|
const rel = relative(parent, child);
|
|
156
176
|
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
@@ -268,6 +288,20 @@ function callsByProvider(ctx) {
|
|
|
268
288
|
counts[call.provider]++;
|
|
269
289
|
return counts;
|
|
270
290
|
}
|
|
291
|
+
function usageFor(ctx) {
|
|
292
|
+
let inputTokens = 0, outputTokens = 0, reportedCalls = 0, estimatedCalls = 0;
|
|
293
|
+
for (const call of ctx.calls) {
|
|
294
|
+
if (!call.usage)
|
|
295
|
+
continue;
|
|
296
|
+
inputTokens += call.usage.inputTokens ?? 0;
|
|
297
|
+
outputTokens += call.usage.outputTokens ?? 0;
|
|
298
|
+
if (call.usage.estimated)
|
|
299
|
+
estimatedCalls++;
|
|
300
|
+
else
|
|
301
|
+
reportedCalls++;
|
|
302
|
+
}
|
|
303
|
+
return reportedCalls || estimatedCalls ? { inputTokens, outputTokens, reportedCalls, estimatedCalls } : undefined;
|
|
304
|
+
}
|
|
271
305
|
async function executeBaseline(arm, item, handles, provider, root) {
|
|
272
306
|
const runId = makeRunId('idea-refinement');
|
|
273
307
|
const writer = new RunWriter(runId, join(root, '.aiki'));
|
|
@@ -287,8 +321,15 @@ async function executeBaseline(arm, item, handles, provider, root) {
|
|
|
287
321
|
const inputPath = await runCtx.writer.writeInput('idea-v3-task.md', input);
|
|
288
322
|
const prompt = B_PROMPT.replace('{{INPUT_PATH}}', inputPath);
|
|
289
323
|
await runCtx.writer.writePrompt('idea-v3-baseline.md', prompt);
|
|
290
|
-
if (arm === 'B') {
|
|
291
|
-
report = await jsonCall(runCtx, runCtx.handle(provider), 'B', prompt, IdeaV3BaselineReport);
|
|
324
|
+
if (arm === 'B' || arm === 'B2') {
|
|
325
|
+
report = await jsonCall(runCtx, runCtx.handle(provider), arm === 'B' ? 'B' : 'B2-primary', prompt, IdeaV3BaselineReport);
|
|
326
|
+
if (arm === 'B2') {
|
|
327
|
+
const verification = B2_VERIFICATION_PROMPT
|
|
328
|
+
.replace('{{INPUT_PATH}}', inputPath)
|
|
329
|
+
.replace('{{REPORT}}', JSON.stringify(report, null, 2));
|
|
330
|
+
await runCtx.writer.writeRaw('B2-verification.prompt.txt', verification);
|
|
331
|
+
report = await jsonCall(runCtx, runCtx.handle(provider), 'B2-verify', verification, IdeaV3BaselineReport);
|
|
332
|
+
}
|
|
292
333
|
}
|
|
293
334
|
else {
|
|
294
335
|
const samples = [];
|
|
@@ -302,6 +343,7 @@ async function executeBaseline(arm, item, handles, provider, root) {
|
|
|
302
343
|
await runCtx.writer.writeRaw('idea-v3-baseline-report.json', JSON.stringify(report, null, 2));
|
|
303
344
|
await runCtx.writer.writeText('final-report', renderBaselineReport(report));
|
|
304
345
|
});
|
|
346
|
+
const usage = usageFor(ctx);
|
|
305
347
|
const base = {
|
|
306
348
|
case_id: item.id,
|
|
307
349
|
arm,
|
|
@@ -310,12 +352,59 @@ async function executeBaseline(arm, item, handles, provider, root) {
|
|
|
310
352
|
calls_by_provider: callsByProvider(ctx),
|
|
311
353
|
repair_calls: ctx.calls.filter((call) => call.stage.endsWith('-repair')).length,
|
|
312
354
|
latency_ms: Date.now() - started,
|
|
355
|
+
...(usage ? { usage } : {}),
|
|
313
356
|
flags: [...ctx.flags],
|
|
314
357
|
};
|
|
315
358
|
return IdeaV3Observation.parse(outcome.ok && report
|
|
316
359
|
? { ...base, status: 'ok', report_markdown: renderBaselineReport(report) }
|
|
317
360
|
: { ...base, status: 'error', error: `${outcome.error?.code ?? 'CRASH'}: ${outcome.error?.message ?? 'baseline produced no report'}` });
|
|
318
361
|
}
|
|
362
|
+
async function executeAuto(item, handles, root) {
|
|
363
|
+
const runId = makeRunId('idea-refinement');
|
|
364
|
+
const writer = new RunWriter(runId, join(root, '.aiki'));
|
|
365
|
+
const input = benchmarkInput(item);
|
|
366
|
+
const evidencePack = await caseEvidencePack(item);
|
|
367
|
+
const resolved = resolveAutoMode(buildTaskProfile(input, {
|
|
368
|
+
urlCount: extractPublicUrls(input).length,
|
|
369
|
+
hasEvidencePack: !!evidencePack,
|
|
370
|
+
requestedOutputs: requestedOutputsFor(input),
|
|
371
|
+
}));
|
|
372
|
+
const ctx = new RunCtx({
|
|
373
|
+
runId,
|
|
374
|
+
workflow: 'idea-refinement',
|
|
375
|
+
mode: resolved.mode,
|
|
376
|
+
handles,
|
|
377
|
+
roles: resolveRoles('idea-refinement', handles.map((handle) => handle.id)),
|
|
378
|
+
writer,
|
|
379
|
+
cwd: writer.dir,
|
|
380
|
+
budget: 7,
|
|
381
|
+
deadlineMs: 45 * 60 * 1000,
|
|
382
|
+
evidencePack,
|
|
383
|
+
autoDecision: {
|
|
384
|
+
resolved: resolved.mode,
|
|
385
|
+
reasons: resolved.reasons,
|
|
386
|
+
...(resolved.fastPath ? { fast_path: true } : {}),
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
const started = Date.now();
|
|
390
|
+
const outcome = await executeRun(ctx, input, runIdeaRefinement);
|
|
391
|
+
const usage = usageFor(ctx);
|
|
392
|
+
const base = {
|
|
393
|
+
case_id: item.id,
|
|
394
|
+
arm: 'A',
|
|
395
|
+
run_id: runId,
|
|
396
|
+
calls: ctx.calls.length,
|
|
397
|
+
calls_by_provider: callsByProvider(ctx),
|
|
398
|
+
repair_calls: ctx.calls.filter((call) => call.stage.endsWith('-repair')).length,
|
|
399
|
+
latency_ms: Date.now() - started,
|
|
400
|
+
...(usage ? { usage } : {}),
|
|
401
|
+
flags: [...ctx.flags],
|
|
402
|
+
};
|
|
403
|
+
if (!outcome.ok) {
|
|
404
|
+
return IdeaV3Observation.parse({ ...base, status: 'error', error: `${outcome.error?.code}: ${outcome.error?.message}` });
|
|
405
|
+
}
|
|
406
|
+
return IdeaV3Observation.parse({ ...base, status: 'ok', report_markdown: await readFile(join(outcome.dir, 'final-report.md'), 'utf8') });
|
|
407
|
+
}
|
|
319
408
|
async function executeResearch(item, handles, root, frozenRoles) {
|
|
320
409
|
const runId = makeRunId('idea-refinement');
|
|
321
410
|
const writer = new RunWriter(runId, join(root, '.aiki'));
|
|
@@ -332,6 +421,7 @@ async function executeResearch(item, handles, root, frozenRoles) {
|
|
|
332
421
|
});
|
|
333
422
|
const started = Date.now();
|
|
334
423
|
const outcome = await executeRun(ctx, benchmarkInput(item), runIdeaRefinement);
|
|
424
|
+
const usage = usageFor(ctx);
|
|
335
425
|
const base = {
|
|
336
426
|
case_id: item.id,
|
|
337
427
|
arm: 'R',
|
|
@@ -340,6 +430,7 @@ async function executeResearch(item, handles, root, frozenRoles) {
|
|
|
340
430
|
calls_by_provider: callsByProvider(ctx),
|
|
341
431
|
repair_calls: ctx.calls.filter((call) => call.stage.endsWith('-repair')).length,
|
|
342
432
|
latency_ms: Date.now() - started,
|
|
433
|
+
...(usage ? { usage } : {}),
|
|
343
434
|
flags: [...ctx.flags],
|
|
344
435
|
};
|
|
345
436
|
if (!outcome.ok) {
|
|
@@ -353,6 +444,9 @@ function parseArms(arms, set) {
|
|
|
353
444
|
throw new Error('at least one idea-v3 arm is required');
|
|
354
445
|
if (set === 'holdout' && unique.includes('D2'))
|
|
355
446
|
throw new Error('D2 is build-set diagnostic only and has no holdout weight');
|
|
447
|
+
if (set === 'holdout' && unique.some((arm) => arm === 'A' || arm === 'B2')) {
|
|
448
|
+
throw new Error('Phase F arms are build-set validation only and cannot change the frozen holdout protocol');
|
|
449
|
+
}
|
|
356
450
|
return unique;
|
|
357
451
|
}
|
|
358
452
|
const CAMPAIGN_NAME = /^idea-v3-(build|holdout)-(claude|codex|agy)-\d{4}-\d{2}-\d{2}\.json$/;
|
|
@@ -447,9 +541,12 @@ export async function runIdeaV3Bench(opts = {}) {
|
|
|
447
541
|
const prior = resolved.prior?.observations ?? [];
|
|
448
542
|
const done = new Set(prior.map((item) => `${item.case_id}:${item.arm}`));
|
|
449
543
|
const handles = opts.handles ?? await setupProviders(protocol?.models);
|
|
450
|
-
if (!handles.some((handle) => handle.id === baselineProvider) && arms.some((arm) => arm === 'B' || arm === 'C')) {
|
|
544
|
+
if (!handles.some((handle) => handle.id === baselineProvider) && arms.some((arm) => arm === 'B' || arm === 'B2' || arm === 'C')) {
|
|
451
545
|
throw new Error(`baseline provider ${baselineProvider} is unavailable`);
|
|
452
546
|
}
|
|
547
|
+
if (arms.includes('A') && new Set(handles.map((handle) => handle.id)).size < 2) {
|
|
548
|
+
throw new Error('A requires at least two providers for protocol-comparable auto escalation');
|
|
549
|
+
}
|
|
453
550
|
if (arms.includes('R') && new Set(handles.map((handle) => handle.id)).size < 3) {
|
|
454
551
|
throw new Error('R requires all three frozen providers for a protocol-comparable run');
|
|
455
552
|
}
|
|
@@ -466,6 +563,8 @@ export async function runIdeaV3Bench(opts = {}) {
|
|
|
466
563
|
const execute = opts.execute ?? (async (target) => {
|
|
467
564
|
if (target.arm === 'D2')
|
|
468
565
|
return d2ByCase.get(target.case.id);
|
|
566
|
+
if (target.arm === 'A')
|
|
567
|
+
return executeAuto(target.case, handles, root);
|
|
469
568
|
if (target.arm === 'R')
|
|
470
569
|
return executeResearch(target.case, handles, root, protocol?.roles);
|
|
471
570
|
return executeBaseline(target.arm, target.case, handles, baselineProvider, root);
|
|
@@ -132,7 +132,7 @@ export function blindIdeaV3Report(report, runId, caseDir) {
|
|
|
132
132
|
.replace(/^(- Report ID:).*$/gm, '$1 [redacted]')
|
|
133
133
|
.replace(/^(- Generated:).*$/gm, '$1 [redacted]')
|
|
134
134
|
.replace(/^(- Models and roles:).*$/gm, '$1 [redacted]')
|
|
135
|
-
.replace(/^(- (?:Mode|Provider calls|Categories|By provider|Recorded model time|Degradation flags):).*$/gm, '$1 [redacted]')
|
|
135
|
+
.replace(/^(- (?:Mode|Provider calls|Categories|By provider|Recorded model time|Tokens|Degradation flags):).*$/gm, '$1 [redacted]')
|
|
136
136
|
.replace(/^(> ⚠ DEGRADED): [a-z0-9_]+(?:, [a-z0-9_]+)*\.?$/gm, '$1 [redacted]');
|
|
137
137
|
}
|
|
138
138
|
async function renderCasePacket(item) {
|
|
@@ -384,6 +384,82 @@ function median(values) {
|
|
|
384
384
|
const middle = Math.floor(sorted.length / 2);
|
|
385
385
|
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
386
386
|
}
|
|
387
|
+
function nearestRank(values, percentile) {
|
|
388
|
+
if (!values.length)
|
|
389
|
+
return 0;
|
|
390
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
391
|
+
return sorted[Math.ceil(percentile * sorted.length) - 1];
|
|
392
|
+
}
|
|
393
|
+
/** Phase F additive metric: adjudicated matched expert claims per 1,000 recorded input+output tokens. */
|
|
394
|
+
export function ideaV3TokenEfficiency(scored, campaign) {
|
|
395
|
+
scored = IdeaV3ScoredCampaign.parse(scored);
|
|
396
|
+
campaign = IdeaV3Campaign.parse(campaign);
|
|
397
|
+
return campaign.arms.map((arm) => {
|
|
398
|
+
const observations = campaign.observations.filter((item) => item.arm === arm);
|
|
399
|
+
const complete = observations.length > 0 && observations.every((item) => item.usage !== undefined);
|
|
400
|
+
const tokens = complete
|
|
401
|
+
? observations.reduce((sum, item) => sum + item.usage.inputTokens + item.usage.outputTokens, 0)
|
|
402
|
+
: null;
|
|
403
|
+
const matched = scored.summary.find((item) => item.arm === arm)?.score.matched ?? 0;
|
|
404
|
+
return {
|
|
405
|
+
arm,
|
|
406
|
+
matched,
|
|
407
|
+
tokens,
|
|
408
|
+
matched_per_1k_tokens: tokens ? matched * 1_000 / tokens : null,
|
|
409
|
+
estimated_calls: observations.reduce((sum, item) => sum + (item.usage?.estimatedCalls ?? 0), 0),
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** Phase F build-only product targets. Original frozen holdout ship gates remain separate. */
|
|
414
|
+
export function evaluateIdeaV3AdaptiveGates(scored, campaign) {
|
|
415
|
+
scored = IdeaV3ScoredCampaign.parse(scored);
|
|
416
|
+
campaign = IdeaV3Campaign.parse(campaign);
|
|
417
|
+
if (scored.set !== 'build' || campaign.set !== 'build') {
|
|
418
|
+
throw new Error('adaptive product targets require the Phase F build campaign');
|
|
419
|
+
}
|
|
420
|
+
const required = ['A', 'B', 'B2', 'D2', 'R'];
|
|
421
|
+
const caseIds = new Set(campaign.observations.filter((item) => item.arm === 'A').map((item) => item.case_id));
|
|
422
|
+
const expectedPairs = [...caseIds].flatMap((caseId) => required.map((arm) => `${caseId}:${arm}`));
|
|
423
|
+
const observedPairs = campaign.observations.filter((item) => required.includes(item.arm));
|
|
424
|
+
const scoredPairs = new Set(scored.reports.map((item) => `${item.case_id}:${item.arm}`));
|
|
425
|
+
const matrixComplete = caseIds.size > 0
|
|
426
|
+
&& observedPairs.length === expectedPairs.length
|
|
427
|
+
&& expectedPairs.every((pair) => observedPairs.some((item) => `${item.case_id}:${item.arm}` === pair) && scoredPairs.has(pair))
|
|
428
|
+
&& required.every((arm) => scored.summary.some((item) => item.arm === arm));
|
|
429
|
+
const score = (arm) => scored.summary.find((item) => item.arm === arm)?.score.f1;
|
|
430
|
+
const a = score('A'), b = score('B'), d2 = score('D2'), r = score('R');
|
|
431
|
+
const aObservations = campaign.observations.filter((item) => item.arm === 'A');
|
|
432
|
+
const rObservations = campaign.observations.filter((item) => item.arm === 'R');
|
|
433
|
+
const usageComplete = aObservations.length > 0 && rObservations.length > 0
|
|
434
|
+
&& [...aObservations, ...rObservations].every((item) => item.usage !== undefined);
|
|
435
|
+
const perCaseTokens = (items) => items.map((item) => item.usage.inputTokens + item.usage.outputTokens);
|
|
436
|
+
const aMedianTokens = usageComplete ? median(perCaseTokens(aObservations)) : null;
|
|
437
|
+
const rMedianTokens = usageComplete ? median(perCaseTokens(rObservations)) : null;
|
|
438
|
+
const tokenReduction = aMedianTokens !== null && rMedianTokens
|
|
439
|
+
? 1 - aMedianTokens / rMedianTokens
|
|
440
|
+
: null;
|
|
441
|
+
const aMedianCalls = median(aObservations.map((item) => item.calls));
|
|
442
|
+
const aP95Calls = nearestRank(aObservations.map((item) => item.calls), 0.95);
|
|
443
|
+
const gates = {
|
|
444
|
+
matrix_complete: matrixComplete,
|
|
445
|
+
usage_complete: usageComplete,
|
|
446
|
+
primary_vs_b: a !== undefined && b !== undefined && a > b,
|
|
447
|
+
quality_floor_d2: a !== undefined && d2 !== undefined && a >= d2 - 0.05,
|
|
448
|
+
quality_floor_r: a !== undefined && r !== undefined && a >= r - 0.05,
|
|
449
|
+
token_savings: tokenReduction !== null && tokenReduction >= 0.4,
|
|
450
|
+
calls_median: aObservations.length > 0 && aMedianCalls <= 3,
|
|
451
|
+
calls_p95: aObservations.length > 0 && aP95Calls <= 6,
|
|
452
|
+
};
|
|
453
|
+
return {
|
|
454
|
+
...gates,
|
|
455
|
+
pass: Object.values(gates).every(Boolean),
|
|
456
|
+
a_median_calls: aMedianCalls,
|
|
457
|
+
a_p95_calls: aP95Calls,
|
|
458
|
+
a_median_tokens: aMedianTokens,
|
|
459
|
+
r_median_tokens: rMedianTokens,
|
|
460
|
+
token_reduction: tokenReduction,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
387
463
|
/** Pure frozen ship-gate calculation. Secondary wins cannot rescue either primary F1 loss. */
|
|
388
464
|
export function evaluateIdeaV3Gates(scored, campaign) {
|
|
389
465
|
scored = IdeaV3ScoredCampaign.parse(scored);
|
|
@@ -441,6 +517,75 @@ export function evaluateIdeaV3Gates(scored, campaign) {
|
|
|
441
517
|
holdout_cases: cases.size,
|
|
442
518
|
};
|
|
443
519
|
}
|
|
520
|
+
/** Publish Phase F build validation without turning it into a frozen holdout claim. */
|
|
521
|
+
export async function writeIdeaV3AdaptiveResults(opts) {
|
|
522
|
+
const root = opts.root ?? process.cwd();
|
|
523
|
+
const scoredPath = resolve(opts.scoredPath);
|
|
524
|
+
const scored = IdeaV3ScoredCampaign.parse(JSON.parse(await readFile(scoredPath, 'utf8')));
|
|
525
|
+
const campaignPath = resolveFrom(dirname(scoredPath), scored.campaign);
|
|
526
|
+
const campaign = IdeaV3Campaign.parse(JSON.parse(await readFile(campaignPath, 'utf8')));
|
|
527
|
+
const gates = evaluateIdeaV3AdaptiveGates(scored, campaign);
|
|
528
|
+
const efficiency = ideaV3TokenEfficiency(scored, campaign);
|
|
529
|
+
const summary = new Map(scored.summary.map((item) => [item.arm, item.score]));
|
|
530
|
+
const rows = [
|
|
531
|
+
['Complete scored A/B/B2/D2/R matrix', gates.matrix_complete, gates.matrix_complete ? 'complete' : 'missing or duplicate pairs'],
|
|
532
|
+
['A/R token usage complete', gates.usage_complete, gates.usage_complete ? 'complete' : 'usage missing'],
|
|
533
|
+
['A strictly beats B on F1', gates.primary_vs_b, `${summary.get('A')?.f1.toFixed(3)} vs ${summary.get('B')?.f1.toFixed(3)}`],
|
|
534
|
+
['A within 0.05 F1 of D2', gates.quality_floor_d2, `${summary.get('A')?.f1.toFixed(3)} vs ${summary.get('D2')?.f1.toFixed(3)}`],
|
|
535
|
+
['A within 0.05 F1 of R', gates.quality_floor_r, `${summary.get('A')?.f1.toFixed(3)} vs ${summary.get('R')?.f1.toFixed(3)}`],
|
|
536
|
+
['A median tokens ≥40% below R', gates.token_savings, gates.token_reduction === null ? 'usage missing' : pct(gates.token_reduction)],
|
|
537
|
+
['A median calls ≤3', gates.calls_median, gates.a_median_calls.toFixed(1)],
|
|
538
|
+
['A p95 calls ≤6', gates.calls_p95, gates.a_p95_calls.toFixed(0)],
|
|
539
|
+
];
|
|
540
|
+
const lines = [
|
|
541
|
+
'# RESULTS-IDEA-V3-ADAPTIVE — Phase F build validation',
|
|
542
|
+
'',
|
|
543
|
+
`**Adaptive build targets: ${gates.pass ? 'PASS' : 'FAIL'}**`,
|
|
544
|
+
'',
|
|
545
|
+
'Build-set product validation; not a frozen holdout claim.',
|
|
546
|
+
'',
|
|
547
|
+
'## Product targets',
|
|
548
|
+
'',
|
|
549
|
+
'| Target | Result | Evidence |',
|
|
550
|
+
'|---|---|---|',
|
|
551
|
+
...rows.map(([name, pass, evidence]) => `| ${name} | ${pass ? 'PASS' : 'FAIL'} | ${evidence} |`),
|
|
552
|
+
'',
|
|
553
|
+
'## Primary metric',
|
|
554
|
+
'',
|
|
555
|
+
'| Arm | Matched | Recall | Precision | F1 |',
|
|
556
|
+
'|---|---:|---:|---:|---:|',
|
|
557
|
+
...campaign.arms.map((arm) => {
|
|
558
|
+
const item = summary.get(arm);
|
|
559
|
+
return `| ${arm} | ${item.matched} | ${pct(item.recall)} | ${pct(item.precision)} | ${item.f1.toFixed(3)} |`;
|
|
560
|
+
}),
|
|
561
|
+
'',
|
|
562
|
+
'## Verified insights per 1,000 tokens',
|
|
563
|
+
'',
|
|
564
|
+
'| Arm | Matched | Tokens | Matched / 1k tokens | Estimated calls |',
|
|
565
|
+
'|---|---:|---:|---:|---:|',
|
|
566
|
+
...efficiency.map((item) => `| ${item.arm} | ${item.matched} | ${item.tokens ?? 'missing'} | ${item.matched_per_1k_tokens?.toFixed(3) ?? 'missing'} | ${item.estimated_calls} |`),
|
|
567
|
+
'',
|
|
568
|
+
'## Every case and arm',
|
|
569
|
+
'',
|
|
570
|
+
'| Case | Arm | Status | Calls | Tokens | Repairs | Wall time | Flags / failure |',
|
|
571
|
+
'|---|---|---|---:|---:|---:|---:|---|',
|
|
572
|
+
...[...campaign.observations]
|
|
573
|
+
.sort((left, right) => left.case_id.localeCompare(right.case_id) || left.arm.localeCompare(right.arm))
|
|
574
|
+
.map((item) => {
|
|
575
|
+
const tokens = item.usage ? item.usage.inputTokens + item.usage.outputTokens : 'missing';
|
|
576
|
+
const detail = item.status === 'error' ? item.error : item.flags.join(', ') || 'none';
|
|
577
|
+
return `| ${item.case_id} | ${item.arm} | ${item.status} | ${item.calls} | ${tokens} | ${item.repair_calls} | ${(item.latency_ms / 1000).toFixed(1)}s | ${mdCell(detail)} |`;
|
|
578
|
+
}),
|
|
579
|
+
'',
|
|
580
|
+
`Token values include input plus output. ${efficiency.some((item) => item.estimated_calls > 0) ? 'At least one call uses Phase A labeled estimation.' : 'All recorded calls use provider-reported totals.'}`,
|
|
581
|
+
'',
|
|
582
|
+
];
|
|
583
|
+
const path = opts.outPath ?? join(root, 'RESULTS-IDEA-V3-ADAPTIVE.md');
|
|
584
|
+
const tmp = `${path}.tmp`;
|
|
585
|
+
await writeFile(tmp, lines.join('\n'), 'utf8');
|
|
586
|
+
await rename(tmp, path);
|
|
587
|
+
return { path, gates };
|
|
588
|
+
}
|
|
444
589
|
function mdCell(value) {
|
|
445
590
|
return value.replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
446
591
|
}
|
|
@@ -511,6 +656,16 @@ export async function writeIdeaV3Results(opts) {
|
|
|
511
656
|
await rename(tmp, path);
|
|
512
657
|
return { path, gates };
|
|
513
658
|
}
|
|
659
|
+
/** CLI publication dispatch: build amendments and frozen holdout results stay visibly separate. */
|
|
660
|
+
export async function publishIdeaV3Results(opts) {
|
|
661
|
+
const scored = IdeaV3ScoredCampaign.parse(JSON.parse(await readFile(resolve(opts.scoredPath), 'utf8')));
|
|
662
|
+
if (scored.set === 'build') {
|
|
663
|
+
const result = await writeIdeaV3AdaptiveResults(opts);
|
|
664
|
+
return { path: result.path, passed: result.gates.pass, label: 'adaptive build targets' };
|
|
665
|
+
}
|
|
666
|
+
const result = await writeIdeaV3Results(opts);
|
|
667
|
+
return { path: result.path, passed: result.gates.ship, label: 'frozen ship gate' };
|
|
668
|
+
}
|
|
514
669
|
export const IdeaV3ProtocolDraft = z.object({
|
|
515
670
|
build_scores: z.string().min(1),
|
|
516
671
|
baseline_provider: z.enum(['claude', 'codex', 'agy']),
|
|
@@ -523,7 +678,7 @@ export const IdeaV3ProtocolDraft = z.object({
|
|
|
523
678
|
}).strict(),
|
|
524
679
|
lane_assignment: z.enum(['agy-market', 'codex-market']),
|
|
525
680
|
}).strict();
|
|
526
|
-
/** Freeze only
|
|
681
|
+
/** Freeze only the original complete, already-scored B/C/D2/R build campaign; the file is one-shot. */
|
|
527
682
|
export async function writeFrozenIdeaV3Protocol(opts) {
|
|
528
683
|
const root = opts.root ?? process.cwd();
|
|
529
684
|
const draftPath = resolve(opts.draftPath);
|
|
@@ -539,7 +694,7 @@ export async function writeFrozenIdeaV3Protocol(opts) {
|
|
|
539
694
|
if (campaign.baseline_provider !== draft.baseline_provider) {
|
|
540
695
|
throw new Error(`draft baseline ${draft.baseline_provider} does not match campaign ${campaign.baseline_provider}`);
|
|
541
696
|
}
|
|
542
|
-
const arms = [
|
|
697
|
+
const arms = ['B', 'C', 'D2', 'R'];
|
|
543
698
|
if (arms.some((arm) => !campaign.arms.includes(arm) || !scored.summary.some((item) => item.arm === arm))) {
|
|
544
699
|
throw new Error('protocol freeze requires scored B, C, D2, and R build arms');
|
|
545
700
|
}
|
package/dist/cli/bench.js
CHANGED
|
@@ -6,7 +6,7 @@ import { planBench, renderTable, runBench } from '../bench/harness.js';
|
|
|
6
6
|
import { setupProviders } from '../orchestration/context.js';
|
|
7
7
|
import { chooseLaneDefault, importLaneAdjudications, planIdeaLaneBench, runIdeaLaneBench } from '../bench/idea-lane-rotation.js';
|
|
8
8
|
import { IDEA_V3_ARM_IDS, planIdeaV3Bench, runIdeaV3Bench } from '../bench/idea-v3-bench.js';
|
|
9
|
-
import { exportIdeaV3BlindBundle, importIdeaV3Ratings,
|
|
9
|
+
import { exportIdeaV3BlindBundle, importIdeaV3Ratings, publishIdeaV3Results, writeFrozenIdeaV3Protocol } from '../bench/idea-v3-rating.js';
|
|
10
10
|
import { DISPLAY_NAME } from '../providers/types.js';
|
|
11
11
|
const VALID_ARMS = ['A', 'B', 'C', 'D', 'E', 'L'];
|
|
12
12
|
/** One-block pre-run summary: what will run + the ≈Opus cost, so the user commits knowingly (§19). */
|
|
@@ -34,9 +34,9 @@ export async function benchCommand(workflow, opts = {}) {
|
|
|
34
34
|
}
|
|
35
35
|
if (opts.publishResults) {
|
|
36
36
|
try {
|
|
37
|
-
const result = await
|
|
37
|
+
const result = await publishIdeaV3Results({ scoredPath: opts.publishResults });
|
|
38
38
|
process.stdout.write(`\nresults: ${result.path}\n`);
|
|
39
|
-
process.stdout.write(
|
|
39
|
+
process.stdout.write(`${result.label}: ${result.passed ? 'PASS' : 'FAIL'}\n\n`);
|
|
40
40
|
return 0;
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
@@ -82,7 +82,7 @@ export async function benchCommand(workflow, opts = {}) {
|
|
|
82
82
|
: set === 'build' ? ['B', 'C', 'D2', 'R'] : ['B', 'C', 'R'];
|
|
83
83
|
const invalid = requested.filter((arm) => !IDEA_V3_ARM_IDS.includes(arm));
|
|
84
84
|
if (invalid.length) {
|
|
85
|
-
process.stderr.write(`invalid idea-v3 arm(s): ${invalid.join(', ')}. Valid: B,C,D2,R\n`);
|
|
85
|
+
process.stderr.write(`invalid idea-v3 arm(s): ${invalid.join(', ')}. Valid: A,B,B2,C,D2,R\n`);
|
|
86
86
|
return 1;
|
|
87
87
|
}
|
|
88
88
|
const arms = requested;
|
|
@@ -100,7 +100,7 @@ export async function benchCommand(workflow, opts = {}) {
|
|
|
100
100
|
return 1;
|
|
101
101
|
}
|
|
102
102
|
process.stdout.write(`\nidea-v3 protocol comparison — ${set} — ${plan.cases.length} case(s) × arms ${arms.join(',')}\n`);
|
|
103
|
-
process.stdout.write(`B/C baseline provider: ${DISPLAY_NAME[provider]}\n`);
|
|
103
|
+
process.stdout.write(`B/B2/C baseline provider: ${DISPLAY_NAME[provider]}\n`);
|
|
104
104
|
if (plan.resumedFrom)
|
|
105
105
|
process.stdout.write(`resume: continuing ${plan.resumedFrom} — ${plan.skipCompleted} recorded pair(s) kept\n`);
|
|
106
106
|
process.stdout.write(`to run: ${plan.toRun.length} case×arm pair(s) → ≤${plan.estimatedProviderCalls} nominal provider call(s)\n`);
|