projectinator 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +7 -6
  2. package/bin/projectinator.mjs +22 -15
  3. package/dist/bakeoff.js +163 -0
  4. package/dist/bakeoff.js.map +1 -0
  5. package/dist/build-state.js +26 -0
  6. package/dist/build-state.js.map +1 -0
  7. package/dist/burndown.js +19 -0
  8. package/dist/burndown.js.map +1 -0
  9. package/dist/calibration.js +86 -0
  10. package/dist/calibration.js.map +1 -0
  11. package/dist/cli.js +289 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/cost.js +35 -0
  14. package/dist/cost.js.map +1 -0
  15. package/dist/council.js +147 -0
  16. package/dist/council.js.map +1 -0
  17. package/dist/demo.js +93 -0
  18. package/dist/demo.js.map +1 -0
  19. package/dist/estimate.js +98 -0
  20. package/dist/estimate.js.map +1 -0
  21. package/dist/executor.js +118 -0
  22. package/dist/executor.js.map +1 -0
  23. package/dist/git.js +82 -0
  24. package/dist/git.js.map +1 -0
  25. package/dist/intake.js +98 -0
  26. package/dist/intake.js.map +1 -0
  27. package/dist/models.js +171 -0
  28. package/dist/models.js.map +1 -0
  29. package/dist/narrate.js +76 -0
  30. package/dist/narrate.js.map +1 -0
  31. package/dist/openrouter.js +112 -0
  32. package/dist/openrouter.js.map +1 -0
  33. package/dist/orchestrator.js +273 -0
  34. package/dist/orchestrator.js.map +1 -0
  35. package/dist/pm.js +245 -0
  36. package/dist/pm.js.map +1 -0
  37. package/dist/preview.js +194 -0
  38. package/dist/preview.js.map +1 -0
  39. package/dist/registry-store.js +38 -0
  40. package/dist/registry-store.js.map +1 -0
  41. package/dist/registry.js +115 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/research.js +98 -0
  44. package/dist/research.js.map +1 -0
  45. package/dist/retro.js +83 -0
  46. package/dist/retro.js.map +1 -0
  47. package/dist/roles.js +357 -0
  48. package/dist/roles.js.map +1 -0
  49. package/dist/router.js +95 -0
  50. package/dist/router.js.map +1 -0
  51. package/dist/run-bakeoff.js +68 -0
  52. package/dist/run-bakeoff.js.map +1 -0
  53. package/dist/run-build.js +193 -0
  54. package/dist/run-build.js.map +1 -0
  55. package/{src/run-dev.ts → dist/run-dev.js} +23 -39
  56. package/dist/run-dev.js.map +1 -0
  57. package/{src/run-pm.ts → dist/run-pm.js} +34 -46
  58. package/dist/run-pm.js.map +1 -0
  59. package/{src/run-research.ts → dist/run-research.js} +19 -30
  60. package/dist/run-research.js.map +1 -0
  61. package/{src/run-scout.ts → dist/run-scout.js} +17 -27
  62. package/dist/run-scout.js.map +1 -0
  63. package/dist/run-web.js +93 -0
  64. package/dist/run-web.js.map +1 -0
  65. package/dist/scout.js +81 -0
  66. package/dist/scout.js.map +1 -0
  67. package/dist/session-cost.js +15 -0
  68. package/dist/session-cost.js.map +1 -0
  69. package/dist/stack.js +34 -0
  70. package/dist/stack.js.map +1 -0
  71. package/dist/stuck.js +21 -0
  72. package/dist/stuck.js.map +1 -0
  73. package/dist/tui/App.js +1269 -0
  74. package/dist/tui/App.js.map +1 -0
  75. package/dist/tui/BakeOff.js +95 -0
  76. package/dist/tui/BakeOff.js.map +1 -0
  77. package/dist/tui/BoardEditor.js +183 -0
  78. package/dist/tui/BoardEditor.js.map +1 -0
  79. package/dist/tui/EditableBoard.js +140 -0
  80. package/dist/tui/EditableBoard.js.map +1 -0
  81. package/dist/tui/Frame.js +78 -0
  82. package/dist/tui/Frame.js.map +1 -0
  83. package/dist/tui/Intake.js +59 -0
  84. package/dist/tui/Intake.js.map +1 -0
  85. package/dist/tui/Kanban.js +75 -0
  86. package/dist/tui/Kanban.js.map +1 -0
  87. package/dist/tui/Settings.js +299 -0
  88. package/dist/tui/Settings.js.map +1 -0
  89. package/dist/tui/StackPick.js +44 -0
  90. package/dist/tui/StackPick.js.map +1 -0
  91. package/dist/tui/WebAccounts.js +131 -0
  92. package/dist/tui/WebAccounts.js.map +1 -0
  93. package/dist/tui/components.js +178 -0
  94. package/dist/tui/components.js.map +1 -0
  95. package/dist/tui/config.js +129 -0
  96. package/dist/tui/config.js.map +1 -0
  97. package/dist/tui/deploy.js +114 -0
  98. package/dist/tui/deploy.js.map +1 -0
  99. package/dist/tui/engine.js +710 -0
  100. package/dist/tui/engine.js.map +1 -0
  101. package/dist/tui/notify.js +47 -0
  102. package/dist/tui/notify.js.map +1 -0
  103. package/dist/tui/panels.js +42 -0
  104. package/dist/tui/panels.js.map +1 -0
  105. package/dist/tui/templates.js +106 -0
  106. package/dist/tui/templates.js.map +1 -0
  107. package/dist/tui/theme.js +42 -0
  108. package/dist/tui/theme.js.map +1 -0
  109. package/dist/tui/validate.js +51 -0
  110. package/dist/tui/validate.js.map +1 -0
  111. package/{src/tui.tsx → dist/tui.js} +23 -33
  112. package/dist/tui.js.map +1 -0
  113. package/dist/types.js +19 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/web/oauth-anthropic.js +179 -0
  116. package/dist/web/oauth-anthropic.js.map +1 -0
  117. package/dist/web/session.js +274 -0
  118. package/dist/web/session.js.map +1 -0
  119. package/package.json +5 -4
  120. package/src/bakeoff.ts +0 -214
  121. package/src/build-state.ts +0 -47
  122. package/src/burndown.ts +0 -35
  123. package/src/calibration.ts +0 -88
  124. package/src/cli.ts +0 -263
  125. package/src/cost.ts +0 -43
  126. package/src/council.ts +0 -175
  127. package/src/demo.ts +0 -106
  128. package/src/estimate.ts +0 -111
  129. package/src/executor.ts +0 -169
  130. package/src/git.ts +0 -72
  131. package/src/intake.ts +0 -125
  132. package/src/models.ts +0 -175
  133. package/src/narrate.ts +0 -87
  134. package/src/openrouter.ts +0 -119
  135. package/src/orchestrator.ts +0 -310
  136. package/src/pm.ts +0 -302
  137. package/src/preview.ts +0 -206
  138. package/src/registry-store.ts +0 -41
  139. package/src/registry.ts +0 -132
  140. package/src/research.ts +0 -123
  141. package/src/retro.ts +0 -99
  142. package/src/roles.ts +0 -401
  143. package/src/router.ts +0 -120
  144. package/src/run-bakeoff.ts +0 -77
  145. package/src/run-build.ts +0 -203
  146. package/src/run-web.ts +0 -87
  147. package/src/scout.ts +0 -121
  148. package/src/session-cost.ts +0 -17
  149. package/src/stack.ts +0 -46
  150. package/src/tui/App.tsx +0 -1760
  151. package/src/tui/BakeOff.tsx +0 -190
  152. package/src/tui/BoardEditor.tsx +0 -260
  153. package/src/tui/EditableBoard.tsx +0 -179
  154. package/src/tui/Frame.tsx +0 -142
  155. package/src/tui/Intake.tsx +0 -111
  156. package/src/tui/Kanban.tsx +0 -158
  157. package/src/tui/Settings.tsx +0 -513
  158. package/src/tui/StackPick.tsx +0 -79
  159. package/src/tui/WebAccounts.tsx +0 -197
  160. package/src/tui/components.tsx +0 -340
  161. package/src/tui/config.ts +0 -150
  162. package/src/tui/deploy.ts +0 -137
  163. package/src/tui/engine.ts +0 -749
  164. package/src/tui/notify.ts +0 -21
  165. package/src/tui/panels.tsx +0 -89
  166. package/src/tui/templates.ts +0 -119
  167. package/src/tui/theme.ts +0 -44
  168. package/src/tui/validate.ts +0 -53
  169. package/src/types.ts +0 -208
  170. package/src/web/oauth-anthropic.ts +0 -206
  171. package/src/web/session.ts +0 -299
