projectinator 0.2.0 → 0.3.1
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 +27 -9
- package/bin/projectinator.mjs +39 -1
- package/package.json +4 -4
- package/src/bakeoff.ts +7 -13
- package/src/cli.ts +263 -0
- package/src/council.ts +7 -12
- 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/pm.ts +4 -9
- package/src/registry.ts +26 -24
- package/src/research.ts +4 -8
- package/src/roles.ts +7 -13
- package/src/run-build.ts +0 -1
- package/src/run-dev.ts +2 -5
- package/src/run-pm.ts +3 -5
- package/src/tui.tsx +4 -0
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/projectinator)
|
|
8
8
|

|
|
9
|
-

|
|
10
|
+

|
|
11
11
|

|
|
12
12
|

|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ watch it happen from a terminal cockpit: a live board, budget bar, and a standup
|
|
|
22
22
|
|
|
23
23
|
Built on the [Pi](https://pi.dev) agent harness (Node/TypeScript). Bring your own API key.
|
|
24
24
|
|
|
25
|
-
**Install & run** (Node ≥
|
|
25
|
+
**Install & run** (Node ≥ 22.19):
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
npx projectinator # run without installing
|
|
@@ -30,6 +30,8 @@ npx projectinator # run without installing
|
|
|
30
30
|
npm install -g projectinator # then just: projectinator
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`projectinator --help` / `--version` work without opening the app; `projectinator doctor` checks your setup.
|
|
34
|
+
|
|
33
35
|
> **It's a CLI, not a library.** `npm install projectinator` (without `-g`) only drops it into a
|
|
34
36
|
> project's `node_modules` — it won't create a runnable command. Use `npx projectinator` or
|
|
35
37
|
> `npm install -g projectinator`. (If `-g` installs but the command isn't found, npm's global
|
|
@@ -120,16 +122,32 @@ Real, unedited output from a full build, kept in [`examples/`](examples/):
|
|
|
120
122
|
|
|
121
123
|
## CLI (same engine, for scripting/CI)
|
|
122
124
|
|
|
125
|
+
Everything below uses the same workspace as the cockpit, so headless builds show up in the app.
|
|
126
|
+
|
|
123
127
|
```bash
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
projectinator doctor # Node, keys, Chromium, Pi catalog, git
|
|
129
|
+
projectinator build "a tip calculator" --dry-run # plan + estimate only (one PM call)
|
|
130
|
+
projectinator build "a tip calculator" --yes # build without the confirmation prompt
|
|
131
|
+
projectinator build "…" --json --budget 2 --provider anthropic # NDJSON events; cap; lock provider
|
|
132
|
+
projectinator build "…" --task-cap 0.5 --task-timeout 5 # per-task limits (USD / minutes)
|
|
133
|
+
projectinator projects # past builds, status, cost
|
|
134
|
+
projectinator models # the roster as it will run, with prices
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Exit codes: `0` ok · `1` environment problem · `2` bad usage · `3` build halted (resume it in the app).
|
|
138
|
+
|
|
139
|
+
<details>
|
|
140
|
+
<summary>Developer scripts (from a clone)</summary>
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm start # the cockpit
|
|
144
|
+
npm run build -- --live --mini # fixed 4-task build, cheap end-to-end proof (~$0.10)
|
|
145
|
+
npm run build -- --live --mini --resume # resume a halted run
|
|
129
146
|
npm run bakeoff -- --capability design "Design a pricing page" # model bake-off
|
|
130
|
-
npm test #
|
|
147
|
+
npm test # 187 tests
|
|
131
148
|
npm run typecheck
|
|
132
149
|
```
|
|
150
|
+
</details>
|
|
133
151
|
|
|
134
152
|
## Configuration & data
|
|
135
153
|
|
package/bin/projectinator.mjs
CHANGED
|
@@ -2,14 +2,52 @@
|
|
|
2
2
|
// Launcher for `projectinator` / `npx github:smanookian/projectinator`.
|
|
3
3
|
// Runs the Ink TUI (TypeScript) through tsx — no build step, no compiled dist.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
|
|
8
9
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
10
|
const entry = join(root, "src", "tui.tsx");
|
|
10
11
|
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
14
|
+
|
|
15
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
16
|
+
console.log(version);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
20
|
+
console.log(`projectinator ${version} — your AI build team in the terminal
|
|
21
|
+
|
|
22
|
+
Usage
|
|
23
|
+
projectinator open the cockpit (TUI)
|
|
24
|
+
projectinator doctor check Node, API keys, Chromium, Pi catalog, git
|
|
25
|
+
projectinator build "<idea>" [...] plan + build headless (--dry-run, --yes, --json,
|
|
26
|
+
--budget, --provider, --concurrency, --task-cap, --task-timeout)
|
|
27
|
+
projectinator projects list past builds with status and cost
|
|
28
|
+
projectinator models the roster as it will run, with prices
|
|
29
|
+
projectinator --version | --help
|
|
30
|
+
|
|
31
|
+
Setup
|
|
32
|
+
Inside the app: Settings → API keys (Anthropic, OpenAI, Gemini, OpenRouter).
|
|
33
|
+
Keys live in ~/.projectinator/config.json (chmod 0600).
|
|
34
|
+
Optional: npx playwright install chromium → lets the tester run the app headless.
|
|
35
|
+
|
|
36
|
+
Exit codes: 0 ok · 1 environment problem · 2 bad usage · 3 build halted
|
|
37
|
+
Docs: https://github.com/smanookian/projectinator#readme`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const COMMANDS = new Set(["doctor", "build", "projects", "models"]);
|
|
42
|
+
if (args.length && !COMMANDS.has(args[0])) {
|
|
43
|
+
console.error(`projectinator: unknown ${args[0].startsWith("-") ? "option" : "command"} "${args[0]}". Try --help.`);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
// `node --import tsx <entry>` registers tsx's loader, then runs the TS entry.
|
|
12
|
-
|
|
48
|
+
// No command → the TUI; a command → the headless CLI with the remaining args.
|
|
49
|
+
const target = args.length ? join(root, "src", "cli.ts") : entry;
|
|
50
|
+
const res = spawnSync(process.execPath, ["--import", "tsx", target, ...args], {
|
|
13
51
|
stdio: "inherit",
|
|
14
52
|
cwd: root,
|
|
15
53
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectinator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Your AI build team in the terminal — hand it an app idea, a PM model plans a Scrum backlog, and the best model per role designs, codes, and tests it into working files. Bring your own API key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"src"
|
|
37
37
|
],
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
39
|
+
"node": ">=22.19.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"start": "tsx src/tui.tsx",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"vitest": "^2.1.8"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
64
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
65
65
|
"@inkjs/ui": "2.0.0",
|
|
66
66
|
"ink": "7.1.0",
|
|
67
67
|
"ink-spinner": "5.0.0",
|
|
68
68
|
"playwright": "1.61.1",
|
|
69
69
|
"react": "19.2.7",
|
|
70
70
|
"tsx": "4.23.1",
|
|
71
|
-
"typebox": "1.
|
|
71
|
+
"typebox": "1.3.7"
|
|
72
72
|
}
|
|
73
73
|
}
|
package/src/bakeoff.ts
CHANGED
|
@@ -7,15 +7,13 @@
|
|
|
7
7
|
// scoring) is a later step.
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
AuthStorage,
|
|
11
|
-
ModelRegistry,
|
|
12
10
|
createAgentSession,
|
|
13
11
|
defineTool,
|
|
14
12
|
type AgentSession,
|
|
15
13
|
} from "@earendil-works/pi-coding-agent";
|
|
16
14
|
import { Type, type Static } from "typebox";
|
|
17
15
|
import type { Capability, Difficulty, Provider, Task } from "./types.js";
|
|
18
|
-
import { resolvePiModel } from "./executor.js"
|
|
16
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
19
17
|
import { buildRolePrompt } from "./roles.js";
|
|
20
18
|
import { estimateTokens } from "./estimate.js";
|
|
21
19
|
import { addSessionCost } from "./session-cost.js";
|
|
@@ -72,13 +70,11 @@ const id = (c: Candidate) => `${c.provider}/${c.model}`;
|
|
|
72
70
|
async function runCandidate(task: Task, cand: Candidate): Promise<BakeoffEntry> {
|
|
73
71
|
const base: BakeoffEntry = { provider: cand.provider, model: cand.model, output: "", cost: 0, ms: 0, outputTokens: 0 };
|
|
74
72
|
try {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const model = resolvePiModel(registry, cand.provider, cand.model);
|
|
73
|
+
const runtime = await piRuntime();
|
|
74
|
+
const model = resolvePiModel(runtime, cand.provider, cand.model);
|
|
78
75
|
const { session } = await createAgentSession({
|
|
79
76
|
model,
|
|
80
|
-
|
|
81
|
-
modelRegistry: registry,
|
|
77
|
+
modelRuntime: runtime,
|
|
82
78
|
thinkingLevel: "medium",
|
|
83
79
|
noTools: "all",
|
|
84
80
|
});
|
|
@@ -144,14 +140,12 @@ async function judge(task: Task, entries: BakeoffEntry[], judgeCand: Candidate):
|
|
|
144
140
|
|
|
145
141
|
const letters = scored.map((_, i) => String.fromCharCode(65 + i)); // A, B, C…
|
|
146
142
|
const blocks = scored.map((e, i) => `### Option ${letters[i]}\n${e.output}`).join("\n\n");
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const model = resolvePiModel(registry, judgeCand.provider, judgeCand.model);
|
|
143
|
+
const runtime = await piRuntime();
|
|
144
|
+
const model = resolvePiModel(runtime, judgeCand.provider, judgeCand.model);
|
|
150
145
|
const { tool, get } = buildJudgeTool();
|
|
151
146
|
const { session } = await createAgentSession({
|
|
152
147
|
model,
|
|
153
|
-
|
|
154
|
-
modelRegistry: registry,
|
|
148
|
+
modelRuntime: runtime,
|
|
155
149
|
thinkingLevel: "medium",
|
|
156
150
|
noTools: "all",
|
|
157
151
|
customTools: [tool],
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Headless CLI — `projectinator <command>`. Same engine as the cockpit (engine.ts),
|
|
2
|
+
// same workspace root, so builds started here show up in the TUI's project list.
|
|
3
|
+
//
|
|
4
|
+
// doctor check Node, keys, Chromium, Pi catalog, git
|
|
5
|
+
// build "<idea>" [...] plan + build without the TUI
|
|
6
|
+
// projects list past builds
|
|
7
|
+
// models the roster as it will actually run, with prices
|
|
8
|
+
//
|
|
9
|
+
// The launcher (bin/projectinator.mjs) handles --version/--help itself and only
|
|
10
|
+
// spawns this file for a real command, so those stay instant.
|
|
11
|
+
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { accessSync, constants, mkdirSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import type { OrchestratorEvent } from "./orchestrator.js";
|
|
18
|
+
import type { Provider } from "./types.js";
|
|
19
|
+
import { MODELS, getModel } from "./models.js";
|
|
20
|
+
import { REGISTRY } from "./registry.js";
|
|
21
|
+
import { piRuntime, resolvePiModel } from "./executor.js";
|
|
22
|
+
import { chromiumAvailable, CHROMIUM_INSTALL_HINT } from "./preview.js";
|
|
23
|
+
import { applyKeysToEnv, getPrefs, loadConfig, ENV_VAR } from "./tui/config.js";
|
|
24
|
+
import {
|
|
25
|
+
availableProviders,
|
|
26
|
+
effectiveRoster,
|
|
27
|
+
listProjects,
|
|
28
|
+
planBuild,
|
|
29
|
+
startBuild,
|
|
30
|
+
PROVIDER_LABEL,
|
|
31
|
+
} from "./tui/engine.js";
|
|
32
|
+
|
|
33
|
+
const NODE_MIN = "22.19.0";
|
|
34
|
+
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
35
|
+
|
|
36
|
+
// ---- tiny argv parser: `--flag value`, `--flag=value`, `--bool`, positionals ----
|
|
37
|
+
|
|
38
|
+
interface Argv {
|
|
39
|
+
positional: string[];
|
|
40
|
+
flags: Record<string, string | true>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseArgv(args: string[]): Argv {
|
|
44
|
+
const out: Argv = { positional: [], flags: {} };
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
const a = args[i]!;
|
|
47
|
+
if (!a.startsWith("--")) { out.positional.push(a); continue; }
|
|
48
|
+
const eq = a.indexOf("=");
|
|
49
|
+
if (eq > 0) { out.flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (next !== undefined && !next.startsWith("--")) { out.flags[a.slice(2)] = next; i++; }
|
|
52
|
+
else out.flags[a.slice(2)] = true;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function num(v: string | true | undefined, fallback: number): number {
|
|
58
|
+
if (v === undefined || v === true) return fallback;
|
|
59
|
+
const n = parseFloat(v);
|
|
60
|
+
return Number.isFinite(n) ? n : fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function semverGte(a: string, b: string): boolean {
|
|
64
|
+
const pa = a.split(".").map(Number);
|
|
65
|
+
const pb = b.split(".").map(Number);
|
|
66
|
+
for (let i = 0; i < 3; i++) {
|
|
67
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
68
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- doctor ----
|
|
74
|
+
|
|
75
|
+
type Check = { label: string; ok: boolean; detail: string; fatal?: boolean };
|
|
76
|
+
|
|
77
|
+
async function doctor(): Promise<number> {
|
|
78
|
+
const checks: Check[] = [];
|
|
79
|
+
|
|
80
|
+
const node = process.versions.node;
|
|
81
|
+
checks.push({ label: "Node", ok: semverGte(node, NODE_MIN), detail: `v${node} (need ≥ ${NODE_MIN})`, fatal: true });
|
|
82
|
+
|
|
83
|
+
const cfg = loadConfig();
|
|
84
|
+
const providers = availableProviders();
|
|
85
|
+
for (const p of Object.keys(ENV_VAR) as Provider[]) {
|
|
86
|
+
const has = providers.includes(p);
|
|
87
|
+
const src = cfg.keys[p] ? "~/.projectinator/config.json" : has ? "env" : "";
|
|
88
|
+
checks.push({ label: `Key: ${PROVIDER_LABEL[p]}`, ok: has, detail: has ? `set (${src})` : `not set — Settings → API keys, or export ${ENV_VAR[p]}` });
|
|
89
|
+
}
|
|
90
|
+
if (providers.length === 0) checks.push({ label: "Any provider key", ok: false, detail: "no keys at all — nothing can run", fatal: true });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const runtime = await piRuntime();
|
|
94
|
+
const bad: string[] = [];
|
|
95
|
+
for (const e of REGISTRY) for (const b of ["api", "web"] as const) {
|
|
96
|
+
try { resolvePiModel(runtime, e.byBackend[b].provider, e.byBackend[b].model); } catch { bad.push(`${e.capability}/${e.tier}/${b}`); }
|
|
97
|
+
}
|
|
98
|
+
checks.push({ label: "Pi model catalog", ok: bad.length === 0, detail: bad.length ? `unresolved: ${bad.join(", ")}` : `${Object.keys(MODELS).length} models priced, every registry pick resolves`, fatal: true });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
checks.push({ label: "Pi model catalog", ok: false, detail: e instanceof Error ? e.message : String(e), fatal: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const chromium = await chromiumAvailable();
|
|
104
|
+
checks.push({ label: "Headless Chromium", ok: chromium, detail: chromium ? "installed — tester runs the app" : `missing — tests will be code-reading only (PASS*); ${CHROMIUM_INSTALL_HINT}` });
|
|
105
|
+
|
|
106
|
+
const git = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
107
|
+
checks.push({ label: "git", ok: git.status === 0, detail: git.status === 0 ? git.stdout.trim() : "not found — builds won't be versioned (undo/history disabled)" });
|
|
108
|
+
|
|
109
|
+
const home = join(homedir(), ".projectinator");
|
|
110
|
+
try { mkdirSync(home, { recursive: true }); accessSync(home, constants.W_OK); checks.push({ label: "Data dir", ok: true, detail: home }); }
|
|
111
|
+
catch { checks.push({ label: "Data dir", ok: false, detail: `${home} not writable`, fatal: true }); }
|
|
112
|
+
|
|
113
|
+
const prefs = getPrefs();
|
|
114
|
+
checks.push({ label: "Prefs", ok: true, detail: `budget cap ${money(prefs.budgetCapUSD)} · ${prefs.concurrency} at once · task limits ${prefs.taskTimeoutMin || "∞"} min / ${prefs.taskCostCapUSD ? money(prefs.taskCostCapUSD) : "∞"}` });
|
|
115
|
+
|
|
116
|
+
console.log("\n projectinator doctor\n");
|
|
117
|
+
for (const c of checks) console.log(` ${c.ok ? "✓" : c.fatal ? "✗" : "!"} ${c.label.padEnd(22)} ${c.detail}`);
|
|
118
|
+
const fatal = checks.filter((c) => !c.ok && c.fatal);
|
|
119
|
+
const warn = checks.filter((c) => !c.ok && !c.fatal);
|
|
120
|
+
console.log(`\n ${fatal.length ? `${fatal.length} blocking problem${fatal.length === 1 ? "" : "s"}` : "Ready"}${warn.length ? ` · ${warn.length} warning${warn.length === 1 ? "" : "s"}` : ""}\n`);
|
|
121
|
+
return fatal.length ? 1 : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- projects ----
|
|
125
|
+
|
|
126
|
+
function projects(): number {
|
|
127
|
+
const list = listProjects();
|
|
128
|
+
if (!list.length) { console.log("\n No projects yet. Run `projectinator` or `projectinator build \"an idea\"`.\n"); return 0; }
|
|
129
|
+
console.log("");
|
|
130
|
+
for (const p of list) {
|
|
131
|
+
const done = p.state.outcomes.filter((o) => !o.error).length;
|
|
132
|
+
const mark = p.status === "complete" ? "✓" : p.status === "halted" ? "⚠" : "…";
|
|
133
|
+
console.log(` ${mark} ${p.slug.padEnd(40)} ${p.status.padEnd(9)} ${money(p.totalCost).padStart(8)} ${done}/${p.taskCount} tasks`);
|
|
134
|
+
console.log(` ${p.idea.slice(0, 90)}${p.idea.length > 90 ? "…" : ""}`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`\n ${list.length} project${list.length === 1 ? "" : "s"} · ${money(list.reduce((a, p) => a + p.totalCost, 0))} all time · ${join(homedir(), "…")}\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- models ----
|
|
141
|
+
|
|
142
|
+
function models(): number {
|
|
143
|
+
const providers = availableProviders();
|
|
144
|
+
console.log(`\n Roster as it will run now${providers.length ? ` (keys: ${providers.join(", ")})` : " (no keys — best-of-breed picks shown)"}\n`);
|
|
145
|
+
for (const r of effectiveRoster()) {
|
|
146
|
+
const m = r.model ? getModel(r.model) : undefined;
|
|
147
|
+
const price = m ? `$${m.cost.input}/$${m.cost.output} per 1M` : "";
|
|
148
|
+
console.log(` ${r.label.padEnd(18)} ${(r.model ?? "—").padEnd(28)} ${(r.provider ?? "").padEnd(11)} ${price}`);
|
|
149
|
+
}
|
|
150
|
+
console.log("\n Change in the app: Settings → Models. Prices are from Pi's catalog; actual cost is measured per run.\n");
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- build ----
|
|
155
|
+
|
|
156
|
+
function ask(question: string): Promise<string> {
|
|
157
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
158
|
+
const { promise, resolve } = Promise.withResolvers<string>();
|
|
159
|
+
rl.question(question, (a) => { rl.close(); resolve(a.trim()); });
|
|
160
|
+
return promise;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function build(argv: Argv): Promise<number> {
|
|
164
|
+
const idea = argv.positional.join(" ").trim();
|
|
165
|
+
const json = argv.flags.json === true;
|
|
166
|
+
const yes = json || argv.flags.yes === true || argv.flags.y === true;
|
|
167
|
+
const dry = argv.flags["dry-run"] === true;
|
|
168
|
+
const emit = (o: Record<string, unknown>) => { if (json) process.stdout.write(JSON.stringify(o) + "\n"); };
|
|
169
|
+
const say = (s: string) => { if (!json) console.log(s); };
|
|
170
|
+
|
|
171
|
+
if (!idea) { console.error(" build: give an idea, e.g. projectinator build \"a tip calculator\""); return 2; }
|
|
172
|
+
|
|
173
|
+
let providers = availableProviders();
|
|
174
|
+
const lock = argv.flags.provider;
|
|
175
|
+
if (typeof lock === "string") {
|
|
176
|
+
if (!(lock in PROVIDER_LABEL)) { console.error(` build: unknown provider "${lock}" (anthropic | openai | google | openrouter)`); return 2; }
|
|
177
|
+
if (!providers.includes(lock as Provider)) { console.error(` build: no key for ${lock} — export ${ENV_VAR[lock as Provider]} or set it in the app`); return 1; }
|
|
178
|
+
providers = [lock as Provider];
|
|
179
|
+
}
|
|
180
|
+
if (!providers.length) { console.error(" build: no API key found. Run `projectinator doctor`."); return 1; }
|
|
181
|
+
|
|
182
|
+
const prefs = getPrefs();
|
|
183
|
+
const budget = num(argv.flags.budget, prefs.budgetCapUSD);
|
|
184
|
+
const concurrency = Math.max(1, Math.floor(num(argv.flags.concurrency, prefs.concurrency)));
|
|
185
|
+
const taskLimits = {
|
|
186
|
+
timeoutMs: num(argv.flags["task-timeout"], prefs.taskTimeoutMin) * 60_000,
|
|
187
|
+
costCapUSD: num(argv.flags["task-cap"], prefs.taskCostCapUSD),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
say(`\n Planning with the PM (${providers.join("/")})…`);
|
|
191
|
+
const plan = await planBuild(idea, providers);
|
|
192
|
+
emit({ event: "plan", provider: plan.provider, modelId: plan.modelId, estCost: plan.estCost, tasks: plan.tasks });
|
|
193
|
+
say(`\n ${plan.tasks.length} tasks · estimated ${money(plan.estCost)} · cap ${money(budget)}${plan.lock ? ` · locked to ${plan.lock}` : ""}\n`);
|
|
194
|
+
for (const t of plan.tasks) {
|
|
195
|
+
const dep = t.dependsOn?.length ? ` ← ${t.dependsOn.join(", ")}` : "";
|
|
196
|
+
say(` ${t.id.padEnd(6)} ${t.capability.padEnd(7)} ${t.difficulty.padEnd(8)} ${t.title}${dep}`);
|
|
197
|
+
}
|
|
198
|
+
if (plan.estCost > budget) say(`\n ⚠ Estimate exceeds the cap — the build may halt partway.`);
|
|
199
|
+
|
|
200
|
+
if (dry) { say("\n Dry run — nothing built (the PM call above was the only spend).\n"); return 0; }
|
|
201
|
+
if (!yes) {
|
|
202
|
+
const a = await ask(`\n Build it for ~${money(plan.estCost)}? [y/N] `);
|
|
203
|
+
if (!/^y(es)?$/i.test(a)) { say(" Cancelled.\n"); return 0; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const onEvent = (e: OrchestratorEvent) => {
|
|
207
|
+
emit({ event: e.type, ...e });
|
|
208
|
+
if (json) return;
|
|
209
|
+
if (e.type === "task_start") console.log(` ▶ ${e.task.id} [${e.task.capability}] → ${e.provider}/${e.modelId}${e.round ? ` (round ${e.round})` : ""}`);
|
|
210
|
+
else if (e.type === "task_done") console.log(` ✓ ${e.outcome.taskId} ${money(e.outcome.cost)} running ${money(e.runningTotal)}${e.outcome.verdict ? ` ${e.outcome.verdict.passed ? (e.outcome.verdict.runtimeChecked || e.outcome.capability !== "test" ? "PASS" : "PASS* (app not executed)") : "FAIL"}` : ""}`);
|
|
211
|
+
else if (e.type === "task_failed") console.log(` ⛔ ${e.outcome.taskId} aborted: ${e.outcome.error} — billed ${money(e.outcome.cost)}`);
|
|
212
|
+
else if (e.type === "task_skipped") console.log(` · ${e.taskId} skipped`);
|
|
213
|
+
else if (e.type === "test_failed") console.log(` ✗ ${e.taskId} failed (${e.bugs} bugs) — round ${e.round}`);
|
|
214
|
+
else if (e.type === "retry_dev") console.log(` ↻ re-running ${e.taskId} to fix ${e.forTest}`);
|
|
215
|
+
else if (e.type === "budget_halt") console.log(` ⚠ budget halt at ${money(e.runningTotal)} (cap ${money(e.cap)})`);
|
|
216
|
+
else if (e.type === "cycle_or_error") console.log(` ✗ ${e.message}`);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
say(`\n Building…\n`);
|
|
220
|
+
const handle = startBuild(idea, plan, { concurrency, budgetCapUSD: budget, taskLimits, onEvent, mode: "auto" });
|
|
221
|
+
const r = await handle.promise;
|
|
222
|
+
emit({ event: "done", halted: r.halted, haltReason: r.haltReason, totalCost: r.totalCost, files: r.files, workspace: handle.workspace });
|
|
223
|
+
say(`\n ${r.halted ? `⚠ Halted (${r.haltReason ?? "?"})` : "✓ Complete"} · ${money(r.totalCost)} · ${r.files.length} file${r.files.length === 1 ? "" : "s"}`);
|
|
224
|
+
say(` ${handle.workspace}\n`);
|
|
225
|
+
return r.halted ? 3 : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- main ----
|
|
229
|
+
|
|
230
|
+
const USAGE = `Usage: projectinator <command> [options]
|
|
231
|
+
|
|
232
|
+
doctor check Node, API keys, Chromium, Pi catalog, git
|
|
233
|
+
build "<idea>" [options] plan + build headless (same workspace as the app)
|
|
234
|
+
--dry-run plan and estimate only (spends one PM call)
|
|
235
|
+
--yes, -y don't ask before building
|
|
236
|
+
--json NDJSON events on stdout (implies --yes)
|
|
237
|
+
--budget <usd> cap for this build (default: your prefs)
|
|
238
|
+
--provider <name> anthropic|openai|google|openrouter (default: prefs / keys)
|
|
239
|
+
--concurrency <n> tasks at once (default: your prefs)
|
|
240
|
+
--task-cap <usd> per-task cost ceiling (default: your prefs; 0 = off)
|
|
241
|
+
--task-timeout <min> per-task timeout (default: your prefs; 0 = off)
|
|
242
|
+
projects list past builds with status and cost
|
|
243
|
+
models the roster as it will run, with prices
|
|
244
|
+
|
|
245
|
+
Exit codes: 0 ok · 1 environment problem · 2 bad usage · 3 build halted`;
|
|
246
|
+
|
|
247
|
+
export async function main(args: string[]): Promise<number> {
|
|
248
|
+
applyKeysToEnv();
|
|
249
|
+
const argv = parseArgv(args);
|
|
250
|
+
const cmd = argv.positional.shift();
|
|
251
|
+
switch (cmd) {
|
|
252
|
+
case "doctor": return doctor();
|
|
253
|
+
case "projects": return projects();
|
|
254
|
+
case "models": return models();
|
|
255
|
+
case "build": return build(argv);
|
|
256
|
+
case undefined: case "help": console.log(USAGE); return cmd ? 0 : 2;
|
|
257
|
+
default: console.error(`projectinator: unknown command "${cmd}".\n\n${USAGE}`); return 2;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file://").href) {
|
|
262
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
263
|
+
}
|
package/src/council.ts
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
// via the normal decomposer seeded with these epics.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
AuthStorage,
|
|
8
|
-
ModelRegistry,
|
|
9
7
|
createAgentSession,
|
|
10
8
|
defineTool,
|
|
11
9
|
type AgentSession,
|
|
@@ -13,7 +11,7 @@ import {
|
|
|
13
11
|
import { Type, type Static } from "typebox";
|
|
14
12
|
import type { Backend, Provider } from "./types.js";
|
|
15
13
|
import { findEntry } from "./registry.js";
|
|
16
|
-
import { resolvePiModel } from "./executor.js"
|
|
14
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
17
15
|
import { addSessionCost } from "./session-cost.js";
|
|
18
16
|
|
|
19
17
|
export interface Epic {
|
|
@@ -67,17 +65,15 @@ interface Ctx {
|
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
async function runEpicAgent(idea: string, system: string, toolName: string, ctx: Ctx): Promise<Epic[]> {
|
|
70
|
-
const
|
|
71
|
-
const registry = ModelRegistry.create(authStorage);
|
|
68
|
+
const runtime = await piRuntime();
|
|
72
69
|
const { entry } = findEntry("plan", "mid");
|
|
73
70
|
const pick = ctx.modelOverride ?? entry.byBackend[ctx.backend];
|
|
74
71
|
try {
|
|
75
|
-
const model = resolvePiModel(
|
|
72
|
+
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
76
73
|
const { tool, get } = buildEpicsTool(toolName);
|
|
77
74
|
const { session } = await createAgentSession({
|
|
78
75
|
model,
|
|
79
|
-
|
|
80
|
-
modelRegistry: registry,
|
|
76
|
+
modelRuntime: runtime,
|
|
81
77
|
thinkingLevel: "low",
|
|
82
78
|
noTools: "all",
|
|
83
79
|
customTools: [tool],
|
|
@@ -145,15 +141,14 @@ export async function councilEpics(idea: string, ctx: Ctx): Promise<CouncilResul
|
|
|
145
141
|
if (!proposals.length) return { epics: [], proposals: [] };
|
|
146
142
|
|
|
147
143
|
// Synthesize.
|
|
148
|
-
const
|
|
149
|
-
const registry = ModelRegistry.create(authStorage);
|
|
144
|
+
const runtime = await piRuntime();
|
|
150
145
|
const { entry } = findEntry("plan", "mid");
|
|
151
146
|
const pick = ctx.modelOverride ?? entry.byBackend[ctx.backend];
|
|
152
147
|
try {
|
|
153
|
-
const model = resolvePiModel(
|
|
148
|
+
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
154
149
|
const { tool, get } = buildEpicsTool("submit_epics");
|
|
155
150
|
const { session } = await createAgentSession({
|
|
156
|
-
model,
|
|
151
|
+
model, modelRuntime: runtime, thinkingLevel: "low",
|
|
157
152
|
noTools: "all", customTools: [tool], tools: ["submit_epics"],
|
|
158
153
|
});
|
|
159
154
|
try {
|
package/src/executor.ts
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
// hits the provider API and spends money — that path is guarded by the caller.
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
ModelRegistry,
|
|
9
|
+
ModelRuntime,
|
|
11
10
|
createAgentSession,
|
|
12
11
|
type AgentSession,
|
|
13
12
|
type AgentSessionEvent,
|
|
@@ -20,17 +19,23 @@ import { getModel } from "./models.js";
|
|
|
20
19
|
import { addSessionCost } from "./session-cost.js";
|
|
21
20
|
|
|
22
21
|
/** Pi's own Model type, derived so we don't depend on a deep sub-path import. */
|
|
23
|
-
type PiModel = NonNullable<ReturnType<
|
|
22
|
+
export type PiModel = NonNullable<ReturnType<ModelRuntime["getModel"]>>;
|
|
23
|
+
|
|
24
|
+
/** Pi's model/auth runtime: built-in catalog + env keys / ~/.pi/agent/auth.json.
|
|
25
|
+
* Created per call on purpose — a key added in Settings must apply to the next session. */
|
|
26
|
+
export function piRuntime(): Promise<ModelRuntime> {
|
|
27
|
+
return ModelRuntime.create();
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
/** Resolve a Projectinator (provider, modelId) to Pi's executable Model.
|
|
26
31
|
* Offline + free — reads Pi's built-in registry. Throws with a clear message
|
|
27
32
|
* if the id isn't one Pi knows (our ids are kept identical to Pi's on purpose). */
|
|
28
33
|
export function resolvePiModel(
|
|
29
|
-
|
|
34
|
+
runtime: ModelRuntime,
|
|
30
35
|
provider: Provider,
|
|
31
36
|
modelId: string,
|
|
32
37
|
): PiModel {
|
|
33
|
-
const m =
|
|
38
|
+
const m = runtime.getModel(provider, modelId);
|
|
34
39
|
if (!m) {
|
|
35
40
|
throw new Error(
|
|
36
41
|
`Pi has no model "${provider}/${modelId}". ` +
|
|
@@ -47,8 +52,6 @@ export interface ExecuteOptions {
|
|
|
47
52
|
thinkingLevel?: "off" | "low" | "medium" | "high";
|
|
48
53
|
/** Optional progress hook — receives raw Pi session events. */
|
|
49
54
|
onEvent?: (event: AgentSessionEvent) => void;
|
|
50
|
-
/** Override auth (tests). Default resolves env keys / ~/.pi/agent/auth.json. */
|
|
51
|
-
authStorage?: AuthStorage;
|
|
52
55
|
/** Tools the agent may use. Default: the coding set. */
|
|
53
56
|
tools?: string[];
|
|
54
57
|
}
|
|
@@ -95,15 +98,13 @@ export async function executeTask(
|
|
|
95
98
|
decision: RouteDecision,
|
|
96
99
|
opts: ExecuteOptions,
|
|
97
100
|
): Promise<ExecuteResult> {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const model = resolvePiModel(registry, decision.provider, decision.model.id);
|
|
101
|
+
const runtime = await piRuntime();
|
|
102
|
+
const model = resolvePiModel(runtime, decision.provider, decision.model.id);
|
|
101
103
|
|
|
102
104
|
const { session } = await createAgentSession({
|
|
103
105
|
model,
|
|
104
106
|
cwd: opts.workspace,
|
|
105
|
-
|
|
106
|
-
modelRegistry: registry,
|
|
107
|
+
modelRuntime: runtime,
|
|
107
108
|
thinkingLevel: opts.thinkingLevel ?? "medium",
|
|
108
109
|
tools: opts.tools ?? ["read", "write", "edit", "bash", "ls", "grep", "find"],
|
|
109
110
|
});
|
package/src/intake.ts
CHANGED
|
@@ -6,15 +6,13 @@
|
|
|
6
6
|
// schema, coerced/validated in code.
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
AuthStorage,
|
|
10
|
-
ModelRegistry,
|
|
11
9
|
createAgentSession,
|
|
12
10
|
defineTool,
|
|
13
11
|
} from "@earendil-works/pi-coding-agent";
|
|
14
12
|
import { Type, type Static } from "typebox";
|
|
15
13
|
import type { Backend, Provider } from "./types.js";
|
|
16
14
|
import { findEntry } from "./registry.js";
|
|
17
|
-
import { resolvePiModel } from "./executor.js"
|
|
15
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
18
16
|
import { addSessionCost } from "./session-cost.js";
|
|
19
17
|
|
|
20
18
|
const IntakeSchema = Type.Object(
|
|
@@ -78,18 +76,16 @@ export interface AssessOptions {
|
|
|
78
76
|
/** Ask the PM whether the request needs clarification; returns up to 4 questions
|
|
79
77
|
* (empty = clear enough to plan directly). Never throws — returns [] on trouble. */
|
|
80
78
|
export async function assessIntake(idea: string, opts: AssessOptions): Promise<IntakeQuestion[]> {
|
|
81
|
-
const
|
|
82
|
-
const registry = ModelRegistry.create(authStorage);
|
|
79
|
+
const runtime = await piRuntime();
|
|
83
80
|
const { entry } = findEntry("plan", "mid");
|
|
84
81
|
const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
|
|
85
82
|
|
|
86
83
|
try {
|
|
87
|
-
const model = resolvePiModel(
|
|
84
|
+
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
88
85
|
const { tool, get } = buildIntakeTool();
|
|
89
86
|
const { session } = await createAgentSession({
|
|
90
87
|
model,
|
|
91
|
-
|
|
92
|
-
modelRegistry: registry,
|
|
88
|
+
modelRuntime: runtime,
|
|
93
89
|
thinkingLevel: "low",
|
|
94
90
|
noTools: "all",
|
|
95
91
|
customTools: [tool],
|
package/src/models.ts
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
// Model pricing table. Rates USD per 1,000,000 tokens
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// for models whose exact cache rates we haven't pinned — refine against provider docs).
|
|
1
|
+
// Model pricing table. Rates USD per 1,000,000 tokens — copied verbatim from Pi's
|
|
2
|
+
// bundled catalog (pi-coding-agent 0.85.1, Sept 2026); test/executor.test.ts pins every
|
|
3
|
+
// entry against it. Shape mirrors Pi's models.json `cost` block.
|
|
5
4
|
|
|
6
5
|
import type { Model } from "./types.js";
|
|
7
6
|
import { findOpenRouterModel } from "./openrouter.js";
|
|
8
7
|
|
|
9
8
|
export const MODELS: Record<string, Model> = {
|
|
10
|
-
// ---- OpenAI: GPT-5.6 family ----
|
|
9
|
+
// ---- OpenAI: GPT-5.6 family (prices cut Sept 2026; past 272k input costs 2x) ----
|
|
11
10
|
"gpt-5.6-sol": {
|
|
12
11
|
id: "gpt-5.6-sol",
|
|
13
12
|
provider: "openai",
|
|
14
13
|
name: "GPT-5.6 Sol",
|
|
15
14
|
contextWindow: 272_000,
|
|
16
|
-
cost: { input: 5, output: 30, cacheRead: 0.
|
|
15
|
+
cost: { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5, tiers: [{ inputTokensAbove: 272_000, input: 8, output: 30, cacheRead: 0.8, cacheWrite: 10 }] },
|
|
17
16
|
},
|
|
18
17
|
"gpt-5.6-terra": {
|
|
19
18
|
id: "gpt-5.6-terra",
|
|
20
19
|
provider: "openai",
|
|
21
20
|
name: "GPT-5.6 Terra",
|
|
22
21
|
contextWindow: 272_000,
|
|
23
|
-
cost: { input: 2.5, output:
|
|
22
|
+
cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5, tiers: [{ inputTokensAbove: 272_000, input: 4, output: 18, cacheRead: 0.4, cacheWrite: 5 }] },
|
|
24
23
|
},
|
|
25
24
|
"gpt-5.6-luna": {
|
|
26
25
|
id: "gpt-5.6-luna",
|
|
27
26
|
provider: "openai",
|
|
28
27
|
name: "GPT-5.6 Luna",
|
|
29
28
|
contextWindow: 272_000,
|
|
30
|
-
cost: { input:
|
|
29
|
+
cost: { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25, tiers: [{ inputTokensAbove: 272_000, input: 0.4, output: 1.8, cacheRead: 0.04, cacheWrite: 0.5 }] },
|
|
31
30
|
},
|
|
32
31
|
|
|
33
32
|
// ---- Anthropic: Claude ----
|
|
33
|
+
"claude-fable-5-1": {
|
|
34
|
+
id: "claude-fable-5-1",
|
|
35
|
+
provider: "anthropic",
|
|
36
|
+
name: "Claude Fable 5.1",
|
|
37
|
+
contextWindow: 1_000_000,
|
|
38
|
+
cost: { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
|
|
39
|
+
},
|
|
34
40
|
"claude-fable-5": {
|
|
35
41
|
id: "claude-fable-5",
|
|
36
42
|
provider: "anthropic",
|
|
@@ -38,6 +44,13 @@ export const MODELS: Record<string, Model> = {
|
|
|
38
44
|
contextWindow: 1_000_000,
|
|
39
45
|
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
40
46
|
},
|
|
47
|
+
"claude-opus-5": {
|
|
48
|
+
id: "claude-opus-5",
|
|
49
|
+
provider: "anthropic",
|
|
50
|
+
name: "Claude Opus 5",
|
|
51
|
+
contextWindow: 1_000_000,
|
|
52
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
53
|
+
},
|
|
41
54
|
"claude-opus-4-8": {
|
|
42
55
|
id: "claude-opus-4-8",
|
|
43
56
|
provider: "anthropic",
|
|
@@ -45,6 +58,13 @@ export const MODELS: Record<string, Model> = {
|
|
|
45
58
|
contextWindow: 1_000_000,
|
|
46
59
|
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
47
60
|
},
|
|
61
|
+
"claude-sonnet-5": {
|
|
62
|
+
id: "claude-sonnet-5",
|
|
63
|
+
provider: "anthropic",
|
|
64
|
+
name: "Claude Sonnet 5",
|
|
65
|
+
contextWindow: 1_000_000,
|
|
66
|
+
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
67
|
+
},
|
|
48
68
|
"claude-sonnet-4-6": {
|
|
49
69
|
id: "claude-sonnet-4-6",
|
|
50
70
|
provider: "anthropic",
|
|
@@ -77,6 +97,13 @@ export const MODELS: Record<string, Model> = {
|
|
|
77
97
|
tiers: [{ inputTokensAbove: 200_000, input: 4, output: 18, cacheRead: 0.4, cacheWrite: 5 }],
|
|
78
98
|
},
|
|
79
99
|
},
|
|
100
|
+
"gemini-3.8-flash": {
|
|
101
|
+
id: "gemini-3.8-flash",
|
|
102
|
+
provider: "google",
|
|
103
|
+
name: "Gemini 3.8 Flash",
|
|
104
|
+
contextWindow: 1_000_000,
|
|
105
|
+
cost: { input: 0.75, output: 3.75, cacheRead: 0.075 },
|
|
106
|
+
},
|
|
80
107
|
"gemini-3-flash-preview": {
|
|
81
108
|
id: "gemini-3-flash-preview",
|
|
82
109
|
provider: "google",
|
|
@@ -88,6 +115,13 @@ export const MODELS: Record<string, Model> = {
|
|
|
88
115
|
// ---- OpenRouter (one key → frontier models). ids are Pi's OpenRouter-catalog
|
|
89
116
|
// slugs (vendor/model). Pricing mirrors the underlying model (OpenRouter passes
|
|
90
117
|
// it through, ~small margin); ACTUAL cost still comes from Pi per run.
|
|
118
|
+
"anthropic/claude-opus-5": {
|
|
119
|
+
id: "anthropic/claude-opus-5",
|
|
120
|
+
provider: "openrouter",
|
|
121
|
+
name: "Claude Opus 5 (OpenRouter)",
|
|
122
|
+
contextWindow: 1_000_000,
|
|
123
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
124
|
+
},
|
|
91
125
|
"anthropic/claude-opus-4.8": {
|
|
92
126
|
id: "anthropic/claude-opus-4.8",
|
|
93
127
|
provider: "openrouter",
|
|
@@ -95,6 +129,13 @@ export const MODELS: Record<string, Model> = {
|
|
|
95
129
|
contextWindow: 1_000_000,
|
|
96
130
|
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
97
131
|
},
|
|
132
|
+
"anthropic/claude-sonnet-5": {
|
|
133
|
+
id: "anthropic/claude-sonnet-5",
|
|
134
|
+
provider: "openrouter",
|
|
135
|
+
name: "Claude Sonnet 5 (OpenRouter)",
|
|
136
|
+
contextWindow: 1_000_000,
|
|
137
|
+
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
138
|
+
},
|
|
98
139
|
"anthropic/claude-sonnet-4.6": {
|
|
99
140
|
id: "anthropic/claude-sonnet-4.6",
|
|
100
141
|
provider: "openrouter",
|
|
@@ -102,12 +143,19 @@ export const MODELS: Record<string, Model> = {
|
|
|
102
143
|
contextWindow: 1_000_000,
|
|
103
144
|
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
104
145
|
},
|
|
146
|
+
"google/gemini-3.8-flash": {
|
|
147
|
+
id: "google/gemini-3.8-flash",
|
|
148
|
+
provider: "openrouter",
|
|
149
|
+
name: "Gemini 3.8 Flash (OpenRouter)",
|
|
150
|
+
contextWindow: 1_000_000,
|
|
151
|
+
cost: { input: 0.75, output: 3.75, cacheRead: 0.075 },
|
|
152
|
+
},
|
|
105
153
|
"openai/gpt-5.6-luna": {
|
|
106
154
|
id: "openai/gpt-5.6-luna",
|
|
107
155
|
provider: "openrouter",
|
|
108
156
|
name: "GPT-5.6 Luna (OpenRouter)",
|
|
109
|
-
contextWindow:
|
|
110
|
-
cost: { input:
|
|
157
|
+
contextWindow: 1_050_000,
|
|
158
|
+
cost: { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
|
|
111
159
|
},
|
|
112
160
|
};
|
|
113
161
|
|
package/src/narrate.ts
CHANGED
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
// (costs a small call) and cached on the build state by the caller.
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
AuthStorage,
|
|
7
|
-
ModelRegistry,
|
|
8
6
|
createAgentSession,
|
|
9
7
|
type AgentSession,
|
|
10
8
|
} from "@earendil-works/pi-coding-agent";
|
|
11
9
|
import type { Backend, Provider } from "./types.js";
|
|
12
10
|
import type { RetroReport } from "./retro.js";
|
|
13
11
|
import { findEntry } from "./registry.js";
|
|
14
|
-
import { resolvePiModel } from "./executor.js"
|
|
12
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
15
13
|
import { addSessionCost } from "./session-cost.js";
|
|
16
14
|
|
|
17
15
|
function lastAssistantText(session: AgentSession): string {
|
|
@@ -66,16 +64,14 @@ export interface NarrateOptions {
|
|
|
66
64
|
|
|
67
65
|
/** Generate the narrative. Throws on failure (caller shows the error). */
|
|
68
66
|
export async function narrateRetro(report: RetroReport, opts: NarrateOptions): Promise<string> {
|
|
69
|
-
const
|
|
70
|
-
const registry = ModelRegistry.create(authStorage);
|
|
67
|
+
const runtime = await piRuntime();
|
|
71
68
|
const { entry } = findEntry("plan", "mid");
|
|
72
69
|
const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
|
|
73
|
-
const model = resolvePiModel(
|
|
70
|
+
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
74
71
|
|
|
75
72
|
const { session } = await createAgentSession({
|
|
76
73
|
model,
|
|
77
|
-
|
|
78
|
-
modelRegistry: registry,
|
|
74
|
+
modelRuntime: runtime,
|
|
79
75
|
thinkingLevel: "low",
|
|
80
76
|
noTools: "all",
|
|
81
77
|
});
|
package/src/openrouter.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// - openRouterModels() / findOpenRouterModel(): SYNC reads (disk cache, else Pi's
|
|
7
7
|
// built-in list) so cost estimation (getModel) can price any picked model.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { piRuntime } from "./executor.js";
|
|
10
10
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
@@ -20,19 +20,25 @@ const CACHE = join(homedir(), ".projectinator", "openrouter-models.json");
|
|
|
20
20
|
let builtinMemo: ORModel[] | null = null;
|
|
21
21
|
let diskMemo: ORModel[] | null | undefined; // undefined = not read yet, null = no cache
|
|
22
22
|
|
|
23
|
-
/** Pi's built-in OpenRouter catalog — offline,
|
|
23
|
+
/** Pi's built-in OpenRouter catalog — offline, names + pricing. Sync read of the memo;
|
|
24
|
+
* call warmBuiltinOpenRouterModels() once at startup to fill it (Pi's runtime is async). */
|
|
24
25
|
export function builtinOpenRouterModels(): ORModel[] {
|
|
26
|
+
return builtinMemo ?? [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Load Pi's built-in OpenRouter list into the memo. Safe to call repeatedly. */
|
|
30
|
+
export async function warmBuiltinOpenRouterModels(): Promise<ORModel[]> {
|
|
25
31
|
if (builtinMemo) return builtinMemo;
|
|
26
32
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.filter((m) => m.
|
|
33
|
+
const runtime = await piRuntime();
|
|
34
|
+
builtinMemo = runtime
|
|
35
|
+
.getModels("openrouter")
|
|
36
|
+
.filter((m) => !!m.cost)
|
|
31
37
|
.map((m) => ({
|
|
32
38
|
id: m.id,
|
|
33
39
|
name: m.name ?? m.id,
|
|
34
40
|
contextWindow: m.contextWindow ?? 200_000,
|
|
35
|
-
cost: m.cost
|
|
41
|
+
cost: m.cost,
|
|
36
42
|
}))
|
|
37
43
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
44
|
} catch {
|
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 ----
|
|
@@ -215,7 +213,6 @@ export function extractBacklogFromText(text: string): Backlog | undefined {
|
|
|
215
213
|
|
|
216
214
|
export interface DecomposeOptions {
|
|
217
215
|
backend: Backend;
|
|
218
|
-
authStorage?: AuthStorage;
|
|
219
216
|
thinkingLevel?: "off" | "low" | "medium" | "high";
|
|
220
217
|
onEvent?: Parameters<import("@earendil-works/pi-coding-agent").AgentSession["subscribe"]>[0];
|
|
221
218
|
/** Override the PM model (else resolved from registry plan/mid). */
|
|
@@ -237,19 +234,17 @@ export interface DecomposeResult {
|
|
|
237
234
|
}
|
|
238
235
|
|
|
239
236
|
export async function decomposeIdea(idea: string, opts: DecomposeOptions): Promise<DecomposeResult> {
|
|
240
|
-
const
|
|
241
|
-
const registry = ModelRegistry.create(authStorage);
|
|
237
|
+
const runtime = await piRuntime();
|
|
242
238
|
|
|
243
239
|
// PM = plan capability, mid tier — unless the caller overrides the model.
|
|
244
240
|
const { entry } = findEntry("plan", "mid");
|
|
245
241
|
const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
|
|
246
|
-
const model = resolvePiModel(
|
|
242
|
+
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
247
243
|
|
|
248
244
|
const { tool, get } = buildBacklogTool();
|
|
249
245
|
const { session } = await createAgentSession({
|
|
250
246
|
model,
|
|
251
|
-
|
|
252
|
-
modelRegistry: registry,
|
|
247
|
+
modelRuntime: runtime,
|
|
253
248
|
thinkingLevel: opts.thinkingLevel ?? "medium",
|
|
254
249
|
noTools: "all",
|
|
255
250
|
customTools: [tool],
|
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,32 @@ 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",
|
|
67
69
|
},
|
|
68
70
|
|
|
69
71
|
// --- REVIEW (read-only wiring check before the tester; one row -> every difficulty is cheap) ---
|
|
@@ -71,10 +73,10 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
71
73
|
capability: "review",
|
|
72
74
|
tier: "fast",
|
|
73
75
|
byBackend: {
|
|
74
|
-
web: { provider: "google", model: "gemini-3.
|
|
75
|
-
api: { provider: "google", model: "gemini-3-flash
|
|
76
|
+
web: { provider: "google", model: "gemini-3.8-flash" },
|
|
77
|
+
api: { provider: "google", model: "gemini-3.8-flash" },
|
|
76
78
|
},
|
|
77
|
-
evidence: "Read-only
|
|
79
|
+
evidence: "Read-only wiring check; strongest cheap model, same pick as test",
|
|
78
80
|
updated: "2026-09-15",
|
|
79
81
|
},
|
|
80
82
|
|
|
@@ -84,10 +86,10 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
84
86
|
tier: "fast",
|
|
85
87
|
byBackend: {
|
|
86
88
|
web: { provider: "google", model: "gemini-3.1-pro-preview" },
|
|
87
|
-
api: { provider: "google", model: "gemini-3-flash
|
|
89
|
+
api: { provider: "google", model: "gemini-3.8-flash" },
|
|
88
90
|
},
|
|
89
|
-
evidence: "
|
|
90
|
-
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",
|
|
91
93
|
},
|
|
92
94
|
|
|
93
95
|
// --- OPS (Runner: terminal / CI / file-driving autonomy) ---
|
|
@@ -98,8 +100,8 @@ export const REGISTRY: RegistryEntry[] = [
|
|
|
98
100
|
web: { provider: "openai", model: "gpt-5.6-sol" },
|
|
99
101
|
api: { provider: "openai", model: "gpt-5.6-sol" },
|
|
100
102
|
},
|
|
101
|
-
evidence: "GPT-5.6 Sol
|
|
102
|
-
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",
|
|
103
105
|
},
|
|
104
106
|
];
|
|
105
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/roles.ts
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
// the orchestrator's feedback loop depends on.
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
AuthStorage,
|
|
9
|
-
ModelRegistry,
|
|
10
8
|
createAgentSession,
|
|
11
9
|
defineTool,
|
|
12
10
|
type AgentSession,
|
|
@@ -24,7 +22,7 @@ import {
|
|
|
24
22
|
type TaskLimits,
|
|
25
23
|
type Verdict,
|
|
26
24
|
} from "./types.js";
|
|
27
|
-
import { resolvePiModel } from "./executor.js"
|
|
25
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
28
26
|
import { renderCheck, chromiumAvailable, CHROMIUM_INSTALL_HINT } from "./preview.js";
|
|
29
27
|
import { estimateCost } from "./cost.js";
|
|
30
28
|
import { getModel } from "./models.js";
|
|
@@ -161,10 +159,10 @@ function buildVerdictTool(runtimeChecked: () => boolean) {
|
|
|
161
159
|
// that provider's sensible model, so route() resolves everything to it.
|
|
162
160
|
|
|
163
161
|
const PROVIDER_MODELS: Record<Provider, { strong: string; mid: string; cheap: string }> = {
|
|
164
|
-
anthropic: { strong: "claude-opus-
|
|
162
|
+
anthropic: { strong: "claude-opus-5", mid: "claude-sonnet-5", cheap: "claude-haiku-4-5" },
|
|
165
163
|
openai: { strong: "gpt-5.6-sol", mid: "gpt-5.6-terra", cheap: "gpt-5.6-luna" },
|
|
166
|
-
google: { strong: "gemini-3.1-pro-preview", mid: "gemini-3.1-pro-preview", cheap: "gemini-3-flash
|
|
167
|
-
openrouter: { strong: "anthropic/claude-opus-
|
|
164
|
+
google: { strong: "gemini-3.1-pro-preview", mid: "gemini-3.1-pro-preview", cheap: "gemini-3.8-flash" },
|
|
165
|
+
openrouter: { strong: "anthropic/claude-opus-5", mid: "anthropic/claude-sonnet-5", cheap: "google/gemini-3.8-flash" },
|
|
168
166
|
};
|
|
169
167
|
|
|
170
168
|
const CAP_STRENGTH: Record<Capability, "strong" | "mid" | "cheap"> = {
|
|
@@ -201,7 +199,6 @@ export function lockRegistryToProvider(provider: Provider): RegistryEntry[] {
|
|
|
201
199
|
export interface PiExecutorOptions {
|
|
202
200
|
workspace: string;
|
|
203
201
|
backend: Backend;
|
|
204
|
-
authStorage?: AuthStorage;
|
|
205
202
|
thinkingLevel?: "off" | "low" | "medium" | "high";
|
|
206
203
|
onEvent?: Parameters<AgentSession["subscribe"]>[0];
|
|
207
204
|
/** Called when a task falls back from its routed provider to another one. */
|
|
@@ -280,8 +277,6 @@ function listFiles(dir: string): string[] {
|
|
|
280
277
|
/** Build a real RoleExecutor backed by Pi. Each call spends money. Falls back to
|
|
281
278
|
* another key-holding provider when the routed one errors or returns 0 tokens. */
|
|
282
279
|
export function makePiExecutor(opts: PiExecutorOptions): RoleExecutor {
|
|
283
|
-
const authStorage = opts.authStorage ?? AuthStorage.create();
|
|
284
|
-
|
|
285
280
|
// One attempt on a specific provider/model. Returns the result + total tokens
|
|
286
281
|
// (0 tokens = the provider call didn't really happen → treat as a failure).
|
|
287
282
|
const runOnce = async (
|
|
@@ -291,8 +286,8 @@ export function makePiExecutor(opts: PiExecutorOptions): RoleExecutor {
|
|
|
291
286
|
modelId: string,
|
|
292
287
|
limits: TaskLimits,
|
|
293
288
|
): Promise<{ result: RoleResult; tokensTotal: number }> => {
|
|
294
|
-
const
|
|
295
|
-
const model = resolvePiModel(
|
|
289
|
+
const runtime = await piRuntime();
|
|
290
|
+
const model = resolvePiModel(runtime, provider, modelId);
|
|
296
291
|
|
|
297
292
|
const isTest = task.capability === "test";
|
|
298
293
|
const isReview = task.capability === "review";
|
|
@@ -303,8 +298,7 @@ export function makePiExecutor(opts: PiExecutorOptions): RoleExecutor {
|
|
|
303
298
|
const { session } = await createAgentSession({
|
|
304
299
|
model,
|
|
305
300
|
cwd: opts.workspace,
|
|
306
|
-
|
|
307
|
-
modelRegistry: registry,
|
|
301
|
+
modelRuntime: runtime,
|
|
308
302
|
thinkingLevel: opts.thinkingLevel ?? "medium",
|
|
309
303
|
...(isTest
|
|
310
304
|
? { customTools: [verdictTool!.tool, checkTool!.tool], tools: ["read", "bash", "ls", "grep", "find", "check_app", "submit_verdict"] }
|
package/src/run-build.ts
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import { mkdirSync } from "node:fs";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
15
14
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
16
15
|
import type { Provider, Task } from "./types.js";
|
|
17
16
|
import { DEFAULT_POLICY } from "./router.js";
|
package/src/run-dev.ts
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
import { mkdirSync } from "node:fs";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
|
-
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
13
12
|
import type { Task } from "./types.js";
|
|
14
13
|
import { DEFAULT_POLICY, route } from "./router.js";
|
|
15
|
-
import { buildDeveloperPrompt, executeTask, resolvePiModel } from "./executor.js";
|
|
14
|
+
import { buildDeveloperPrompt, executeTask, piRuntime, resolvePiModel } from "./executor.js";
|
|
16
15
|
|
|
17
16
|
const TASK: Task = {
|
|
18
17
|
id: "T-DEV1",
|
|
@@ -29,9 +28,7 @@ const live = process.argv.includes("--live");
|
|
|
29
28
|
const policy = { ...DEFAULT_POLICY, backendMode: "api" as const };
|
|
30
29
|
const decision = route(TASK, { policy });
|
|
31
30
|
|
|
32
|
-
const
|
|
33
|
-
const registry = ModelRegistry.create(auth);
|
|
34
|
-
const piModel = resolvePiModel(registry, decision.provider, decision.model.id); // offline, free
|
|
31
|
+
const piModel = resolvePiModel(await piRuntime(), decision.provider, decision.model.id); // offline, free
|
|
35
32
|
|
|
36
33
|
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
37
34
|
|
package/src/run-pm.ts
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Live decomposition needs an API key (PM routes to an OpenAI model by default).
|
|
7
7
|
|
|
8
|
-
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
10
9
|
import { DEFAULT_POLICY, routeBacklog } from "./router.js";
|
|
11
10
|
import { findEntry } from "./registry.js";
|
|
12
|
-
import { resolvePiModel } from "./executor.js"
|
|
11
|
+
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
13
12
|
import { decomposeIdea, pmSystemPrompt } from "./pm.js";
|
|
14
13
|
|
|
15
14
|
const args = process.argv.slice(2);
|
|
@@ -26,13 +25,12 @@ const idea = args.filter((a) => !consumed.has(a)).join(" ").trim() ||
|
|
|
26
25
|
const backend = "api" as const; // web-login backend not built yet
|
|
27
26
|
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
28
27
|
|
|
29
|
-
const
|
|
30
|
-
const registry = ModelRegistry.create(auth);
|
|
28
|
+
const runtime = await piRuntime();
|
|
31
29
|
const { entry } = findEntry("plan", "mid");
|
|
32
30
|
const pick = pmOverride
|
|
33
31
|
? { provider: pmOverride.split("/")[0] as typeof entry.byBackend[typeof backend]["provider"], model: pmOverride.split("/").slice(1).join("/") }
|
|
34
32
|
: entry.byBackend[backend];
|
|
35
|
-
const pm = resolvePiModel(
|
|
33
|
+
const pm = resolvePiModel(runtime, pick.provider, pick.model); // offline
|
|
36
34
|
|
|
37
35
|
console.log(`\n Projectinator — Phase 3 PM decomposer [${live ? "LIVE" : "DRY"}]\n`);
|
|
38
36
|
console.log(` Idea: ${idea}`);
|
package/src/tui.tsx
CHANGED
|
@@ -8,9 +8,13 @@ import { applyKeysToEnv } from "./tui/config.js";
|
|
|
8
8
|
import { listProjects } from "./tui/engine.js";
|
|
9
9
|
import { sessionCost } from "./session-cost.js";
|
|
10
10
|
import { closeWebSessions } from "./web/session.js";
|
|
11
|
+
import { warmBuiltinOpenRouterModels } from "./openrouter.js";
|
|
11
12
|
|
|
12
13
|
// Load any keys saved via Settings into the environment so Pi picks them up.
|
|
13
14
|
applyKeysToEnv();
|
|
15
|
+
// Pi's catalog is async; fill the OpenRouter pricing memo in the background so the
|
|
16
|
+
// sync cost estimator can price OpenRouter slugs by the time a plan is shown.
|
|
17
|
+
void warmBuiltinOpenRouterModels();
|
|
14
18
|
|
|
15
19
|
const AMBER = "\x1b[38;2;224;167;45m";
|
|
16
20
|
const DIM = "\x1b[2m";
|