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.
@@ -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 type {
12
- RegistryEntry,
13
- RoleExecutor,
14
- RoutingPolicy,
15
- Task,
16
- TaskOutcome,
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.set(o.taskId, o); // last wins (retries overwrite)
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(seed.map((o) => o.taskId));
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 result = await execute({ task, decision, contextText, round });
136
- const outcome: TaskOutcome = {
137
- ...result,
138
- taskId: task.id,
139
- capability: task.capability,
140
- provider: decision.provider,
141
- modelId: decision.model.id,
142
- round,
143
- };
144
- running += result.cost;
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
- if (task.capability === "test" && outcome.verdict && !outcome.verdict.passed) {
155
- const codeDeps = (task.dependsOn ?? [])
156
- .map((id) => byId.get(id))
157
- .filter((t): t is Task => !!t && t.capability === "code");
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 halted = false;
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
- const est = route(task, { policy, registry, runningTotalBefore: round2(running + reserved) });
239
- if (round2(running + reserved + est.cost) > policy.budgetCapUSD) {
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 = round2(reserved + est.cost);
275
+ reserved += est.cost;
248
276
  const cost = est.cost;
249
277
  const isCode = task.capability === "code";
250
278
  if (isCode) codeInFlight++;
251
- const p = runTaskUnit(task).then(() => {
279
+ const settle = () => {
252
280
  if (isCode) codeInFlight--;
253
- reserved = round2(reserved - cost);
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
- "plus 1 test task only if the change is risky. Do NOT re-plan the whole project,",
83
- "do NOT add design/setup/deploy tasks. One small tweak = one task.",
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 authStorage = opts.authStorage ?? AuthStorage.create();
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(registry, pick.provider, pick.model);
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
- authStorage,
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 July-2026 verified roster. This is the ONE file the scout edits.
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-07-15",
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-07-15",
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" }, // free -> 95% SWE-bench ceiling
44
- api: { provider: "anthropic", model: "claude-opus-4-8" }, // paid -> 88.6% value pick
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 — Fable 5 95%, Opus 4.8 88.6%",
48
- updated: "2026-07-15",
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-4-8" },
55
- api: { provider: "anthropic", model: "claude-sonnet-4-6" },
54
+ web: { provider: "anthropic", model: "claude-opus-5" },
55
+ api: { provider: "anthropic", model: "claude-sonnet-5" },
56
56
  },
57
- updated: "2026-07-15",
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-4-6" },
64
- api: { provider: "anthropic", model: "claude-haiku-4-5" },
64
+ web: { provider: "anthropic", model: "claude-sonnet-5" },
65
+ api: { provider: "google", model: "gemini-3.8-flash" },
65
66
  },
66
- updated: "2026-07-15",
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-preview" },
89
+ api: { provider: "google", model: "gemini-3.8-flash" },
76
90
  },
77
- evidence: "Fast tier ~5x cheaper for high-volume review",
78
- updated: "2026-07-15",
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 leads Terminal-Bench 2.1",
90
- updated: "2026-07-15",
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 authStorage = opts.authStorage ?? AuthStorage.create();
99
- const registry = ModelRegistry.create(authStorage);
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, authStorage, modelRegistry: registry,
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 { BuildState } from "./build-state.js";
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 = new Set(outcomes.map((o) => o.taskId));
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;