package/src/preview.ts DELETED
@@ -1,206 +0,0 @@
1
- // Static-serving + headless render check for built web apps.
2
- //
3
- // Two uses:
4
- // - renderCheck(): load the built app in a headless browser, collect JS/console
5
- // errors + the rendered text, so the TESTER role verifies the app actually
6
- // RUNS (not just that the code reads correctly).
7
- // - startStaticServer(): a tiny local file server, reused by live preview.
8
- //
9
- // A real http server (not file://) so ES modules, fetch of local assets, and
10
- // relative paths all resolve the way they will in production.
11
-
12
- import { createServer, type Server } from "node:http";
13
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
14
- import { extname, join, normalize } from "node:path";
15
- import { pathToFileURL } from "node:url";
16
-
17
- const TYPES: Record<string, string> = {
18
- ".html": "text/html", ".htm": "text/html", ".css": "text/css",
19
- ".js": "text/javascript", ".mjs": "text/javascript", ".json": "application/json",
20
- ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
21
- ".gif": "image/gif", ".webp": "image/webp", ".ico": "image/x-icon",
22
- ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf",
23
- };
24
-
25
- export interface StaticServer {
26
- url: string; // http://127.0.0.1:<port>
27
- port: number;
28
- close: () => Promise<void>;
29
- }
30
-
31
- // Injected into served HTML when liveReload is on: polls /__mtime and reloads
32
- // when any file in the directory changes (so the page refreshes as a build runs).
33
- const RELOAD_SNIPPET = `<script>(function(){let last=null;setInterval(async function(){try{var r=await fetch('/__mtime');var t=await r.text();if(last!==null&&t!==last){location.reload();}last=t;}catch(e){}},1000);})();</script>`;
34
-
35
- /** Newest mtime (ms) across all files in dir — a cheap change signal. */
36
- function maxMtime(dir: string): number {
37
- let max = 0;
38
- const walk = (d: string) => {
39
- let entries: string[];
40
- try { entries = readdirSync(d); } catch { return; }
41
- for (const name of entries) {
42
- if (name.startsWith(".")) continue;
43
- const full = join(d, name);
44
- let st;
45
- try { st = statSync(full); } catch { continue; }
46
- if (st.isDirectory()) walk(full);
47
- else if (st.mtimeMs > max) max = st.mtimeMs;
48
- }
49
- };
50
- walk(dir);
51
- return max;
52
- }
53
-
54
- /** Serve `dir` on a random loopback port. Path traversal is blocked.
55
- * opts.liveReload injects a poller that reloads the page when files change. */
56
- export function startStaticServer(dir: string, opts: { liveReload?: boolean } = {}): Promise<StaticServer> {
57
- return new Promise((resolve, reject) => {
58
- const server: Server = createServer((req, res) => {
59
- try {
60
- const reqPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
61
- if (opts.liveReload && reqPath === "/__mtime") {
62
- res.writeHead(200, { "Content-Type": "text/plain" });
63
- res.end(String(maxMtime(dir)));
64
- return;
65
- }
66
- // Resolve within dir; reject anything that escapes it.
67
- const rel = normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
68
- let filePath = join(dir, rel);
69
- if (!filePath.startsWith(dir)) { res.writeHead(403).end("forbidden"); return; }
70
- let st;
71
- try { st = statSync(filePath); } catch { res.writeHead(404).end("not found"); return; }
72
- if (st.isDirectory()) filePath = join(filePath, "index.html");
73
- const type = TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
74
- if (opts.liveReload && type === "text/html") {
75
- let html = readFileSync(filePath, "utf8");
76
- html = html.includes("</body>") ? html.replace("</body>", `${RELOAD_SNIPPET}</body>`) : html + RELOAD_SNIPPET;
77
- res.writeHead(200, { "Content-Type": type });
78
- res.end(html);
79
- return;
80
- }
81
- res.writeHead(200, { "Content-Type": type });
82
- res.end(readFileSync(filePath));
83
- } catch {
84
- res.writeHead(500).end("error");
85
- }
86
- });
87
- server.on("error", reject);
88
- server.listen(0, "127.0.0.1", () => {
89
- const addr = server.address();
90
- const port = typeof addr === "object" && addr ? addr.port : 0;
91
- resolve({
92
- url: `http://127.0.0.1:${port}`,
93
- port,
94
- close: () => new Promise<void>((r) => server.close(() => r())),
95
- });
96
- });
97
- });
98
- }
99
-
100
- export interface RenderReport {
101
- ok: boolean; // rendered over http with no JS/console errors
102
- file: string;
103
- title: string;
104
- text: string; // rendered body text (trimmed)
105
- errors: string[]; // console errors + uncaught page errors (http)
106
- screenshotPath?: string;
107
- // The way a non-technical user opens the folder: double-click → file://.
108
- // ES modules + relative imports (and fetch of local assets) die here even
109
- // though they work over a server — so we render BOTH and compare.
110
- fileOk: boolean; // rendered over file:// with no errors AND real content
111
- fileText: string; // rendered body text via file://
112
- fileErrors: string[]; // errors seen via file://
113
- // True when the app clearly works over a server but is broken on double-click
114
- // (renders content over http, but blank/erroring over file://). The classic
115
- // "AI shipped an app that only runs behind a server the user won't start".
116
- doubleClickBroken: boolean;
117
- }
118
-
119
- interface OneRender { title: string; text: string; errors: string[]; }
120
-
121
- /** Render a single URL and capture title, visible text, and errors. */
122
- async function renderOne(
123
- browser: import("playwright").Browser,
124
- url: string,
125
- opts: { screenshotPath?: string; timeoutMs?: number } = {},
126
- ): Promise<OneRender> {
127
- const errors: string[] = [];
128
- const page = await browser.newPage();
129
- page.on("console", (m) => { if (m.type() === "error") errors.push(`console.error: ${m.text()}`); });
130
- page.on("pageerror", (e) => errors.push(`uncaught: ${e.message}`));
131
- page.on("requestfailed", (r) => {
132
- const u = r.url();
133
- if (!u.endsWith("/favicon.ico")) errors.push(`failed request: ${u} (${r.failure()?.errorText ?? "?"})`);
134
- });
135
- try {
136
- await page.goto(url, { waitUntil: "networkidle", timeout: opts.timeoutMs ?? 15_000 });
137
- const title = await page.title().catch(() => "");
138
- const text = (await page.locator("body").innerText().catch(() => "")).trim().slice(0, 800);
139
- if (opts.screenshotPath) {
140
- try { await page.screenshot({ path: opts.screenshotPath, fullPage: true }); } catch { /* non-fatal */ }
141
- }
142
- return { title, text, errors };
143
- } finally {
144
- await page.close();
145
- }
146
- }
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
-
162
- /** Load a built page in headless Chromium and report what actually happened —
163
- * over http (production-like) AND over file:// (how a user double-clicks it). */
164
- export async function renderCheck(
165
- dir: string,
166
- file = "index.html",
167
- opts: { screenshotPath?: string; timeoutMs?: number } = {},
168
- ): Promise<RenderReport> {
169
- const { chromium } = await import("playwright");
170
- const server = await startStaticServer(dir);
171
- let browser;
172
- try {
173
- browser = await chromium.launch({ headless: true });
174
- } catch (e) {
175
- await server.close(); // don't leak the port if Chromium can't launch
176
- throw e;
177
- }
178
- try {
179
- const http = await renderOne(browser, `${server.url}/${file}`, opts);
180
- // file:// gets no screenshot — the http render is the one we keep.
181
- const fileUrl = pathToFileURL(join(dir, file)).href;
182
- const fileR = await renderOne(browser, fileUrl, { timeoutMs: opts.timeoutMs });
183
-
184
- const ok = http.errors.length === 0;
185
- const fileHasContent = fileR.text.length > 0;
186
- const fileOk = fileR.errors.length === 0 && fileHasContent;
187
- // Broken-on-double-click = works served, but blank or erroring as a file.
188
- const doubleClickBroken = ok && http.text.length > 0 && !fileOk;
189
-
190
- return {
191
- ok,
192
- file,
193
- title: http.title,
194
- text: http.text,
195
- errors: http.errors,
196
- screenshotPath: opts.screenshotPath,
197
- fileOk,
198
- fileText: fileR.text,
199
- fileErrors: fileR.errors,
200
- doubleClickBroken,
201
- };
202
- } finally {
203
- await browser.close();
204
- await server.close();
205
- }
206
- }
@@ -1,41 +0,0 @@
1
- // Registry persistence — makes the "swappable brain" actually swappable at runtime.
2
- // The in-code REGISTRY is the seed. An optional registry.overrides.json (written by
3
- // the Scout) merges on top by (capability, tier) key. New frontier model next month
4
- // => Scout edits the JSON => every route updates. No code change.
5
-
6
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
- import type { RegistryEntry } from "./types.js";
8
- import { REGISTRY } from "./registry.js";
9
-
10
- export const OVERRIDES_FILENAME = "registry.overrides.json";
11
-
12
- const key = (e: Pick<RegistryEntry, "capability" | "tier">) => `${e.capability}/${e.tier}`;
13
-
14
- /** Merge overrides onto the seed registry, replacing entries by (capability, tier). */
15
- export function mergeRegistry(seed: RegistryEntry[], overrides: RegistryEntry[]): RegistryEntry[] {
16
- const map = new Map(seed.map((e) => [key(e), e]));
17
- for (const o of overrides) map.set(key(o), o);
18
- return [...map.values()];
19
- }
20
-
21
- /** Load the effective registry: seed + overrides file if present. */
22
- export function loadRegistry(overridesPath: string, seed: RegistryEntry[] = REGISTRY): RegistryEntry[] {
23
- if (!existsSync(overridesPath)) return seed;
24
- try {
25
- const parsed = JSON.parse(readFileSync(overridesPath, "utf-8")) as { entries?: RegistryEntry[] };
26
- if (!parsed.entries?.length) return seed;
27
- return mergeRegistry(seed, parsed.entries);
28
- } catch (e) {
29
- throw new Error(`Bad ${OVERRIDES_FILENAME}: ${e instanceof Error ? e.message : e}`);
30
- }
31
- }
32
-
33
- /** Persist override entries (the full set the Scout wants applied). */
34
- export function saveOverrides(entries: RegistryEntry[], overridesPath: string): void {
35
- writeFileSync(overridesPath, JSON.stringify({ updated: nowStamp(), entries }, null, 2) + "\n");
36
- }
37
-
38
- // Deterministic-ish stamp without Date.now (kept simple; callers may override).
39
- function nowStamp(): string {
40
- return "scout";
41
- }
package/src/registry.ts DELETED
@@ -1,132 +0,0 @@
1
- // The Model Registry — the swappable brain.
2
- // Maps capability + tier -> model, per backend. Change an entry, re-route everything.
3
- // Seeded from the September-2026 verified roster. This is the ONE file the scout edits.
4
-
5
- import type { Capability, RegistryEntry, Tier } from "./types.js";
6
-
7
- const TIER_ORDER: Tier[] = ["fast", "mid", "high"];
8
-
9
- // Backend intent:
10
- // web = user's free web subscription -> use the strongest brand model.
11
- // api = metered -> use the cost-appropriate model; ask the user when it matters.
12
- export const REGISTRY: RegistryEntry[] = [
13
- // --- PLAN (PM / decomposition, long-horizon planning) ---
14
- {
15
- capability: "plan",
16
- tier: "mid",
17
- byBackend: {
18
- web: { provider: "openai", model: "gpt-5.6-sol" },
19
- api: { provider: "openai", model: "gpt-5.6-terra" },
20
- },
21
- evidence: "OpenAI leads DeepPlanning long-horizon planning; Terra repriced to $2/$12 (Sept 2026)",
22
- updated: "2026-09-15",
23
- },
24
-
25
- // --- DESIGN (UI/UX) ---
26
- {
27
- capability: "design",
28
- tier: "high",
29
- byBackend: {
30
- web: { provider: "anthropic", model: "claude-fable-5-1" },
31
- api: { provider: "openai", model: "gpt-5.6-sol" },
32
- },
33
- ask: true,
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
- },
37
-
38
- // --- CODE (development) ---
39
- {
40
- capability: "code",
41
- tier: "high",
42
- byBackend: {
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
- },
46
- ask: true,
47
- evidence: "SWE-bench Verified — Opus 5 96% (Opus 4.8 was 88.6%), same $5/$25",
48
- updated: "2026-09-15",
49
- },
50
- {
51
- capability: "code",
52
- tier: "mid",
53
- byBackend: {
54
- web: { provider: "anthropic", model: "claude-opus-5" },
55
- api: { provider: "anthropic", model: "claude-sonnet-5" },
56
- },
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",
59
- },
60
- {
61
- capability: "code",
62
- tier: "fast",
63
- byBackend: {
64
- web: { provider: "anthropic", model: "claude-sonnet-5" },
65
- api: { provider: "google", model: "gemini-3.8-flash" },
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",
81
- },
82
-
83
- // --- TEST (QA / review, high volume -> cheap) ---
84
- {
85
- capability: "test",
86
- tier: "fast",
87
- byBackend: {
88
- web: { provider: "google", model: "gemini-3.1-pro-preview" },
89
- api: { provider: "google", model: "gemini-3.8-flash" },
90
- },
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",
93
- },
94
-
95
- // --- OPS (Runner: terminal / CI / file-driving autonomy) ---
96
- {
97
- capability: "ops",
98
- tier: "high",
99
- byBackend: {
100
- web: { provider: "openai", model: "gpt-5.6-sol" },
101
- api: { provider: "openai", model: "gpt-5.6-sol" },
102
- },
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",
105
- },
106
- ];
107
-
108
- /** Find the best registry entry for a capability at (or near) a tier.
109
- * Exact tier wins; otherwise fall back to the nearest available tier. */
110
- export function findEntry(
111
- capability: Capability,
112
- tier: Tier,
113
- registry: RegistryEntry[] = REGISTRY,
114
- ): { entry: RegistryEntry; exactTier: boolean } {
115
- const forCap = registry.filter((e) => e.capability === capability);
116
- if (forCap.length === 0) {
117
- throw new Error(`No registry entry for capability "${capability}".`);
118
- }
119
-
120
- const exact = forCap.find((e) => e.tier === tier);
121
- if (exact) return { entry: exact, exactTier: true };
122
-
123
- // Nearest tier by distance in TIER_ORDER.
124
- const want = TIER_ORDER.indexOf(tier);
125
- const nearest = forCap
126
- .slice()
127
- .sort(
128
- (a, b) =>
129
- Math.abs(TIER_ORDER.indexOf(a.tier) - want) - Math.abs(TIER_ORDER.indexOf(b.tier) - want),
130
- )[0]!;
131
- return { entry: nearest, exactTier: false };
132
- }
package/src/research.ts DELETED
@@ -1,123 +0,0 @@
1
- // Auto-feed the Scout — turn a research report into structured findings.
2
- //
3
- // The deep-research harness (or any benchmark write-up) produces prose. This module
4
- // extracts it into clean Finding[] the Scout can consume: a model reads the report and
5
- // calls a forced typebox tool. That's reliable because it's reformatting text we give
6
- // it, not recalling facts from memory.
7
- //
8
- // Flow: research report (text) -> extractFindings() -> findings.json -> scout --from
9
-
10
- import {
11
- createAgentSession,
12
- defineTool,
13
- type AgentSession,
14
- } from "@earendil-works/pi-coding-agent";
15
- import { Type, type Static } from "typebox";
16
- import type { Provider } from "./types.js";
17
- import type { Finding } from "./scout.js";
18
- import { piRuntime, resolvePiModel } from "./executor.js"
19
- import { MODELS } from "./models.js";
20
-
21
- const FindingsSchema = Type.Object({
22
- findings: Type.Array(
23
- Type.Object({
24
- capability: Type.Union([
25
- Type.Literal("plan"), Type.Literal("design"), Type.Literal("code"),
26
- Type.Literal("test"), Type.Literal("ops"),
27
- ]),
28
- tier: Type.Union([Type.Literal("fast"), Type.Literal("mid"), Type.Literal("high")]),
29
- backend: Type.Union([Type.Literal("web"), Type.Literal("api")]),
30
- provider: Type.Union([Type.Literal("anthropic"), Type.Literal("openai"), Type.Literal("google")]),
31
- model: Type.String({ description: "exact model id, e.g. claude-opus-4-8" }),
32
- evidence: Type.String({ description: "one-line benchmark/source justification" }),
33
- }),
34
- ),
35
- });
36
- type FindingsRaw = Static<typeof FindingsSchema>;
37
-
38
- export interface ValidationIssue {
39
- index: number;
40
- model: string;
41
- problem: string;
42
- }
43
-
44
- /** Pure check: does each finding reference a real model with a matching provider? */
45
- export function validateFindings(findings: Finding[]): { ok: boolean; issues: ValidationIssue[] } {
46
- const issues: ValidationIssue[] = [];
47
- findings.forEach((f, index) => {
48
- const m = MODELS[f.model];
49
- if (!m) {
50
- issues.push({ index, model: f.model, problem: "model not in models.ts" });
51
- } else if (m.provider !== f.provider) {
52
- issues.push({ index, model: f.model, problem: `provider mismatch (models.ts says ${m.provider})` });
53
- }
54
- });
55
- return { ok: issues.length === 0, issues };
56
- }
57
-
58
- function buildFindingsTool() {
59
- let captured: Finding[] | undefined;
60
- const tool = defineTool({
61
- name: "submit_findings",
62
- label: "Submit Findings",
63
- description: "Submit the extracted model-per-role findings.",
64
- parameters: FindingsSchema,
65
- execute: async (_id, params: FindingsRaw) => {
66
- captured = params.findings as Finding[];
67
- return { content: [{ type: "text", text: `Extracted ${params.findings.length} findings.` }], details: {} };
68
- },
69
- });
70
- return { tool, get: () => captured };
71
- }
72
-
73
- export function extractionPrompt(report: string): string {
74
- return [
75
- "You are a data extractor. From the research report below, extract the single best",
76
- "model for each role the report covers, as structured findings.",
77
- "",
78
- "For each finding set: capability (plan|design|code|test|ops), tier (fast|mid|high),",
79
- "backend (usually 'api' for benchmark-driven picks), provider, the EXACT model id,",
80
- "and a one-line evidence note. Only include roles the report actually supports.",
81
- "Do not invent models. When done, call submit_findings once.",
82
- "",
83
- "--- REPORT ---",
84
- report,
85
- ].join("\n");
86
- }
87
-
88
- export interface ExtractOptions {
89
- model: { provider: Provider; model: string };
90
- onEvent?: Parameters<AgentSession["subscribe"]>[0];
91
- }
92
-
93
- /** Extract findings from a report via a model. Spends money (one model call). */
94
- export async function extractFindings(report: string, opts: ExtractOptions): Promise<Finding[]> {
95
- const runtime = await piRuntime();
96
- const model = resolvePiModel(runtime, opts.model.provider, opts.model.model);
97
-
98
- const { tool, get } = buildFindingsTool();
99
- const { session } = await createAgentSession({
100
- model, modelRuntime: runtime,
101
- thinkingLevel: "low",
102
- noTools: "all",
103
- customTools: [tool],
104
- tools: ["submit_findings"],
105
- });
106
-
107
- const unsub = opts.onEvent ? session.subscribe(opts.onEvent) : undefined;
108
- try {
109
- await session.prompt(extractionPrompt(report));
110
- let out = get();
111
- if (!out) {
112
- await session.followUp("Call submit_findings now.");
113
- out = get();
114
- }
115
- if (!out) throw new Error("Extractor did not call submit_findings.");
116
- return out;
117
- } finally {
118
- unsub?.();
119
- session.dispose();
120
- }
121
- }
122
-
123
- export { FindingsSchema, buildFindingsTool };
package/src/retro.ts DELETED
@@ -1,99 +0,0 @@
1
- // Build retro — a free, data-driven summary of a finished build, computed from
2
- // build-state: what passed, what the tester flagged, cost per epic and per
3
- // model, retries, and the priciest tasks. No model call.
4
-
5
- import { completedIds, type BuildState } from "./build-state.js";
6
- import type { Bug, Difficulty } from "./types.js";
7
- import { baselineTokens } from "./estimate.js";
8
- import { estimateCost } from "./cost.js";
9
- import { getModel } from "./models.js";
10
-
11
- export interface RetroReport {
12
- idea: string;
13
- status: BuildState["status"];
14
- totalCost: number;
15
- estCost: number; // baseline predicted cost for the tasks that ran
16
- taskCount: number;
17
- doneCount: number;
18
- tests: { passed: number; failed: number };
19
- bugs: Bug[]; // everything the tester flagged during the build
20
- retries: { taskId: string; title: string; rounds: number }[];
21
- byEpic: { epic: string; cost: number; tasks: number }[];
22
- byModel: { model: string; cost: number; tasks: number }[];
23
- topCost: { taskId: string; title: string; cost: number }[];
24
- }
25
-
26
- const round2 = (n: number) => Math.round(n * 100) / 100;
27
-
28
- export function computeRetro(state: BuildState): RetroReport {
29
- const titleById = new Map(state.tasks.map((t) => [t.id, t.title]));
30
- const epicById = new Map(state.tasks.map((t) => [t.id, t.epic || "General"]));
31
- const diffById = new Map(state.tasks.map((t) => [t.id, t.difficulty]));
32
- const outcomes = state.outcomes;
33
- const doneIds = completedIds(state);
34
-
35
- // Baseline-predicted cost for each run: static token budget × the model that ran it.
36
- let estCost = 0;
37
- for (const o of outcomes) {
38
- const diff = (diffById.get(o.taskId) ?? "medium") as Difficulty;
39
- try {
40
- const model = getModel(o.modelId);
41
- const tokens = baselineTokens(o.capability, diff);
42
- estCost += estimateCost({ input: tokens.input, output: tokens.output, cachedInputFraction: 0.55 }, model);
43
- } catch {
44
- /* unknown model — skip its estimate */
45
- }
46
- }
47
-
48
- // Tests: judge each test task by its LAST outcome (final state after retries).
49
- const lastTestByTask = new Map<string, (typeof outcomes)[number]>();
50
- for (const o of outcomes) if (o.capability === "test") lastTestByTask.set(o.taskId, o);
51
- let passed = 0;
52
- let failed = 0;
53
- for (const o of lastTestByTask.values()) {
54
- if (o.verdict?.passed) passed++;
55
- else if (o.verdict) failed++;
56
- }
57
-
58
- // Bugs the tester flagged anywhere during the build (signal, even if later fixed).
59
- const bugs: Bug[] = [];
60
- for (const o of outcomes) if (o.verdict?.bugs) bugs.push(...o.verdict.bugs);
61
-
62
- // Retries: any outcome past round 0 means a Tester→Developer rebuild happened.
63
- const roundsByTask = new Map<string, number>();
64
- for (const o of outcomes) if (o.round > 0) roundsByTask.set(o.taskId, Math.max(roundsByTask.get(o.taskId) ?? 0, o.round));
65
- const retries = [...roundsByTask.entries()].map(([taskId, rounds]) => ({ taskId, title: titleById.get(taskId) ?? taskId, rounds }));
66
-
67
- // Cost per epic + per model.
68
- const epicCost = new Map<string, { cost: number; tasks: number }>();
69
- const modelCost = new Map<string, { cost: number; tasks: number }>();
70
- for (const o of outcomes) {
71
- const e = epicById.get(o.taskId) ?? "General";
72
- const ec = epicCost.get(e) ?? { cost: 0, tasks: 0 };
73
- epicCost.set(e, { cost: ec.cost + o.cost, tasks: ec.tasks + 1 });
74
- const mc = modelCost.get(o.modelId) ?? { cost: 0, tasks: 0 };
75
- modelCost.set(o.modelId, { cost: mc.cost + o.cost, tasks: mc.tasks + 1 });
76
- }
77
- const byEpic = [...epicCost.entries()].map(([epic, v]) => ({ epic, cost: round2(v.cost), tasks: v.tasks })).sort((a, b) => b.cost - a.cost);
78
- const byModel = [...modelCost.entries()].map(([model, v]) => ({ model, cost: round2(v.cost), tasks: v.tasks })).sort((a, b) => b.cost - a.cost);
79
-
80
- const topCost = [...outcomes]
81
- .sort((a, b) => b.cost - a.cost)
82
- .slice(0, 3)
83
- .map((o) => ({ taskId: o.taskId, title: titleById.get(o.taskId) ?? o.taskId, cost: round2(o.cost) }));
84
-
85
- return {
86
- idea: state.idea ?? state.id,
87
- status: state.status,
88
- totalCost: round2(state.totalCost),
89
- estCost: round2(estCost),
90
- taskCount: state.tasks.length,
91
- doneCount: doneIds.size,
92
- tests: { passed, failed },
93
- bugs,
94
- retries,
95
- byEpic,
96
- byModel,
97
- topCost,
98
- };
99
- }