projectinator 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -11
- package/bin/projectinator.mjs +56 -11
- package/dist/bakeoff.js +163 -0
- package/dist/bakeoff.js.map +1 -0
- package/dist/build-state.js +26 -0
- package/dist/build-state.js.map +1 -0
- package/dist/burndown.js +19 -0
- package/dist/burndown.js.map +1 -0
- package/dist/calibration.js +86 -0
- package/dist/calibration.js.map +1 -0
- package/dist/cli.js +289 -0
- package/dist/cli.js.map +1 -0
- package/dist/cost.js +35 -0
- package/dist/cost.js.map +1 -0
- package/dist/council.js +147 -0
- package/dist/council.js.map +1 -0
- package/dist/demo.js +93 -0
- package/dist/demo.js.map +1 -0
- package/dist/estimate.js +98 -0
- package/dist/estimate.js.map +1 -0
- package/dist/executor.js +118 -0
- package/dist/executor.js.map +1 -0
- package/dist/git.js +82 -0
- package/dist/git.js.map +1 -0
- package/dist/intake.js +98 -0
- package/dist/intake.js.map +1 -0
- package/dist/models.js +171 -0
- package/dist/models.js.map +1 -0
- package/dist/narrate.js +76 -0
- package/dist/narrate.js.map +1 -0
- package/dist/openrouter.js +112 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/orchestrator.js +273 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/pm.js +245 -0
- package/dist/pm.js.map +1 -0
- package/dist/preview.js +194 -0
- package/dist/preview.js.map +1 -0
- package/dist/registry-store.js +38 -0
- package/dist/registry-store.js.map +1 -0
- package/dist/registry.js +115 -0
- package/dist/registry.js.map +1 -0
- package/dist/research.js +98 -0
- package/dist/research.js.map +1 -0
- package/dist/retro.js +83 -0
- package/dist/retro.js.map +1 -0
- package/dist/roles.js +357 -0
- package/dist/roles.js.map +1 -0
- package/dist/router.js +95 -0
- package/dist/router.js.map +1 -0
- package/dist/run-bakeoff.js +68 -0
- package/dist/run-bakeoff.js.map +1 -0
- package/dist/run-build.js +193 -0
- package/dist/run-build.js.map +1 -0
- package/{src/run-dev.ts → dist/run-dev.js} +23 -39
- package/dist/run-dev.js.map +1 -0
- package/{src/run-pm.ts → dist/run-pm.js} +34 -46
- package/dist/run-pm.js.map +1 -0
- package/{src/run-research.ts → dist/run-research.js} +19 -30
- package/dist/run-research.js.map +1 -0
- package/{src/run-scout.ts → dist/run-scout.js} +17 -27
- package/dist/run-scout.js.map +1 -0
- package/dist/run-web.js +93 -0
- package/dist/run-web.js.map +1 -0
- package/dist/scout.js +81 -0
- package/dist/scout.js.map +1 -0
- package/dist/session-cost.js +15 -0
- package/dist/session-cost.js.map +1 -0
- package/dist/stack.js +34 -0
- package/dist/stack.js.map +1 -0
- package/dist/stuck.js +21 -0
- package/dist/stuck.js.map +1 -0
- package/dist/tui/App.js +1269 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/BakeOff.js +95 -0
- package/dist/tui/BakeOff.js.map +1 -0
- package/dist/tui/BoardEditor.js +183 -0
- package/dist/tui/BoardEditor.js.map +1 -0
- package/dist/tui/EditableBoard.js +140 -0
- package/dist/tui/EditableBoard.js.map +1 -0
- package/dist/tui/Frame.js +78 -0
- package/dist/tui/Frame.js.map +1 -0
- package/dist/tui/Intake.js +59 -0
- package/dist/tui/Intake.js.map +1 -0
- package/dist/tui/Kanban.js +75 -0
- package/dist/tui/Kanban.js.map +1 -0
- package/dist/tui/Settings.js +299 -0
- package/dist/tui/Settings.js.map +1 -0
- package/dist/tui/StackPick.js +44 -0
- package/dist/tui/StackPick.js.map +1 -0
- package/dist/tui/WebAccounts.js +131 -0
- package/dist/tui/WebAccounts.js.map +1 -0
- package/dist/tui/components.js +178 -0
- package/dist/tui/components.js.map +1 -0
- package/dist/tui/config.js +129 -0
- package/dist/tui/config.js.map +1 -0
- package/dist/tui/deploy.js +114 -0
- package/dist/tui/deploy.js.map +1 -0
- package/dist/tui/engine.js +710 -0
- package/dist/tui/engine.js.map +1 -0
- package/dist/tui/notify.js +47 -0
- package/dist/tui/notify.js.map +1 -0
- package/dist/tui/panels.js +42 -0
- package/dist/tui/panels.js.map +1 -0
- package/dist/tui/templates.js +106 -0
- package/dist/tui/templates.js.map +1 -0
- package/dist/tui/theme.js +42 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/validate.js +51 -0
- package/dist/tui/validate.js.map +1 -0
- package/{src/tui.tsx → dist/tui.js} +23 -33
- package/dist/tui.js.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/web/oauth-anthropic.js +179 -0
- package/dist/web/oauth-anthropic.js.map +1 -0
- package/dist/web/session.js +274 -0
- package/dist/web/session.js.map +1 -0
- package/package.json +5 -4
- package/src/bakeoff.ts +0 -214
- package/src/build-state.ts +0 -47
- package/src/burndown.ts +0 -35
- package/src/calibration.ts +0 -88
- package/src/cost.ts +0 -43
- package/src/council.ts +0 -175
- package/src/demo.ts +0 -106
- package/src/estimate.ts +0 -111
- package/src/executor.ts +0 -169
- package/src/git.ts +0 -72
- package/src/intake.ts +0 -125
- package/src/models.ts +0 -175
- package/src/narrate.ts +0 -87
- package/src/openrouter.ts +0 -119
- package/src/orchestrator.ts +0 -310
- package/src/pm.ts +0 -302
- package/src/preview.ts +0 -206
- package/src/registry-store.ts +0 -41
- package/src/registry.ts +0 -132
- package/src/research.ts +0 -123
- package/src/retro.ts +0 -99
- package/src/roles.ts +0 -401
- package/src/router.ts +0 -120
- package/src/run-bakeoff.ts +0 -77
- package/src/run-build.ts +0 -203
- package/src/run-web.ts +0 -87
- package/src/scout.ts +0 -121
- package/src/session-cost.ts +0 -17
- package/src/stack.ts +0 -46
- package/src/tui/App.tsx +0 -1760
- package/src/tui/BakeOff.tsx +0 -190
- package/src/tui/BoardEditor.tsx +0 -260
- package/src/tui/EditableBoard.tsx +0 -179
- package/src/tui/Frame.tsx +0 -142
- package/src/tui/Intake.tsx +0 -111
- package/src/tui/Kanban.tsx +0 -158
- package/src/tui/Settings.tsx +0 -513
- package/src/tui/StackPick.tsx +0 -79
- package/src/tui/WebAccounts.tsx +0 -197
- package/src/tui/components.tsx +0 -340
- package/src/tui/config.ts +0 -150
- package/src/tui/deploy.ts +0 -137
- package/src/tui/engine.ts +0 -749
- package/src/tui/notify.ts +0 -21
- package/src/tui/panels.tsx +0 -89
- package/src/tui/templates.ts +0 -119
- package/src/tui/theme.ts +0 -44
- package/src/tui/validate.ts +0 -53
- package/src/types.ts +0 -208
- package/src/web/oauth-anthropic.ts +0 -206
- package/src/web/session.ts +0 -299
package/src/demo.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Phase 1 demo — proves the brain before wiring any agents.
|
|
2
|
-
// Feeds a fake "landing page + contact form" backlog through the router and prints
|
|
3
|
-
// backend, model, and estimated cost per task, plus the total.
|
|
4
|
-
//
|
|
5
|
-
// Run: npm run demo (cost-first / web backend)
|
|
6
|
-
// npm run demo -- api (force API backend)
|
|
7
|
-
// npm run demo -- ask (interactive-style; here stubbed to demonstrate)
|
|
8
|
-
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
11
|
-
import type { RoutingPolicy, Task } from "./types.js";
|
|
12
|
-
import { DEFAULT_POLICY, routeBacklog } from "./router.js";
|
|
13
|
-
import { loadRegistry, OVERRIDES_FILENAME } from "./registry-store.js";
|
|
14
|
-
|
|
15
|
-
const BACKLOG: Task[] = [
|
|
16
|
-
{
|
|
17
|
-
id: "T-01", epic: "E-1", story: "S-1", title: "Break brief into backlog",
|
|
18
|
-
capability: "plan", difficulty: "medium",
|
|
19
|
-
estTokens: { input: 10_000, output: 5_000 },
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
id: "T-02", epic: "E-1", story: "S-2", title: "Design hero + layout system",
|
|
23
|
-
capability: "design", difficulty: "high",
|
|
24
|
-
estTokens: { input: 20_000, output: 15_000 },
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: "T-03", epic: "E-2", story: "S-3", title: "Build page + contact form",
|
|
28
|
-
capability: "code", difficulty: "high", dependsOn: ["T-02"],
|
|
29
|
-
estTokens: { input: 400_000, output: 60_000, cachedInputFraction: 0.6 },
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: "T-04", epic: "E-2", story: "S-4", title: "Wire form handler",
|
|
33
|
-
capability: "code", difficulty: "low", dependsOn: ["T-03"],
|
|
34
|
-
estTokens: { input: 30_000, output: 8_000, cachedInputFraction: 0.5 },
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "T-05", epic: "E-3", story: "S-5", title: "Review + test the build",
|
|
38
|
-
capability: "test", difficulty: "trivial", dependsOn: ["T-03", "T-04"],
|
|
39
|
-
estTokens: { input: 80_000, output: 15_000 },
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "T-06", epic: "E-3", story: "S-6", title: "Run it, drive the CI/preview",
|
|
43
|
-
capability: "ops", difficulty: "high", dependsOn: ["T-04"],
|
|
44
|
-
estTokens: { input: 100_000, output: 20_000, cachedInputFraction: 0.4 },
|
|
45
|
-
},
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const arg = process.argv[2];
|
|
49
|
-
const policy: RoutingPolicy = { ...DEFAULT_POLICY };
|
|
50
|
-
if (arg === "api") policy.backendMode = "api";
|
|
51
|
-
if (arg === "web") policy.backendMode = "web";
|
|
52
|
-
|
|
53
|
-
// For "ask" mode we'd wire real prompts; here we stub a chooser so the demo runs headless.
|
|
54
|
-
const prompts =
|
|
55
|
-
arg === "ask"
|
|
56
|
-
? {
|
|
57
|
-
chooseBackend: () => "api" as const,
|
|
58
|
-
chooseModel: (_t: Task, _e: unknown, _b: unknown) => undefined, // accept defaults
|
|
59
|
-
}
|
|
60
|
-
: undefined;
|
|
61
|
-
if (arg === "ask") policy.backendMode = "ask";
|
|
62
|
-
|
|
63
|
-
// Load the effective registry (seed + any Scout overrides), so applied Scout
|
|
64
|
-
// changes are reflected here.
|
|
65
|
-
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
66
|
-
const registry = loadRegistry(join(projectRoot, OVERRIDES_FILENAME));
|
|
67
|
-
|
|
68
|
-
const decisions = routeBacklog(BACKLOG, { policy, registry, prompts });
|
|
69
|
-
|
|
70
|
-
const pad = (s: string, n: number) => s.padEnd(n);
|
|
71
|
-
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
72
|
-
|
|
73
|
-
console.log(`\n Projectinator — routing plan [backendMode=${policy.backendMode}, cap=${money(policy.budgetCapUSD)}]\n`);
|
|
74
|
-
console.log(
|
|
75
|
-
" " +
|
|
76
|
-
pad("TASK", 7) + pad("CAPABILITY", 11) + pad("DIFF", 9) +
|
|
77
|
-
pad("BACKEND", 9) + pad("MODEL", 20) + pad("TIER", 6) + pad("COST", 9) + "RUNNING",
|
|
78
|
-
);
|
|
79
|
-
console.log(" " + "-".repeat(78));
|
|
80
|
-
|
|
81
|
-
for (const d of decisions) {
|
|
82
|
-
const task = BACKLOG.find((t) => t.id === d.taskId)!;
|
|
83
|
-
const flag = d.overCap ? " ⚠ OVER CAP" : "";
|
|
84
|
-
console.log(
|
|
85
|
-
" " +
|
|
86
|
-
pad(d.taskId, 7) +
|
|
87
|
-
pad(task.capability, 11) +
|
|
88
|
-
pad(task.difficulty, 9) +
|
|
89
|
-
pad(d.backend, 9) +
|
|
90
|
-
pad(d.model.name, 20) +
|
|
91
|
-
pad(d.tier, 6) +
|
|
92
|
-
pad(money(d.cost), 9) +
|
|
93
|
-
money(d.runningTotal) +
|
|
94
|
-
flag,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const total = decisions.at(-1)?.runningTotal ?? 0;
|
|
99
|
-
console.log(" " + "-".repeat(78));
|
|
100
|
-
console.log(` TOTAL ESTIMATED: ${money(total)}\n`);
|
|
101
|
-
|
|
102
|
-
console.log(" Decision trail for T-03 (the developer task):");
|
|
103
|
-
for (const r of decisions.find((d) => d.taskId === "T-03")!.reasons) {
|
|
104
|
-
console.log(" · " + r);
|
|
105
|
-
}
|
|
106
|
-
console.log("");
|
package/src/estimate.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Token estimation lives in CODE, not in the model.
|
|
2
|
-
// The Phase 2 live run proved models estimate their own token use terribly
|
|
3
|
-
// (a "high" single-file task used ~1.5k output, not the 12k a model guessed).
|
|
4
|
-
// So the PM only tags capability + difficulty; we map that to a token budget here.
|
|
5
|
-
//
|
|
6
|
-
// CALIBRATION (2026-07-15) — retuned against real getSessionStats from live runs:
|
|
7
|
-
// Phase 2 dev (code, single-file): output ~1.5k, input almost entirely cached
|
|
8
|
-
// (fresh 4 of ~3.3k tokens).
|
|
9
|
-
// Phase 4 mini: design/low, code/low ~ $0.03-0.04 each; test/trivial ~ $0.02.
|
|
10
|
-
// Takeaways baked in below:
|
|
11
|
-
// - a "task" is ONE atomic unit (a file / component / spec), not a whole feature,
|
|
12
|
-
// so outputs are small — and output dominates cost (priced ~5-6x input).
|
|
13
|
-
// - Pi caches context aggressively, so most input bills at the cheap cacheRead rate.
|
|
14
|
-
// These stay heuristic; wire measured actuals back in for self-calibration later.
|
|
15
|
-
|
|
16
|
-
import type { Capability, Difficulty, TokenEstimate } from "./types.js";
|
|
17
|
-
import { calibratedTokens, allSamples } from "./calibration.js";
|
|
18
|
-
|
|
19
|
-
type Bucket = { input: number; output: number };
|
|
20
|
-
|
|
21
|
-
const BUCKETS: Record<Capability, Record<Difficulty, Bucket>> = {
|
|
22
|
-
plan: {
|
|
23
|
-
trivial: { input: 2_000, output: 800 },
|
|
24
|
-
low: { input: 4_000, output: 1_500 },
|
|
25
|
-
medium: { input: 8_000, output: 3_000 },
|
|
26
|
-
high: { input: 12_000, output: 5_000 },
|
|
27
|
-
},
|
|
28
|
-
design: {
|
|
29
|
-
trivial: { input: 4_000, output: 2_000 },
|
|
30
|
-
low: { input: 6_000, output: 3_500 },
|
|
31
|
-
medium: { input: 10_000, output: 6_000 },
|
|
32
|
-
high: { input: 15_000, output: 9_000 },
|
|
33
|
-
},
|
|
34
|
-
code: {
|
|
35
|
-
trivial: { input: 6_000, output: 1_500 },
|
|
36
|
-
low: { input: 10_000, output: 3_000 },
|
|
37
|
-
medium: { input: 25_000, output: 7_000 },
|
|
38
|
-
high: { input: 60_000, output: 14_000 },
|
|
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
|
-
},
|
|
47
|
-
test: {
|
|
48
|
-
trivial: { input: 15_000, output: 1_200 },
|
|
49
|
-
low: { input: 25_000, output: 2_500 },
|
|
50
|
-
medium: { input: 50_000, output: 5_000 },
|
|
51
|
-
high: { input: 80_000, output: 9_000 },
|
|
52
|
-
},
|
|
53
|
-
ops: {
|
|
54
|
-
trivial: { input: 10_000, output: 2_000 },
|
|
55
|
-
low: { input: 20_000, output: 4_000 },
|
|
56
|
-
medium: { input: 40_000, output: 7_000 },
|
|
57
|
-
high: { input: 70_000, output: 12_000 },
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Pi auto-caches system prompt + context aggressively (live run: fresh input 4 tokens
|
|
62
|
-
// of ~3.3k). Real cached share is often 80-99%; 0.55 is a conservative planning value.
|
|
63
|
-
const DEFAULT_CACHED_INPUT_FRACTION = 0.55;
|
|
64
|
-
|
|
65
|
-
export function estimateTokens(capability: Capability, difficulty: Difficulty): TokenEstimate {
|
|
66
|
-
// Prefer the learned average once we have enough real samples; else the static bucket.
|
|
67
|
-
const learned = calibratedTokens(capability, difficulty);
|
|
68
|
-
if (learned) return learned;
|
|
69
|
-
const b = BUCKETS[capability][difficulty];
|
|
70
|
-
return { input: b.input, output: b.output, cachedInputFraction: DEFAULT_CACHED_INPUT_FRACTION };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The static baseline (pre-calibration) token budget for a bucket. */
|
|
74
|
-
export function baselineTokens(capability: Capability, difficulty: Difficulty): Bucket {
|
|
75
|
-
return BUCKETS[capability][difficulty];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface AccuracyRow {
|
|
79
|
-
capability: Capability;
|
|
80
|
-
difficulty: Difficulty;
|
|
81
|
-
baseOutput: number;
|
|
82
|
-
actualOutput: number;
|
|
83
|
-
baseInput: number;
|
|
84
|
-
actualInput: number;
|
|
85
|
-
n: number;
|
|
86
|
-
active: boolean; // true once calibration overrides the baseline (enough samples)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Baseline vs measured tokens for every bucket that has real samples. */
|
|
90
|
-
export function estimateAccuracy(): AccuracyRow[] {
|
|
91
|
-
const samples = allSamples();
|
|
92
|
-
const rows: AccuracyRow[] = [];
|
|
93
|
-
for (const cap of Object.keys(BUCKETS) as Capability[]) {
|
|
94
|
-
for (const diff of Object.keys(BUCKETS[cap]) as Difficulty[]) {
|
|
95
|
-
const s = samples[`${cap}/${diff}`];
|
|
96
|
-
if (!s) continue;
|
|
97
|
-
const b = BUCKETS[cap][diff];
|
|
98
|
-
rows.push({
|
|
99
|
-
capability: cap,
|
|
100
|
-
difficulty: diff,
|
|
101
|
-
baseOutput: b.output,
|
|
102
|
-
actualOutput: Math.round(s.output),
|
|
103
|
-
baseInput: b.input,
|
|
104
|
-
actualInput: Math.round(s.input),
|
|
105
|
-
n: s.n,
|
|
106
|
-
active: !!calibratedTokens(cap, diff),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return rows;
|
|
111
|
-
}
|
package/src/executor.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// Phase 2 — the Executor. Runs ONE task on a real Pi agent session with the model
|
|
2
|
-
// the router chose. Writes real files to a workspace dir, then reads back Pi's own
|
|
3
|
-
// token usage + cost so we can compare estimated vs actual.
|
|
4
|
-
//
|
|
5
|
-
// Constructing a session and resolving models is offline + free. Only session.prompt()
|
|
6
|
-
// hits the provider API and spends money — that path is guarded by the caller.
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
ModelRuntime,
|
|
10
|
-
createAgentSession,
|
|
11
|
-
type AgentSession,
|
|
12
|
-
type AgentSessionEvent,
|
|
13
|
-
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { readdirSync, statSync } from "node:fs";
|
|
15
|
-
import { join, relative } from "node:path";
|
|
16
|
-
import type { Provider, RouteDecision, Task } from "./types.js";
|
|
17
|
-
import { estimateCost } from "./cost.js";
|
|
18
|
-
import { getModel } from "./models.js";
|
|
19
|
-
import { addSessionCost } from "./session-cost.js";
|
|
20
|
-
|
|
21
|
-
/** Pi's own Model type, derived so we don't depend on a deep sub-path import. */
|
|
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
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Resolve a Projectinator (provider, modelId) to Pi's executable Model.
|
|
31
|
-
* Offline + free — reads Pi's built-in registry. Throws with a clear message
|
|
32
|
-
* if the id isn't one Pi knows (our ids are kept identical to Pi's on purpose). */
|
|
33
|
-
export function resolvePiModel(
|
|
34
|
-
runtime: ModelRuntime,
|
|
35
|
-
provider: Provider,
|
|
36
|
-
modelId: string,
|
|
37
|
-
): PiModel {
|
|
38
|
-
const m = runtime.getModel(provider, modelId);
|
|
39
|
-
if (!m) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Pi has no model "${provider}/${modelId}". ` +
|
|
42
|
-
`Check src/models.ts uses Pi's exact built-in id.`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return m;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ExecuteOptions {
|
|
49
|
-
/** Directory the agent builds into (its cwd). */
|
|
50
|
-
workspace: string;
|
|
51
|
-
/** Extended-thinking level. Pi clamps to model capability. */
|
|
52
|
-
thinkingLevel?: "off" | "low" | "medium" | "high";
|
|
53
|
-
/** Optional progress hook — receives raw Pi session events. */
|
|
54
|
-
onEvent?: (event: AgentSessionEvent) => void;
|
|
55
|
-
/** Tools the agent may use. Default: the coding set. */
|
|
56
|
-
tools?: string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface ExecuteResult {
|
|
60
|
-
taskId: string;
|
|
61
|
-
provider: Provider;
|
|
62
|
-
modelId: string;
|
|
63
|
-
/** Files present in the workspace after the run (repo-relative). */
|
|
64
|
-
files: string[];
|
|
65
|
-
/** Pi's own measured usage for the run. */
|
|
66
|
-
actual: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
|
|
67
|
-
/** Pi's own computed dollar cost. */
|
|
68
|
-
actualCost: number;
|
|
69
|
-
/** What we predicted before running, for calibration. */
|
|
70
|
-
estCost: number;
|
|
71
|
-
/** actualCost - estCost. Positive = we under-estimated. */
|
|
72
|
-
costDelta: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Build the Developer role's instruction for a single task. */
|
|
76
|
-
export function buildDeveloperPrompt(task: Task): string {
|
|
77
|
-
return [
|
|
78
|
-
`You are the DEVELOPER on an autonomous build team. Complete exactly this task and nothing more.`,
|
|
79
|
-
``,
|
|
80
|
-
`Task ${task.id}: ${task.title}`,
|
|
81
|
-
task.story ? `Story: ${task.story}` : ``,
|
|
82
|
-
``,
|
|
83
|
-
`Rules:`,
|
|
84
|
-
`- Write real files into the current working directory using your file tools.`,
|
|
85
|
-
`- Keep it minimal and correct; no placeholders, no TODOs.`,
|
|
86
|
-
`- Do not explain at length. Build, then stop.`,
|
|
87
|
-
]
|
|
88
|
-
.filter(Boolean)
|
|
89
|
-
.join("\n");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Execute one task on a live Pi session. THIS SPENDS MONEY (session.prompt).
|
|
94
|
-
* Caller is responsible for gating on a live flag + present API key.
|
|
95
|
-
*/
|
|
96
|
-
export async function executeTask(
|
|
97
|
-
task: Task,
|
|
98
|
-
decision: RouteDecision,
|
|
99
|
-
opts: ExecuteOptions,
|
|
100
|
-
): Promise<ExecuteResult> {
|
|
101
|
-
const runtime = await piRuntime();
|
|
102
|
-
const model = resolvePiModel(runtime, decision.provider, decision.model.id);
|
|
103
|
-
|
|
104
|
-
const { session } = await createAgentSession({
|
|
105
|
-
model,
|
|
106
|
-
cwd: opts.workspace,
|
|
107
|
-
modelRuntime: runtime,
|
|
108
|
-
thinkingLevel: opts.thinkingLevel ?? "medium",
|
|
109
|
-
tools: opts.tools ?? ["read", "write", "edit", "bash", "ls", "grep", "find"],
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
let unsubscribe: (() => void) | undefined;
|
|
113
|
-
if (opts.onEvent) unsubscribe = session.subscribe(opts.onEvent);
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
await session.prompt(buildDeveloperPrompt(task));
|
|
117
|
-
const stats = session.getSessionStats();
|
|
118
|
-
addSessionCost(stats.cost);
|
|
119
|
-
|
|
120
|
-
const estCost = estimateCost(task.estTokens, getModel(decision.model.id));
|
|
121
|
-
const actualCost = round2(stats.cost);
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
taskId: task.id,
|
|
125
|
-
provider: decision.provider,
|
|
126
|
-
modelId: decision.model.id,
|
|
127
|
-
files: listFiles(opts.workspace),
|
|
128
|
-
actual: {
|
|
129
|
-
input: stats.tokens.input,
|
|
130
|
-
output: stats.tokens.output,
|
|
131
|
-
cacheRead: stats.tokens.cacheRead,
|
|
132
|
-
cacheWrite: stats.tokens.cacheWrite,
|
|
133
|
-
total: stats.tokens.total,
|
|
134
|
-
},
|
|
135
|
-
actualCost,
|
|
136
|
-
estCost,
|
|
137
|
-
costDelta: round2(actualCost - estCost),
|
|
138
|
-
};
|
|
139
|
-
} finally {
|
|
140
|
-
unsubscribe?.();
|
|
141
|
-
session.dispose();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** List files in a workspace, skipping Pi/session/VCS noise. */
|
|
146
|
-
function listFiles(dir: string): string[] {
|
|
147
|
-
const out: string[] = [];
|
|
148
|
-
const skip = new Set([".pi", ".git", "node_modules"]);
|
|
149
|
-
const walk = (d: string) => {
|
|
150
|
-
let entries: string[];
|
|
151
|
-
try {
|
|
152
|
-
entries = readdirSync(d);
|
|
153
|
-
} catch {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
for (const name of entries) {
|
|
157
|
-
if (skip.has(name)) continue;
|
|
158
|
-
const full = join(d, name);
|
|
159
|
-
if (statSync(full).isDirectory()) walk(full);
|
|
160
|
-
else out.push(relative(dir, full));
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
walk(dir);
|
|
164
|
-
return out.sort();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function round2(n: number): number {
|
|
168
|
-
return Math.round(n * 100) / 100;
|
|
169
|
-
}
|
package/src/git.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// Git-per-build: init a repo in the build workspace and commit after each task,
|
|
2
|
-
// so every build has history + diffs (and a foundation for undo). Best-effort —
|
|
3
|
-
// if git isn't available or a command fails, builds carry on uninterrupted.
|
|
4
|
-
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
interface GitOut { ok: boolean; out: string; }
|
|
10
|
-
|
|
11
|
-
function git(dir: string, args: string[]): GitOut {
|
|
12
|
-
try {
|
|
13
|
-
const r = spawnSync("git", args, { cwd: dir, encoding: "utf8" });
|
|
14
|
-
return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
15
|
-
} catch {
|
|
16
|
-
return { ok: false, out: "" };
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isRepo(dir: string): boolean {
|
|
21
|
-
return existsSync(join(dir, ".git"));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** git init + a local identity + an initial commit. Idempotent. Returns success. */
|
|
25
|
-
export function initRepo(dir: string): boolean {
|
|
26
|
-
if (isRepo(dir)) return true;
|
|
27
|
-
const gi = join(dir, ".gitignore");
|
|
28
|
-
if (!existsSync(gi)) writeFileSync(gi, ".deploy/\nbuild-state.json\nnode_modules/\n");
|
|
29
|
-
if (!git(dir, ["init"]).ok) return false;
|
|
30
|
-
// Local identity so commits work even when the user has no global git config.
|
|
31
|
-
git(dir, ["config", "user.email", "bot@projectinator.local"]);
|
|
32
|
-
git(dir, ["config", "user.name", "Projectinator"]);
|
|
33
|
-
git(dir, ["add", "-A"]);
|
|
34
|
-
git(dir, ["commit", "-m", "chore: initial workspace", "--allow-empty"]);
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Commit whatever a task produced. Returns the short hash, or null on failure. */
|
|
39
|
-
export function commitTask(dir: string, taskId: string, title: string): string | null {
|
|
40
|
-
if (!isRepo(dir)) return null;
|
|
41
|
-
git(dir, ["add", "-A"]);
|
|
42
|
-
const msg = `${taskId}: ${title}`.replace(/\s+/g, " ").slice(0, 72);
|
|
43
|
-
const c = git(dir, ["commit", "-m", msg, "--allow-empty"]);
|
|
44
|
-
if (!c.ok) return null;
|
|
45
|
-
const h = git(dir, ["rev-parse", "--short", "HEAD"]);
|
|
46
|
-
return h.ok ? h.out : null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Hard-reset one commit back (undo the last task's file changes). Returns the
|
|
50
|
-
* reverted task id parsed from the commit message. Refuses to undo past the
|
|
51
|
-
* initial commit. */
|
|
52
|
-
export function undoLastCommit(dir: string): { ok: boolean; taskId?: string } {
|
|
53
|
-
if (!isRepo(dir)) return { ok: false };
|
|
54
|
-
const commits = history(dir);
|
|
55
|
-
if (commits.length <= 1) return { ok: false }; // nothing but the initial commit
|
|
56
|
-
const taskId = (commits[0]?.msg.split(":")[0] ?? "").trim() || undefined;
|
|
57
|
-
const r = git(dir, ["reset", "--hard", "HEAD~1"]);
|
|
58
|
-
return { ok: r.ok, taskId };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface Commit { hash: string; msg: string; }
|
|
62
|
-
|
|
63
|
-
/** Commit log, newest first. */
|
|
64
|
-
export function history(dir: string): Commit[] {
|
|
65
|
-
if (!isRepo(dir)) return [];
|
|
66
|
-
const r = git(dir, ["log", "--oneline", "--no-decorate", "--no-color"]);
|
|
67
|
-
if (!r.ok || !r.out) return [];
|
|
68
|
-
return r.out.split("\n").filter(Boolean).map((line) => {
|
|
69
|
-
const i = line.indexOf(" ");
|
|
70
|
-
return i < 0 ? { hash: line, msg: "" } : { hash: line.slice(0, i), msg: line.slice(i + 1) };
|
|
71
|
-
});
|
|
72
|
-
}
|
package/src/intake.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// Intake — the PM's clarifying-questions step. Before planning a vague request,
|
|
2
|
-
// the PM asks 2-4 short questions (each with suggested options) to pin down what
|
|
3
|
-
// to build. A clear request returns no questions and skips straight to planning.
|
|
4
|
-
//
|
|
5
|
-
// Forced structured output, same discipline as pm.ts: one tool, permissive
|
|
6
|
-
// schema, coerced/validated in code.
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
createAgentSession,
|
|
10
|
-
defineTool,
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { Type, type Static } from "typebox";
|
|
13
|
-
import type { Backend, Provider } from "./types.js";
|
|
14
|
-
import { findEntry } from "./registry.js";
|
|
15
|
-
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
16
|
-
import { addSessionCost } from "./session-cost.js";
|
|
17
|
-
|
|
18
|
-
const IntakeSchema = Type.Object(
|
|
19
|
-
{
|
|
20
|
-
needsClarification: Type.Boolean({ description: "true only if the request is too vague to plan well" }),
|
|
21
|
-
questions: Type.Array(
|
|
22
|
-
Type.Object(
|
|
23
|
-
{
|
|
24
|
-
question: Type.String({ description: "one short clarifying question" }),
|
|
25
|
-
options: Type.Array(Type.String(), { description: "2-4 concrete pickable answers; may be empty for free text" }),
|
|
26
|
-
multi: Type.Boolean({ description: "true if several options can apply at once" }),
|
|
27
|
-
},
|
|
28
|
-
{ additionalProperties: true },
|
|
29
|
-
),
|
|
30
|
-
{ description: "empty when needsClarification is false; at most 4" },
|
|
31
|
-
),
|
|
32
|
-
},
|
|
33
|
-
{ additionalProperties: true },
|
|
34
|
-
);
|
|
35
|
-
type IntakeRaw = Static<typeof IntakeSchema>;
|
|
36
|
-
|
|
37
|
-
export interface IntakeQuestion {
|
|
38
|
-
question: string;
|
|
39
|
-
options: string[];
|
|
40
|
-
multi: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function buildIntakeTool() {
|
|
44
|
-
let captured: IntakeRaw | undefined;
|
|
45
|
-
const tool = defineTool({
|
|
46
|
-
name: "submit_intake",
|
|
47
|
-
label: "Submit Intake",
|
|
48
|
-
description: "Submit whether clarification is needed and, if so, the clarifying questions. Call exactly once.",
|
|
49
|
-
parameters: IntakeSchema,
|
|
50
|
-
execute: async (_id, params) => {
|
|
51
|
-
captured = params as IntakeRaw;
|
|
52
|
-
return { content: [{ type: "text", text: `Intake: ${params.needsClarification ? `${params.questions.length} questions` : "clear"}.` }], details: {} };
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
return { tool, get: () => captured };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const SYSTEM = [
|
|
59
|
-
"You are the PROJECT MANAGER doing intake for a build request.",
|
|
60
|
-
"If the request already has enough detail to plan and build — its purpose and the",
|
|
61
|
-
"must-have features are clear or reasonably inferable — set needsClarification=false",
|
|
62
|
-
"and questions=[].",
|
|
63
|
-
"If it is vague (a bare template like 'landing page', a one-liner missing the",
|
|
64
|
-
"essentials), set needsClarification=true and produce 2-4 SHORT questions that most",
|
|
65
|
-
"reduce ambiguity: what the product/business actually is, a name, must-have",
|
|
66
|
-
"sections/features, and style. For each question give 2-4 concrete pickable options;",
|
|
67
|
-
"set multi=true when several can apply (e.g. which sections). Never more than 4",
|
|
68
|
-
"questions. Do not ask what you can reasonably assume. Call submit_intake exactly once.",
|
|
69
|
-
].join("\n");
|
|
70
|
-
|
|
71
|
-
export interface AssessOptions {
|
|
72
|
-
backend: Backend;
|
|
73
|
-
modelOverride?: { provider: Provider; model: string };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Ask the PM whether the request needs clarification; returns up to 4 questions
|
|
77
|
-
* (empty = clear enough to plan directly). Never throws — returns [] on trouble. */
|
|
78
|
-
export async function assessIntake(idea: string, opts: AssessOptions): Promise<IntakeQuestion[]> {
|
|
79
|
-
const runtime = await piRuntime();
|
|
80
|
-
const { entry } = findEntry("plan", "mid");
|
|
81
|
-
const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const model = resolvePiModel(runtime, pick.provider, pick.model);
|
|
85
|
-
const { tool, get } = buildIntakeTool();
|
|
86
|
-
const { session } = await createAgentSession({
|
|
87
|
-
model,
|
|
88
|
-
modelRuntime: runtime,
|
|
89
|
-
thinkingLevel: "low",
|
|
90
|
-
noTools: "all",
|
|
91
|
-
customTools: [tool],
|
|
92
|
-
tools: ["submit_intake"],
|
|
93
|
-
});
|
|
94
|
-
try {
|
|
95
|
-
await session.prompt(`${SYSTEM}\n\n--- REQUEST ---\n${idea}`);
|
|
96
|
-
let raw = get();
|
|
97
|
-
if (!raw) {
|
|
98
|
-
await session.prompt("Call submit_intake now.");
|
|
99
|
-
raw = get();
|
|
100
|
-
}
|
|
101
|
-
addSessionCost(session.getSessionStats().cost);
|
|
102
|
-
if (!raw || !raw.needsClarification) return [];
|
|
103
|
-
return (raw.questions ?? [])
|
|
104
|
-
.slice(0, 4)
|
|
105
|
-
.map((q) => ({
|
|
106
|
-
question: String(q.question ?? "").trim(),
|
|
107
|
-
options: (q.options ?? []).map((o) => String(o).trim()).filter(Boolean).slice(0, 6),
|
|
108
|
-
multi: !!q.multi,
|
|
109
|
-
}))
|
|
110
|
-
.filter((q) => q.question);
|
|
111
|
-
} finally {
|
|
112
|
-
session.dispose();
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
return []; // never block a build on intake — fall through to planning
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Fold the collected answers into the brief the PM will plan from. */
|
|
120
|
-
export function enrichBrief(idea: string, answers: { question: string; answer: string }[]): string {
|
|
121
|
-
const kept = answers.filter((a) => a.answer.trim());
|
|
122
|
-
if (!kept.length) return idea;
|
|
123
|
-
const lines = kept.map((a) => `- ${a.question} ${a.answer.trim()}`);
|
|
124
|
-
return `${idea}\n\nClarifications from the requester:\n${lines.join("\n")}`;
|
|
125
|
-
}
|