projectinator 0.1.5 → 0.3.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/README.md +19 -12
- package/package.json +4 -4
- package/src/bakeoff.ts +7 -13
- package/src/build-state.ts +3 -2
- package/src/cost.ts +4 -6
- package/src/council.ts +7 -12
- package/src/estimate.ts +7 -0
- package/src/executor.ts +13 -12
- package/src/intake.ts +4 -8
- package/src/models.ts +58 -10
- package/src/narrate.ts +4 -8
- package/src/openrouter.ts +13 -7
- package/src/orchestrator.ts +71 -32
- package/src/pm.ts +14 -15
- package/src/preview.ts +15 -1
- package/src/registry.ts +35 -21
- package/src/research.ts +4 -8
- package/src/retro.ts +2 -2
- package/src/roles.ts +84 -42
- package/src/router.ts +2 -5
- package/src/run-build.ts +20 -8
- package/src/run-dev.ts +2 -5
- package/src/run-pm.ts +3 -5
- package/src/tui/App.tsx +31 -11
- package/src/tui/BoardEditor.tsx +14 -2
- package/src/tui/EditableBoard.tsx +25 -15
- package/src/tui/Kanban.tsx +5 -2
- package/src/tui/Settings.tsx +34 -8
- package/src/tui/components.tsx +5 -3
- package/src/tui/config.ts +17 -2
- package/src/tui/engine.ts +12 -7
- package/src/tui/panels.tsx +1 -1
- package/src/tui.tsx +4 -0
- package/src/types.ts +34 -1
package/src/orchestrator.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
// The executor is INJECTED (RoleExecutor), so this entire control flow is testable
|
|
9
9
|
// offline with a fake — no model, no spend. The real Pi executor lives in roles.ts.
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
import {
|
|
12
|
+
TaskLimitError,
|
|
13
|
+
type RegistryEntry,
|
|
14
|
+
type RoleExecutor,
|
|
15
|
+
type RoutingPolicy,
|
|
16
|
+
type Task,
|
|
17
|
+
type TaskOutcome,
|
|
17
18
|
} from "./types.js";
|
|
18
19
|
import { route } from "./router.js";
|
|
19
20
|
import { REGISTRY } from "./registry.js";
|
|
@@ -62,6 +63,7 @@ export interface RunOptions {
|
|
|
62
63
|
export type OrchestratorEvent =
|
|
63
64
|
| { type: "task_start"; task: Task; round: number; provider: string; modelId: string }
|
|
64
65
|
| { type: "task_done"; outcome: TaskOutcome; runningTotal: number }
|
|
66
|
+
| { type: "task_failed"; outcome: TaskOutcome; runningTotal: number }
|
|
65
67
|
| { type: "task_skipped"; taskId: string }
|
|
66
68
|
| { type: "test_failed"; taskId: string; bugs: number; round: number }
|
|
67
69
|
| { type: "retry_dev"; taskId: string; forTest: string; round: number }
|
|
@@ -107,14 +109,19 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
107
109
|
let running = 0;
|
|
108
110
|
|
|
109
111
|
// Resume: replay prior outcomes so finished tasks are skipped and cost is restored.
|
|
112
|
+
// A failed attempt is billed but never "done" — it is rebuilt.
|
|
110
113
|
const seed = opts.seedOutcomes ?? [];
|
|
111
114
|
for (const o of seed) {
|
|
112
115
|
record.push(o);
|
|
113
|
-
outcomes.
|
|
116
|
+
if (o.error) outcomes.delete(o.taskId); // last wins: a later failure voids an earlier pass
|
|
117
|
+
else outcomes.set(o.taskId, o);
|
|
114
118
|
running += o.cost;
|
|
115
119
|
}
|
|
116
120
|
running = round2(running);
|
|
117
|
-
const wasDone = new Set(
|
|
121
|
+
const wasDone = new Set(outcomes.keys());
|
|
122
|
+
|
|
123
|
+
let halted = false;
|
|
124
|
+
let haltReason: string | undefined;
|
|
118
125
|
|
|
119
126
|
const emit = opts.onProgress ?? (() => {});
|
|
120
127
|
const checkpoint = () => opts.onCheckpoint?.(record, round2(running));
|
|
@@ -132,39 +139,58 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
132
139
|
const decision = route(task, { policy, registry, runningTotalBefore: running });
|
|
133
140
|
emit({ type: "task_start", task, round, provider: decision.provider, modelId: decision.model.id });
|
|
134
141
|
const contextText = contextOverride ?? gatherContext(task, outcomes);
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
const meta = { taskId: task.id, capability: task.capability, provider: decision.provider, modelId: decision.model.id, round };
|
|
143
|
+
let outcome: TaskOutcome;
|
|
144
|
+
try {
|
|
145
|
+
outcome = { ...(await execute({ task, decision, contextText, round, limits: policy.taskLimits })), ...meta };
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (!(e instanceof TaskLimitError)) throw e;
|
|
148
|
+
// Limit breach: bill what was spent, record the failure, halt the build.
|
|
149
|
+
outcome = { finalText: "", files: [], cost: e.costSoFar, error: e.message, ...meta };
|
|
150
|
+
running += outcome.cost;
|
|
151
|
+
record.push(outcome);
|
|
152
|
+
halted = true;
|
|
153
|
+
haltReason = `${task.id} aborted: ${e.message}`;
|
|
154
|
+
emit({ type: "task_failed", outcome, runningTotal: round2(running) });
|
|
155
|
+
return outcome;
|
|
156
|
+
}
|
|
157
|
+
running += outcome.cost;
|
|
145
158
|
outcomes.set(task.id, outcome);
|
|
146
159
|
record.push(outcome);
|
|
147
160
|
emit({ type: "task_done", outcome, runningTotal: round2(running) });
|
|
148
161
|
return outcome;
|
|
149
162
|
};
|
|
150
163
|
|
|
151
|
-
// One task's full lifecycle: run it, then its Tester->Developer feedback loop.
|
|
164
|
+
// One task's full lifecycle: run it, then its Reviewer/Tester -> Developer feedback loop.
|
|
152
165
|
const runTaskUnit = async (task: Task): Promise<void> => {
|
|
153
166
|
let outcome = await runOne(task, 0);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
const judges = task.capability === "test" || task.capability === "review";
|
|
168
|
+
if (!outcome.error && judges && outcome.verdict && !outcome.verdict.passed) {
|
|
169
|
+
// The code to fix: direct code deps, plus code deps reached through a review
|
|
170
|
+
// (a test depends on the review, which depends on the code).
|
|
171
|
+
const codeDeps: Task[] = [];
|
|
172
|
+
for (const id of task.dependsOn ?? []) {
|
|
173
|
+
const dep = byId.get(id);
|
|
174
|
+
if (!dep) continue;
|
|
175
|
+
if (dep.capability === "code") codeDeps.push(dep);
|
|
176
|
+
else if (dep.capability === "review") {
|
|
177
|
+
for (const id2 of dep.dependsOn ?? []) {
|
|
178
|
+
const d2 = byId.get(id2);
|
|
179
|
+
if (d2?.capability === "code" && !codeDeps.includes(d2)) codeDeps.push(d2);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
158
183
|
|
|
159
184
|
let round = 1;
|
|
160
|
-
while (outcome.verdict && !outcome.verdict.passed && round <= policy.maxFeedbackRounds) {
|
|
185
|
+
fix: while (outcome.verdict && !outcome.verdict.passed && round <= policy.maxFeedbackRounds) {
|
|
161
186
|
emit({ type: "test_failed", taskId: task.id, bugs: outcome.verdict.bugs.length, round });
|
|
162
187
|
const fixContext = bugReport(outcome.verdict.bugs);
|
|
163
188
|
for (const dep of codeDeps) {
|
|
164
189
|
emit({ type: "retry_dev", taskId: dep.id, forTest: task.id, round });
|
|
165
|
-
await runOne(dep, round, fixContext);
|
|
190
|
+
if ((await runOne(dep, round, fixContext)).error) break fix;
|
|
166
191
|
}
|
|
167
192
|
outcome = await runOne(task, round); // re-test
|
|
193
|
+
if (outcome.error) break;
|
|
168
194
|
round++;
|
|
169
195
|
}
|
|
170
196
|
}
|
|
@@ -201,6 +227,7 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
201
227
|
return { outcomes: record, totalCost: round2(running), halted: true, haltReason: "budget cap" };
|
|
202
228
|
}
|
|
203
229
|
await runTaskUnit(task);
|
|
230
|
+
if (halted) return { outcomes: record, totalCost: round2(running), halted, haltReason };
|
|
204
231
|
}
|
|
205
232
|
return { outcomes: record, totalCost: round2(running), halted: false };
|
|
206
233
|
}
|
|
@@ -215,8 +242,7 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
215
242
|
const inFlight = new Map<string, Promise<void>>();
|
|
216
243
|
let reserved = 0;
|
|
217
244
|
let codeInFlight = 0; // code tasks are serialized (they share files) even in parallel mode
|
|
218
|
-
let
|
|
219
|
-
let haltReason: string | undefined;
|
|
245
|
+
let failure: unknown; // first task error; rethrown after in-flight work drains
|
|
220
246
|
|
|
221
247
|
const depsSatisfied = (t: Task) => (t.dependsOn ?? []).every((d) => !remaining.has(d));
|
|
222
248
|
const readyTasks = () =>
|
|
@@ -235,8 +261,10 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
235
261
|
if (inFlight.size >= concurrency) break;
|
|
236
262
|
// Only one code task builds at a time — they write to the shared workspace.
|
|
237
263
|
if (task.capability === "code" && codeInFlight >= 1) continue;
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
// Reservations keep full precision: rounding each one to cents drops sub-cent
|
|
265
|
+
// estimates entirely, so a wide backlog of cheap tasks would under-reserve.
|
|
266
|
+
const est = route(task, { policy, registry, runningTotalBefore: running + reserved });
|
|
267
|
+
if (running + reserved + est.cost > policy.budgetCapUSD) {
|
|
240
268
|
if (inFlight.size === 0) {
|
|
241
269
|
emit({ type: "budget_halt", runningTotal: round2(running + est.cost), cap: policy.budgetCapUSD });
|
|
242
270
|
halted = true;
|
|
@@ -244,15 +272,25 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
244
272
|
}
|
|
245
273
|
break; // wait for in-flight tasks to free budget/capacity
|
|
246
274
|
}
|
|
247
|
-
reserved
|
|
275
|
+
reserved += est.cost;
|
|
248
276
|
const cost = est.cost;
|
|
249
277
|
const isCode = task.capability === "code";
|
|
250
278
|
if (isCode) codeInFlight++;
|
|
251
|
-
const
|
|
279
|
+
const settle = () => {
|
|
252
280
|
if (isCode) codeInFlight--;
|
|
253
|
-
reserved
|
|
281
|
+
reserved -= cost;
|
|
254
282
|
remaining.delete(task.id);
|
|
255
283
|
inFlight.delete(task.id);
|
|
284
|
+
};
|
|
285
|
+
// A rejection must NOT escape through Promise.race below: that abandons the
|
|
286
|
+
// sibling promises, and their later rejections would have no handler attached
|
|
287
|
+
// (unhandled rejection -> the host process dies mid-build). Capture the first
|
|
288
|
+
// failure, stop launching, drain what's running, checkpoint, then rethrow.
|
|
289
|
+
const p = runTaskUnit(task).then(settle, (e: unknown) => {
|
|
290
|
+
settle();
|
|
291
|
+
halted = true;
|
|
292
|
+
haltReason ??= e instanceof Error ? e.message : String(e);
|
|
293
|
+
failure ??= e;
|
|
256
294
|
});
|
|
257
295
|
inFlight.set(task.id, p);
|
|
258
296
|
}
|
|
@@ -263,6 +301,7 @@ export async function runBacklog(tasks: Task[], opts: RunOptions): Promise<RunRe
|
|
|
263
301
|
|
|
264
302
|
await Promise.all(inFlight.values());
|
|
265
303
|
checkpoint();
|
|
304
|
+
if (failure) throw failure;
|
|
266
305
|
return { outcomes: record, totalCost: round2(running), halted, haltReason };
|
|
267
306
|
}
|
|
268
307
|
|
package/src/pm.ts
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
// code buckets after decomposition. The PM only decomposes + tags.
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
AuthStorage,
|
|
13
|
-
ModelRegistry,
|
|
14
12
|
createAgentSession,
|
|
15
13
|
defineTool,
|
|
16
14
|
type AgentSession,
|
|
@@ -19,7 +17,7 @@ import { Type, type Static } from "typebox";
|
|
|
19
17
|
import type { Backend, Capability, Difficulty, Provider, Task } from "./types.js";
|
|
20
18
|
import { estimateTokens } from "./estimate.js";
|
|
21
19
|
import { findEntry } from "./registry.js";
|
|
22
|
-
import { resolvePiModel } from "./executor.js";
|
|
20
|
+
import { piRuntime, resolvePiModel } from "./executor.js";
|
|
23
21
|
import { addSessionCost } from "./session-cost.js";
|
|
24
22
|
|
|
25
23
|
// ---- typebox schema = the backlog contract ----
|
|
@@ -31,7 +29,7 @@ const TaskSchema = Type.Object(
|
|
|
31
29
|
{
|
|
32
30
|
id: Type.String({ description: "Unique task id, e.g. T-01" }),
|
|
33
31
|
title: Type.String({ description: "One concrete, buildable unit of work" }),
|
|
34
|
-
capability: Type.String({ description: "one of: plan | design | code | test | ops" }),
|
|
32
|
+
capability: Type.String({ description: "one of: plan | design | code | review | test | ops" }),
|
|
35
33
|
difficulty: Type.String({ description: "one of: trivial | low | medium | high" }),
|
|
36
34
|
dependsOn: Type.Optional(Type.Array(Type.String(), { description: "task ids that must finish first" })),
|
|
37
35
|
epic: Type.Optional(Type.String({ description: "optional grouping label" })),
|
|
@@ -41,7 +39,7 @@ const TaskSchema = Type.Object(
|
|
|
41
39
|
);
|
|
42
40
|
const BacklogSchema = Type.Object({ tasks: Type.Array(TaskSchema) }, { additionalProperties: true });
|
|
43
41
|
|
|
44
|
-
const CAPS = new Set<Capability>(["plan", "design", "code", "test", "ops"]);
|
|
42
|
+
const CAPS = new Set<Capability>(["plan", "design", "code", "review", "test", "ops"]);
|
|
45
43
|
const DIFFS = new Set<Difficulty>(["trivial", "low", "medium", "high"]);
|
|
46
44
|
function coerceCap(s: string): Capability {
|
|
47
45
|
const v = s?.toLowerCase().trim() as Capability;
|
|
@@ -78,9 +76,10 @@ export function pmSystemPrompt(scope: Scope = "full"): string {
|
|
|
78
76
|
scope === "change"
|
|
79
77
|
? [
|
|
80
78
|
"This is a CHANGE to an EXISTING project whose files are already on disk.",
|
|
81
|
-
"Produce the FEWEST tasks that accomplish the change — usually 1 code task
|
|
82
|
-
"
|
|
83
|
-
"
|
|
79
|
+
"Produce the FEWEST tasks that accomplish the change — usually 1 code task plus 1",
|
|
80
|
+
"`review` task that dependsOn it (cheap read-only wiring check), plus 1 test task",
|
|
81
|
+
"(dependsOn the review) only if the change is risky. Do NOT re-plan the whole project,",
|
|
82
|
+
"do NOT add design/setup/deploy tasks. One small tweak = code + review.",
|
|
84
83
|
]
|
|
85
84
|
: [
|
|
86
85
|
"Scale the number of tasks to the request. A tiny page = a few tasks; a full app = many.",
|
|
@@ -92,6 +91,9 @@ export function pmSystemPrompt(scope: Scope = "full"): string {
|
|
|
92
91
|
"src/components/Header.jsx') and keep file names CONSISTENT across tasks — decide one",
|
|
93
92
|
"structure and reuse it. When several files must agree, add ONE early design task that",
|
|
94
93
|
"defines the file tree, and have the code tasks depend on it.",
|
|
94
|
+
"After EVERY code task add one `review` task that dependsOn that code task (a cheap",
|
|
95
|
+
"read-only wiring check). The test task must dependsOn the review task(s), not the code",
|
|
96
|
+
"task(s) directly. Order: design -> code -> review -> test.",
|
|
95
97
|
];
|
|
96
98
|
return [
|
|
97
99
|
"You are the PROJECT MANAGER on an autonomous software team.",
|
|
@@ -100,7 +102,7 @@ export function pmSystemPrompt(scope: Scope = "full"): string {
|
|
|
100
102
|
"",
|
|
101
103
|
"Each TASK must be:",
|
|
102
104
|
"- atomic: one model can complete it in one focused turn",
|
|
103
|
-
"- tagged with a capability: plan | design | code | test | ops",
|
|
105
|
+
"- tagged with a capability: plan | design | code | review | test | ops",
|
|
104
106
|
"- tagged with a difficulty: trivial | low | medium | high (how hard the thinking is)",
|
|
105
107
|
"Optional per task: dependsOn (ids that must finish first, e.g. code depends on design),",
|
|
106
108
|
"and epic/story labels for grouping. Use ids like T-01, unique across the list.",
|
|
@@ -211,7 +213,6 @@ export function extractBacklogFromText(text: string): Backlog | undefined {
|
|
|
211
213
|
|
|
212
214
|
export interface DecomposeOptions {
|
|
213
215
|
backend: Backend;
|
|
214
|
-
authStorage?: AuthStorage;
|
|
215
216
|
thinkingLevel?: "off" | "low" | "medium" | "high";
|
|
216
217
|
onEvent?: Parameters<import("@earendil-works/pi-coding-agent").AgentSession["subscribe"]>[0];
|
|
217
218
|
/** Override the PM model (else resolved from registry plan/mid). */
|
|
@@ -233,19 +234,17 @@ export interface DecomposeResult {
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
export async function decomposeIdea(idea: string, opts: DecomposeOptions): Promise<DecomposeResult> {
|
|
236
|
-
const
|
|
237
|
-
const registry = ModelRegistry.create(authStorage);
|
|
237
|
+
const runtime = await piRuntime();
|
|
238
238
|
|
|
239
239
|
// PM = plan capability, mid tier — unless the caller overrides the model.
|
|
240
240
|
const { entry } = findEntry("plan", "mid");
|
|
241
241
|
const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
|
|
242
|
-
const model = resolvePiModel(
|
|
242
|
+
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
243
243
|
|
|
244
244
|
const { tool, get } = buildBacklogTool();
|
|
245
245
|
const { session } = await createAgentSession({
|
|
246
246
|
model,
|
|
247
|
-
|
|
248
|
-
modelRegistry: registry,
|
|
247
|
+
modelRuntime: runtime,
|
|
249
248
|
thinkingLevel: opts.thinkingLevel ?? "medium",
|
|
250
249
|
noTools: "all",
|
|
251
250
|
customTools: [tool],
|
package/src/preview.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// relative paths all resolve the way they will in production.
|
|
11
11
|
|
|
12
12
|
import { createServer, type Server } from "node:http";
|
|
13
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
14
14
|
import { extname, join, normalize } from "node:path";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
|
|
@@ -145,6 +145,20 @@ async function renderOne(
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Whether the tester can actually run apps: Playwright's Chromium is installed.
|
|
149
|
+
* No launch, just the executable lookup — cheap enough to call per task.
|
|
150
|
+
* Dynamic import on purpose (same as renderCheck): playwright is optional. */
|
|
151
|
+
export async function chromiumAvailable(): Promise<boolean> {
|
|
152
|
+
try {
|
|
153
|
+
const { chromium } = await import("playwright");
|
|
154
|
+
return existsSync(chromium.executablePath());
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const CHROMIUM_INSTALL_HINT = "run `npx playwright install chromium` to enable real test execution";
|
|
161
|
+
|
|
148
162
|
/** Load a built page in headless Chromium and report what actually happened —
|
|
149
163
|
* over http (production-like) AND over file:// (how a user double-clicks it). */
|
|
150
164
|
export async function renderCheck(
|
package/src/registry.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The Model Registry — the swappable brain.
|
|
2
2
|
// Maps capability + tier -> model, per backend. Change an entry, re-route everything.
|
|
3
|
-
// Seeded from the
|
|
3
|
+
// Seeded from the September-2026 verified roster. This is the ONE file the scout edits.
|
|
4
4
|
|
|
5
5
|
import type { Capability, RegistryEntry, Tier } from "./types.js";
|
|
6
6
|
|
|
@@ -18,8 +18,8 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
18
18
|
web: { provider: "openai", model: "gpt-5.6-sol" },
|
|
19
19
|
api: { provider: "openai", model: "gpt-5.6-terra" },
|
|
20
20
|
},
|
|
21
|
-
evidence: "OpenAI leads DeepPlanning long-horizon planning",
|
|
22
|
-
updated: "2026-
|
|
21
|
+
evidence: "OpenAI leads DeepPlanning long-horizon planning; Terra repriced to $2/$12 (Sept 2026)",
|
|
22
|
+
updated: "2026-09-15",
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
// --- DESIGN (UI/UX) ---
|
|
@@ -27,12 +27,12 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
27
27
|
capability: "design",
|
|
28
28
|
tier: "high",
|
|
29
29
|
byBackend: {
|
|
30
|
-
web: { provider: "anthropic", model: "claude-fable-5" },
|
|
30
|
+
web: { provider: "anthropic", model: "claude-fable-5-1" },
|
|
31
31
|
api: { provider: "openai", model: "gpt-5.6-sol" },
|
|
32
32
|
},
|
|
33
33
|
ask: true,
|
|
34
|
-
evidence: "Design Arena Elo — Fable 5 #2, GPT-5.6 Sol #3",
|
|
35
|
-
updated: "2026-
|
|
34
|
+
evidence: "Design Arena Elo — Fable 5 #2, GPT-5.6 Sol #3; Sol repriced to $4/$20 (Sept 2026)",
|
|
35
|
+
updated: "2026-09-15",
|
|
36
36
|
},
|
|
37
37
|
|
|
38
38
|
// --- CODE (development) ---
|
|
@@ -40,30 +40,44 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
40
40
|
capability: "code",
|
|
41
41
|
tier: "high",
|
|
42
42
|
byBackend: {
|
|
43
|
-
web: { provider: "anthropic", model: "claude-fable-5" },
|
|
44
|
-
api: { provider: "anthropic", model: "claude-opus-
|
|
43
|
+
web: { provider: "anthropic", model: "claude-fable-5-1" },
|
|
44
|
+
api: { provider: "anthropic", model: "claude-opus-5" }, // same price as Opus 4.8, 96% SWE-bench V
|
|
45
45
|
},
|
|
46
46
|
ask: true,
|
|
47
|
-
evidence: "SWE-bench Verified —
|
|
48
|
-
updated: "2026-
|
|
47
|
+
evidence: "SWE-bench Verified — Opus 5 96% (Opus 4.8 was 88.6%), same $5/$25",
|
|
48
|
+
updated: "2026-09-15",
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
capability: "code",
|
|
52
52
|
tier: "mid",
|
|
53
53
|
byBackend: {
|
|
54
|
-
web: { provider: "anthropic", model: "claude-opus-
|
|
55
|
-
api: { provider: "anthropic", model: "claude-sonnet-
|
|
54
|
+
web: { provider: "anthropic", model: "claude-opus-5" },
|
|
55
|
+
api: { provider: "anthropic", model: "claude-sonnet-5" },
|
|
56
56
|
},
|
|
57
|
-
|
|
57
|
+
evidence: "Sonnet 5 — 85.2% SWE-bench V, beats Opus 4.8 on Terminal-Bench 2.1, $2/$10 in Pi's table",
|
|
58
|
+
updated: "2026-09-15",
|
|
58
59
|
},
|
|
59
60
|
{
|
|
60
61
|
capability: "code",
|
|
61
62
|
tier: "fast",
|
|
62
63
|
byBackend: {
|
|
63
|
-
web: { provider: "anthropic", model: "claude-sonnet-
|
|
64
|
-
api: { provider: "
|
|
64
|
+
web: { provider: "anthropic", model: "claude-sonnet-5" },
|
|
65
|
+
api: { provider: "google", model: "gemini-3.8-flash" },
|
|
65
66
|
},
|
|
66
|
-
|
|
67
|
+
evidence: "Gemini 3.8 Flash — 90.8% Terminal-Bench 2.1 at $0.75/$3.75",
|
|
68
|
+
updated: "2026-09-15",
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// --- REVIEW (read-only wiring check before the tester; one row -> every difficulty is cheap) ---
|
|
72
|
+
{
|
|
73
|
+
capability: "review",
|
|
74
|
+
tier: "fast",
|
|
75
|
+
byBackend: {
|
|
76
|
+
web: { provider: "google", model: "gemini-3.8-flash" },
|
|
77
|
+
api: { provider: "google", model: "gemini-3.8-flash" },
|
|
78
|
+
},
|
|
79
|
+
evidence: "Read-only wiring check; strongest cheap model, same pick as test",
|
|
80
|
+
updated: "2026-09-15",
|
|
67
81
|
},
|
|
68
82
|
|
|
69
83
|
// --- TEST (QA / review, high volume -> cheap) ---
|
|
@@ -72,10 +86,10 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
72
86
|
tier: "fast",
|
|
73
87
|
byBackend: {
|
|
74
88
|
web: { provider: "google", model: "gemini-3.1-pro-preview" },
|
|
75
|
-
api: { provider: "google", model: "gemini-3-flash
|
|
89
|
+
api: { provider: "google", model: "gemini-3.8-flash" },
|
|
76
90
|
},
|
|
77
|
-
evidence: "
|
|
78
|
-
updated: "2026-
|
|
91
|
+
evidence: "Gemini 3.8 Flash — 90.8% Terminal-Bench 2.1; +50% over 3 Flash for a much stronger tester",
|
|
92
|
+
updated: "2026-09-15",
|
|
79
93
|
},
|
|
80
94
|
|
|
81
95
|
// --- OPS (Runner: terminal / CI / file-driving autonomy) ---
|
|
@@ -86,8 +100,8 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
86
100
|
web: { provider: "openai", model: "gpt-5.6-sol" },
|
|
87
101
|
api: { provider: "openai", model: "gpt-5.6-sol" },
|
|
88
102
|
},
|
|
89
|
-
evidence: "GPT-5.6 Sol
|
|
90
|
-
updated: "2026-
|
|
103
|
+
evidence: "GPT-5.6 Sol on Terminal-Bench; GPT-6 Astra scores higher (57.9 vs 37.3 on TB 4.0) but 2.5x the price — ops tasks are rare",
|
|
104
|
+
updated: "2026-09-15",
|
|
91
105
|
},
|
|
92
106
|
];
|
|
93
107
|
|
package/src/research.ts
CHANGED
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
// Flow: research report (text) -> extractFindings() -> findings.json -> scout --from
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
AuthStorage,
|
|
12
|
-
ModelRegistry,
|
|
13
11
|
createAgentSession,
|
|
14
12
|
defineTool,
|
|
15
13
|
type AgentSession,
|
|
@@ -17,7 +15,7 @@ import {
|
|
|
17
15
|
import { Type, type Static } from "typebox";
|
|
18
16
|
import type { Provider } from "./types.js";
|
|
19
17
|
import type { Finding } from "./scout.js";
|
|
20
|
-
import { resolvePiModel } from "./executor.js"
|
|
18
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
21
19
|
import { MODELS } from "./models.js";
|
|
22
20
|
|
|
23
21
|
const FindingsSchema = Type.Object({
|
|
@@ -89,19 +87,17 @@ export function extractionPrompt(report: string): string {
|
|
|
89
87
|
|
|
90
88
|
export interface ExtractOptions {
|
|
91
89
|
model: { provider: Provider; model: string };
|
|
92
|
-
authStorage?: AuthStorage;
|
|
93
90
|
onEvent?: Parameters<AgentSession["subscribe"]>[0];
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
/** Extract findings from a report via a model. Spends money (one model call). */
|
|
97
94
|
export async function extractFindings(report: string, opts: ExtractOptions): Promise<Finding[]> {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const model = resolvePiModel(registry, opts.model.provider, opts.model.model);
|
|
95
|
+
const runtime = await piRuntime();
|
|
96
|
+
const model = resolvePiModel(runtime, opts.model.provider, opts.model.model);
|
|
101
97
|
|
|
102
98
|
const { tool, get } = buildFindingsTool();
|
|
103
99
|
const { session } = await createAgentSession({
|
|
104
|
-
model,
|
|
100
|
+
model, modelRuntime: runtime,
|
|
105
101
|
thinkingLevel: "low",
|
|
106
102
|
noTools: "all",
|
|
107
103
|
customTools: [tool],
|
package/src/retro.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// build-state: what passed, what the tester flagged, cost per epic and per
|
|
3
3
|
// model, retries, and the priciest tasks. No model call.
|
|
4
4
|
|
|
5
|
-
import type
|
|
5
|
+
import { completedIds, type BuildState } from "./build-state.js";
|
|
6
6
|
import type { Bug, Difficulty } from "./types.js";
|
|
7
7
|
import { baselineTokens } from "./estimate.js";
|
|
8
8
|
import { estimateCost } from "./cost.js";
|
|
@@ -30,7 +30,7 @@ export function computeRetro(state: BuildState): RetroReport {
|
|
|
30
30
|
const epicById = new Map(state.tasks.map((t) => [t.id, t.epic || "General"]));
|
|
31
31
|
const diffById = new Map(state.tasks.map((t) => [t.id, t.difficulty]));
|
|
32
32
|
const outcomes = state.outcomes;
|
|
33
|
-
const doneIds =
|
|
33
|
+
const doneIds = completedIds(state);
|
|
34
34
|
|
|
35
35
|
// Baseline-predicted cost for each run: static token budget × the model that ran it.
|
|
36
36
|
let estCost = 0;
|