projectinator 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +4 -4
- package/src/bakeoff.ts +7 -13
- 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
|
|
@@ -127,7 +127,7 @@ npm run build -- --live --lock anthropic "idea" # full pipeline on one provide
|
|
|
127
127
|
npm run build -- --live --mini --resume # resume a halted/finished build (skips done tasks)
|
|
128
128
|
npm run build -- --live --mini --task-cap 0.5 --task-timeout 5 # per-task limits (USD / minutes)
|
|
129
129
|
npm run bakeoff -- --capability design "Design a pricing page" # model bake-off
|
|
130
|
-
npm test #
|
|
130
|
+
npm test # 181 tests
|
|
131
131
|
npm run typecheck
|
|
132
132
|
```
|
|
133
133
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectinator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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/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";
|