projectinator 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -12
- package/package.json +4 -4
- package/src/bakeoff.ts +7 -13
- package/src/build-state.ts +3 -2
- package/src/cost.ts +4 -6
- package/src/council.ts +7 -12
- package/src/estimate.ts +7 -0
- package/src/executor.ts +13 -12
- package/src/intake.ts +4 -8
- package/src/models.ts +58 -10
- package/src/narrate.ts +4 -8
- package/src/openrouter.ts +13 -7
- package/src/orchestrator.ts +71 -32
- package/src/pm.ts +14 -15
- package/src/preview.ts +15 -1
- package/src/registry.ts +35 -21
- package/src/research.ts +4 -8
- package/src/retro.ts +2 -2
- package/src/roles.ts +84 -42
- package/src/router.ts +2 -5
- package/src/run-build.ts +20 -8
- package/src/run-dev.ts +2 -5
- package/src/run-pm.ts +3 -5
- package/src/tui/App.tsx +31 -11
- package/src/tui/BoardEditor.tsx +14 -2
- package/src/tui/EditableBoard.tsx +25 -15
- package/src/tui/Kanban.tsx +5 -2
- package/src/tui/Settings.tsx +34 -8
- package/src/tui/components.tsx +5 -3
- package/src/tui/config.ts +17 -2
- package/src/tui/engine.ts +12 -7
- package/src/tui/panels.tsx +1 -1
- package/src/tui.tsx +4 -0
- package/src/types.ts +34 -1
package/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
|
|
@@ -49,7 +49,8 @@ npm start
|
|
|
49
49
|
</details>
|
|
50
50
|
|
|
51
51
|
> **Optional:** `npx playwright install chromium` lets the tester actually run web apps in a
|
|
52
|
-
> headless browser and enables live preview.
|
|
52
|
+
> headless browser and enables live preview. Without it the tester can only read the code —
|
|
53
|
+
> such passes show as **PASS\*** with a warning. Everything else works without it.
|
|
53
54
|
|
|
54
55
|
> Projectinator spends **your** API money. Every screen shows the running cost; you set a
|
|
55
56
|
> budget cap and it halts before crossing it. A tiny landing page is cents; a full app is
|
|
@@ -64,13 +65,15 @@ Type an idea → it plans → you approve → it builds, tests, and hands you wo
|
|
|
64
65
|
- **Best model per role.** Roles bind to a *capability + tier*, never a model name. A
|
|
65
66
|
swappable registry maps capabilities to models — new frontier model next month, edit one
|
|
66
67
|
place, every route updates. Run a **bake-off** to pick empirically.
|
|
67
|
-
- **A real pipeline.** PM decomposes → Designer specs → Developer writes files →
|
|
68
|
-
**runs the app headless and catches real
|
|
68
|
+
- **A real pipeline.** PM decomposes → Designer specs → Developer writes files → **Reviewer**
|
|
69
|
+
(cheap, read-only) checks the wiring → Tester **runs the app headless and catches real
|
|
70
|
+
bugs** → feedback loop re-runs the dev on failure.
|
|
69
71
|
- **Multi-file apps.** Vanilla HTML/CSS/JS or **React (CDN, no build)** — your choice.
|
|
70
72
|
- **The cockpit.** A polished terminal UI: editable board, Kanban, standup, per-task cost,
|
|
71
73
|
live budget bar, desktop notification when done.
|
|
72
74
|
- **Honest cost.** Live spend tracking, per-project budget cap + an alert before the cap,
|
|
73
|
-
and predicted-vs-actual reporting that sharpens
|
|
75
|
+
**per-task timeout and cost ceiling**, and predicted-vs-actual reporting that sharpens
|
|
76
|
+
itself over real runs.
|
|
74
77
|
|
|
75
78
|
## Highlights
|
|
76
79
|
|
|
@@ -79,13 +82,16 @@ Type an idea → it plans → you approve → it builds, tests, and hands you wo
|
|
|
79
82
|
| 🧠 **PM intake** | Vague request? The PM asks 2–4 clarifying questions (with pickable options) before planning. Specific requests skip straight through. |
|
|
80
83
|
| 🏛 **Deep plan (council)** | Opt-in: architect + product + risk leads propose epics in parallel, a synthesizer merges them, you approve, then they expand into the backlog. |
|
|
81
84
|
| 🆚 **Model bake-off** | Run one task across models, an LLM judge scores the outputs, compare cost/latency/quality — save the winner to the registry. |
|
|
82
|
-
|
|
|
85
|
+
| 🔍 **Reviewer** | A cheap read-only model checks each code task's wiring (missing files, unresolved `<script src>`, undefined functions, ES modules on a static site) before the Tester spends a browser run. Fails feed the same Developer fix loop. |
|
|
86
|
+
| 🧪 **Real test execution** | The tester loads the built app in headless Chromium and fails on JS/console errors — not just by reading the code. Without Chromium, passes are marked **PASS\*** so you know the app was never run. |
|
|
83
87
|
| 👁 **Live preview** | Local server + auto-reload; ES modules and fetch resolve like production. |
|
|
84
88
|
| 🚀 **Deploy** | One click to Cloudflare Pages, Vercel, or Netlify (their CLI + your login). |
|
|
85
89
|
| 📤 **Export** | Backlog → Markdown, CSV, **Jira** CSV, **Trello** CSV. |
|
|
86
90
|
| 📜 **Git per build** | The workspace is a git repo; one commit per task. History view + **undo a task**. |
|
|
87
91
|
| 📊 **Analytics** | Retro (with optional AI narrative), burndown, cost by epic/model, and estimate accuracy that self-calibrates from real runs. |
|
|
88
92
|
| 💾 **Templates** | Save a project's brief as a reusable template; import/share as a file. |
|
|
93
|
+
| ⛔ **Task limits** | Per-task timeout and cost ceiling (Settings → Preferences). A runaway task is aborted, billed for what it spent, and the build halts — resumable. |
|
|
94
|
+
| ✎ **Task notes** | Annotate any task on the board (`n`). Yours only — never sent to a model; included in exports. |
|
|
89
95
|
|
|
90
96
|
## How it works
|
|
91
97
|
|
|
@@ -95,8 +101,7 @@ idea
|
|
|
95
101
|
└─ intake? PM asks clarifying questions if the request is vague
|
|
96
102
|
└─ plan mode? Quick (one PM) or Deep (planning council → approve epics)
|
|
97
103
|
└─ decompose → a routed, epic-tagged backlog with a cost estimate
|
|
98
|
-
└─
|
|
99
|
-
└─ build toposort deps · design → code → test · Tester→Dev feedback loop
|
|
104
|
+
└─ build toposort deps · design → code → review → test · Reviewer/Tester→Dev feedback loop
|
|
100
105
|
└─ done working files + retro + deploy/export/preview
|
|
101
106
|
```
|
|
102
107
|
|
|
@@ -120,8 +125,9 @@ npm start # the cockpit (the normal way
|
|
|
120
125
|
npm run build -- --live --mini # cheap end-to-end proof (~$0.10)
|
|
121
126
|
npm run build -- --live --lock anthropic "idea" # full pipeline on one provider
|
|
122
127
|
npm run build -- --live --mini --resume # resume a halted/finished build (skips done tasks)
|
|
128
|
+
npm run build -- --live --mini --task-cap 0.5 --task-timeout 5 # per-task limits (USD / minutes)
|
|
123
129
|
npm run bakeoff -- --capability design "Design a pricing page" # model bake-off
|
|
124
|
-
npm test #
|
|
130
|
+
npm test # 181 tests
|
|
125
131
|
npm run typecheck
|
|
126
132
|
```
|
|
127
133
|
|
|
@@ -129,7 +135,8 @@ npm run typecheck
|
|
|
129
135
|
|
|
130
136
|
- **Keys** are stored at `~/.projectinator/config.json` (chmod 0600) — never in the repo.
|
|
131
137
|
- **Settings** (in the app): API keys, preferred provider, default workflow, default stack,
|
|
132
|
-
model assignments, budget cap + alert %,
|
|
138
|
+
model assignments (incl. the Reviewer), budget cap + alert %, per-task timeout + cost cap,
|
|
139
|
+
estimate accuracy.
|
|
133
140
|
- **User data** lives under `~/.projectinator/` (config, calibration, templates, exports).
|
|
134
141
|
Each build gets its own git-versioned workspace folder (one commit per finished task).
|
|
135
142
|
|
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/build-state.ts
CHANGED
|
@@ -40,7 +40,8 @@ export function loadState(path: string): BuildState | undefined {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** Which task ids are already finished (last outcome wins
|
|
43
|
+
/** Which task ids are already finished (last outcome wins; a failed attempt does not
|
|
44
|
+
* count). Used to skip on resume. */
|
|
44
45
|
export function completedIds(state: BuildState): Set<string> {
|
|
45
|
-
return new Set(state.outcomes.map((o) => o.taskId));
|
|
46
|
+
return new Set(state.outcomes.filter((o) => !o.error).map((o) => o.taskId));
|
|
46
47
|
}
|
package/src/cost.ts
CHANGED
|
@@ -22,7 +22,9 @@ function ratesFor(model: Model, inputTokens: number) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Estimate USD cost for one task on a given model.
|
|
25
|
-
* A fraction of input can be served from cache at the cheaper cacheRead rate.
|
|
25
|
+
* A fraction of input can be served from cache at the cheaper cacheRead rate.
|
|
26
|
+
* Kept to 4 decimals, not cents: a backlog of sub-cent tasks would otherwise
|
|
27
|
+
* estimate to $0.00 and under-reserve budget in the parallel scheduler. */
|
|
26
28
|
export function estimateCost(est: TokenEstimate, model: Model): number {
|
|
27
29
|
const cachedFraction = clamp01(est.cachedInputFraction ?? 0);
|
|
28
30
|
const rate = ratesFor(model, est.input);
|
|
@@ -33,13 +35,9 @@ export function estimateCost(est: TokenEstimate, model: Model): number {
|
|
|
33
35
|
const inputCost = (freshInput / PER_MILLION) * rate.input + (cachedInput / PER_MILLION) * rate.cacheRead;
|
|
34
36
|
const outputCost = (est.output / PER_MILLION) * rate.output;
|
|
35
37
|
|
|
36
|
-
return
|
|
38
|
+
return Math.round((inputCost + outputCost) * 10_000) / 10_000;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
function clamp01(n: number): number {
|
|
40
42
|
return Math.max(0, Math.min(1, n));
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
function round2(n: number): number {
|
|
44
|
-
return Math.round(n * 100) / 100;
|
|
45
|
-
}
|
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/estimate.ts
CHANGED
|
@@ -37,6 +37,13 @@ const BUCKETS: Record<Capability, Record<Difficulty, Bucket>> = {
|
|
|
37
37
|
medium: { input: 25_000, output: 7_000 },
|
|
38
38
|
high: { input: 60_000, output: 14_000 },
|
|
39
39
|
},
|
|
40
|
+
// Review reads the same tree the tester does but never runs it — shorter output.
|
|
41
|
+
review: {
|
|
42
|
+
trivial: { input: 15_000, output: 800 },
|
|
43
|
+
low: { input: 25_000, output: 1_500 },
|
|
44
|
+
medium: { input: 50_000, output: 3_000 },
|
|
45
|
+
high: { input: 80_000, output: 5_000 },
|
|
46
|
+
},
|
|
40
47
|
test: {
|
|
41
48
|
trivial: { input: 15_000, output: 1_200 },
|
|
42
49
|
low: { input: 25_000, output: 2_500 },
|
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 {
|