projectinator 0.1.5 → 0.2.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 +17 -10
- package/package.json +1 -1
- package/src/build-state.ts +3 -2
- package/src/cost.ts +4 -6
- package/src/estimate.ts +7 -0
- package/src/orchestrator.ts +71 -32
- package/src/pm.ts +10 -6
- package/src/preview.ts +15 -1
- package/src/registry.ts +12 -0
- package/src/retro.ts +2 -2
- package/src/roles.ts +77 -29
- package/src/router.ts +2 -5
- package/src/run-build.ts +20 -7
- 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/types.ts +34 -1
package/src/tui/Kanban.tsx
CHANGED
|
@@ -16,9 +16,11 @@ export interface BoardTask {
|
|
|
16
16
|
dependsOn?: string[];
|
|
17
17
|
status: "pending" | "running" | "done" | "failed" | "skipped";
|
|
18
18
|
cost?: number;
|
|
19
|
-
verdict?: "PASS" | "FAIL";
|
|
19
|
+
verdict?: "PASS" | "PASS*" | "FAIL";
|
|
20
20
|
/** The teammate (model) working this task, when known. */
|
|
21
21
|
assignee?: string;
|
|
22
|
+
/** Human annotation from the board editor. */
|
|
23
|
+
notes?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
type Col = "backlog" | "notStarted" | "inProgress" | "done";
|
|
@@ -62,11 +64,12 @@ function Card({ t }: { t: BoardTask }): React.ReactElement {
|
|
|
62
64
|
<Text>{ROLE_META[t.capability].emoji} </Text>
|
|
63
65
|
<Text color={C.dim}>{t.id} </Text>
|
|
64
66
|
<Text color={C.accent}>{t.capability}</Text>
|
|
65
|
-
{t.verdict ? <Text> <Badge color={t.verdict === "PASS" ? "
|
|
67
|
+
{t.verdict ? <Text> <Badge color={t.verdict === "FAIL" ? "red" : t.verdict === "PASS*" ? "yellow" : "green"}>{t.verdict}</Badge></Text> : null}
|
|
66
68
|
{t.cost ? <Text color={C.dim}> ${t.cost.toFixed(2)}</Text> : null}
|
|
67
69
|
</Box>
|
|
68
70
|
<Text color={failed ? C.bad : C.text} wrap="truncate-end">{t.title}</Text>
|
|
69
71
|
{t.assignee ? <Text color={C.dim}>{t.assignee}</Text> : null}
|
|
72
|
+
{t.notes ? <Text color={C.dim} wrap="truncate-end">✎ {t.notes}</Text> : null}
|
|
70
73
|
</Box>
|
|
71
74
|
);
|
|
72
75
|
}
|
package/src/tui/Settings.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import { WebAccounts } from "./WebAccounts.js";
|
|
|
10
10
|
import { connectedProviders } from "../web/session.js";
|
|
11
11
|
import { estimateAccuracy } from "../estimate.js";
|
|
12
12
|
import { availableProviders, effectiveRoster, allModels, setRoleModel, PROVIDER_LABEL } from "./engine.js";
|
|
13
|
-
import { setKey, getPrefs, setPrefs, loadConfig, setPreferredProvider, getDefaultMode, setDefaultMode, getNotify, setNotify, getPreferredStack, setPreferredStack, ENV_VAR } from "./config.js";
|
|
13
|
+
import { setKey, getPrefs, setPrefs, loadConfig, setPreferredProvider, getDefaultMode, setDefaultMode, getNotify, setNotify, getPreferredStack, setPreferredStack, ENV_VAR, type Prefs } from "./config.js";
|
|
14
14
|
import { validateKey } from "./validate.js";
|
|
15
15
|
import { openRouterModels, refreshOpenRouterModels } from "../openrouter.js";
|
|
16
16
|
|
|
@@ -428,25 +428,35 @@ function PrefsEditor({
|
|
|
428
428
|
onDone,
|
|
429
429
|
onCancel,
|
|
430
430
|
}: {
|
|
431
|
-
initial:
|
|
432
|
-
onDone: (p:
|
|
431
|
+
initial: Prefs;
|
|
432
|
+
onDone: (p: Prefs) => void;
|
|
433
433
|
onCancel: () => void;
|
|
434
434
|
}): React.ReactElement {
|
|
435
435
|
const [cap, setCap] = useState(String(initial.budgetCapUSD));
|
|
436
436
|
const [conc, setConc] = useState(String(initial.concurrency));
|
|
437
437
|
const [pct, setPct] = useState(String(initial.budgetAlertPct));
|
|
438
|
-
const [
|
|
438
|
+
const [tmo, setTmo] = useState(String(initial.taskTimeoutMin));
|
|
439
|
+
const [tcap, setTcap] = useState(String(initial.taskCostCapUSD));
|
|
440
|
+
const [field, setField] = useState<"cap" | "conc" | "pct" | "tmo" | "tcap">("cap");
|
|
439
441
|
|
|
440
442
|
const commit = () => {
|
|
441
443
|
const b = Math.max(1, parseFloat(cap) || initial.budgetCapUSD);
|
|
442
444
|
const c = Math.max(1, Math.floor(parseFloat(conc) || initial.concurrency));
|
|
443
445
|
const p = Math.min(99, Math.max(1, Math.round(parseFloat(pct) || initial.budgetAlertPct)));
|
|
444
|
-
|
|
446
|
+
// "0" is a valid value here (unlimited), so only a non-number falls back.
|
|
447
|
+
const parse0 = (s: string, fallback: number) => { const n = parseFloat(s); return Number.isFinite(n) ? Math.max(0, n) : fallback; };
|
|
448
|
+
onDone({
|
|
449
|
+
budgetCapUSD: b,
|
|
450
|
+
concurrency: c,
|
|
451
|
+
budgetAlertPct: p,
|
|
452
|
+
taskTimeoutMin: parse0(tmo, initial.taskTimeoutMin),
|
|
453
|
+
taskCostCapUSD: parse0(tcap, initial.taskCostCapUSD),
|
|
454
|
+
});
|
|
445
455
|
};
|
|
446
456
|
|
|
447
457
|
return (
|
|
448
458
|
<Box flexDirection="column">
|
|
449
|
-
<Panel title="Budget, speed &
|
|
459
|
+
<Panel title="Budget, speed, alerts & task limits">
|
|
450
460
|
<Box flexDirection="column">
|
|
451
461
|
<Box>
|
|
452
462
|
<Box width={22}><Text color={field === "cap" ? C.accent : C.text}>Budget cap (USD)</Text></Box>
|
|
@@ -471,14 +481,30 @@ function PrefsEditor({
|
|
|
471
481
|
<Box>
|
|
472
482
|
<Box width={22}><Text color={field === "pct" ? C.accent : C.text}>Alert at % of cap</Text></Box>
|
|
473
483
|
{field === "pct" ? (
|
|
474
|
-
<TextInput value={pct} onChange={setPct} onSubmit={
|
|
484
|
+
<TextInput value={pct} onChange={setPct} onSubmit={() => setField("tmo")} />
|
|
475
485
|
) : (
|
|
476
486
|
<Text>{pct}%</Text>
|
|
477
487
|
)}
|
|
478
488
|
</Box>
|
|
489
|
+
<Box>
|
|
490
|
+
<Box width={22}><Text color={field === "tmo" ? C.accent : C.text}>Task timeout (min)</Text></Box>
|
|
491
|
+
{field === "tmo" ? (
|
|
492
|
+
<TextInput value={tmo} onChange={setTmo} onSubmit={() => setField("tcap")} />
|
|
493
|
+
) : (
|
|
494
|
+
<Text>{tmo === "0" ? "unlimited" : tmo}</Text>
|
|
495
|
+
)}
|
|
496
|
+
</Box>
|
|
497
|
+
<Box>
|
|
498
|
+
<Box width={22}><Text color={field === "tcap" ? C.accent : C.text}>Task cost cap (USD)</Text></Box>
|
|
499
|
+
{field === "tcap" ? (
|
|
500
|
+
<TextInput value={tcap} onChange={setTcap} onSubmit={commit} />
|
|
501
|
+
) : (
|
|
502
|
+
<Text>{tcap === "0" ? "unlimited" : tcap}</Text>
|
|
503
|
+
)}
|
|
504
|
+
</Box>
|
|
479
505
|
</Box>
|
|
480
506
|
<Box flexDirection="column" marginTop={1}>
|
|
481
|
-
<Text color={C.textSubtle}>Enter moves to the next field, then saves.</Text>
|
|
507
|
+
<Text color={C.textSubtle}>Enter moves to the next field, then saves. A task over its limit is aborted and the build halts (resumable); 0 = unlimited.</Text>
|
|
482
508
|
<KeyHint hints={[{ keys: "Enter", label: "next / save" }, { keys: "Ctrl+C", label: "cancel" }]} />
|
|
483
509
|
</Box>
|
|
484
510
|
</Panel>
|
package/src/tui/components.tsx
CHANGED
|
@@ -261,11 +261,12 @@ export interface TaskView {
|
|
|
261
261
|
title: string;
|
|
262
262
|
cost?: number;
|
|
263
263
|
model?: string;
|
|
264
|
-
|
|
264
|
+
/** "PASS*" = passed, but the tester never ran the app (no Chromium). */
|
|
265
|
+
verdict?: "PASS" | "PASS*" | "FAIL";
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
const CAP_LABEL: Record<Capability, string> = {
|
|
268
|
-
plan: "plan", design: "design", code: "code", test: "test", ops: "ops",
|
|
269
|
+
plan: "plan", design: "design", code: "code", review: "review", test: "test", ops: "ops",
|
|
269
270
|
};
|
|
270
271
|
|
|
271
272
|
/** The AI team: each capability is a role with a friendly name + icon. */
|
|
@@ -273,6 +274,7 @@ export const ROLE_META: Record<Capability, { emoji: string; label: string }> = {
|
|
|
273
274
|
plan: { emoji: "🧭", label: "Project manager" },
|
|
274
275
|
design: { emoji: "🎨", label: "Designer" },
|
|
275
276
|
code: { emoji: "🧠", label: "Developer" },
|
|
277
|
+
review: { emoji: "🔍", label: "Reviewer" },
|
|
276
278
|
test: { emoji: "🔎", label: "Tester" },
|
|
277
279
|
ops: { emoji: "🚀", label: "Runner" },
|
|
278
280
|
};
|
|
@@ -311,7 +313,7 @@ export function TaskRow({ t }: { t: TaskView }): React.ReactElement {
|
|
|
311
313
|
</Box>
|
|
312
314
|
<Box width={10} justifyContent="flex-end">
|
|
313
315
|
{t.verdict ? (
|
|
314
|
-
<Text color={t.verdict === "PASS" ? C.
|
|
316
|
+
<Text color={t.verdict === "FAIL" ? C.bad : t.verdict === "PASS*" ? C.warn : C.good}>{t.verdict}</Text>
|
|
315
317
|
) : t.cost !== undefined ? (
|
|
316
318
|
<Text color={C.dim}>${t.cost.toFixed(2)}</Text>
|
|
317
319
|
) : (
|
package/src/tui/config.ts
CHANGED
|
@@ -15,6 +15,9 @@ export interface AppConfig {
|
|
|
15
15
|
concurrency?: number;
|
|
16
16
|
/** Warn (not halt) once spend crosses this % of the cap. Default 80. */
|
|
17
17
|
budgetAlertPct?: number;
|
|
18
|
+
/** Per-task limits; 0 = unlimited. Defaults: 10 min, $3. */
|
|
19
|
+
taskTimeoutMin?: number;
|
|
20
|
+
taskCostCapUSD?: number;
|
|
18
21
|
/** If set, always route to this provider (when it has a key), ignoring the others. */
|
|
19
22
|
preferredProvider?: Provider;
|
|
20
23
|
/** Default workflow for new builds: auto-run, or require PM approval before building. */
|
|
@@ -117,19 +120,31 @@ export function setKey(provider: Provider, key: string): void {
|
|
|
117
120
|
process.env[ENV_VAR[provider]] = key;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
export
|
|
123
|
+
export interface Prefs {
|
|
124
|
+
budgetCapUSD: number;
|
|
125
|
+
concurrency: number;
|
|
126
|
+
budgetAlertPct: number;
|
|
127
|
+
taskTimeoutMin: number;
|
|
128
|
+
taskCostCapUSD: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getPrefs(): Prefs {
|
|
121
132
|
const cfg = loadConfig();
|
|
122
133
|
return {
|
|
123
134
|
budgetCapUSD: cfg.budgetCapUSD ?? 25,
|
|
124
135
|
concurrency: cfg.concurrency ?? 3,
|
|
125
136
|
budgetAlertPct: cfg.budgetAlertPct ?? 80,
|
|
137
|
+
taskTimeoutMin: cfg.taskTimeoutMin ?? 10,
|
|
138
|
+
taskCostCapUSD: cfg.taskCostCapUSD ?? 3,
|
|
126
139
|
};
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
export function setPrefs(prefs:
|
|
142
|
+
export function setPrefs(prefs: Partial<Prefs>): void {
|
|
130
143
|
const cfg = loadConfig();
|
|
131
144
|
if (prefs.budgetCapUSD !== undefined) cfg.budgetCapUSD = prefs.budgetCapUSD;
|
|
132
145
|
if (prefs.concurrency !== undefined) cfg.concurrency = prefs.concurrency;
|
|
133
146
|
if (prefs.budgetAlertPct !== undefined) cfg.budgetAlertPct = prefs.budgetAlertPct;
|
|
147
|
+
if (prefs.taskTimeoutMin !== undefined) cfg.taskTimeoutMin = prefs.taskTimeoutMin;
|
|
148
|
+
if (prefs.taskCostCapUSD !== undefined) cfg.taskCostCapUSD = prefs.taskCostCapUSD;
|
|
134
149
|
saveConfig(cfg);
|
|
135
150
|
}
|
package/src/tui/engine.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
|
-
import type { Capability, Provider, RegistryEntry, Task, TaskOutcome, Tier } from "../types.js";
|
|
9
|
+
import type { Capability, Provider, RegistryEntry, Task, TaskLimits, TaskOutcome, Tier } from "../types.js";
|
|
10
10
|
import { DEFAULT_POLICY, route } from "../router.js";
|
|
11
11
|
import { REGISTRY } from "../registry.js";
|
|
12
12
|
import { MODELS } from "../models.js";
|
|
@@ -21,7 +21,7 @@ import { narrateRetro } from "../narrate.js";
|
|
|
21
21
|
import { decomposeIdea } from "../pm.js";
|
|
22
22
|
import { assessIntake, type IntakeQuestion } from "../intake.js";
|
|
23
23
|
import { councilEpics, type Epic, type CouncilResult } from "../council.js";
|
|
24
|
-
import { newBuildState, loadState, saveState, type BuildState } from "../build-state.js";
|
|
24
|
+
import { newBuildState, loadState, saveState, completedIds, type BuildState } from "../build-state.js";
|
|
25
25
|
|
|
26
26
|
const PROVIDER_KEYS: Record<Provider, string[]> = {
|
|
27
27
|
anthropic: ["ANTHROPIC_API_KEY"],
|
|
@@ -81,6 +81,7 @@ export const ROLE_TIERS: { capability: Capability; tier: Tier; label: string }[]
|
|
|
81
81
|
{ capability: "plan", tier: "mid", label: "Project manager" },
|
|
82
82
|
{ capability: "design", tier: "high", label: "Designer" },
|
|
83
83
|
{ capability: "code", tier: "high", label: "Developer" },
|
|
84
|
+
{ capability: "review", tier: "fast", label: "Reviewer" },
|
|
84
85
|
{ capability: "test", tier: "fast", label: "Tester" },
|
|
85
86
|
{ capability: "ops", tier: "high", label: "Runner / ops" },
|
|
86
87
|
];
|
|
@@ -332,7 +333,7 @@ export function saveProjectTasks(dir: string, tasks: Task[]): void {
|
|
|
332
333
|
export function exportProject(dir: string): { md: string; csv: string } {
|
|
333
334
|
const state = loadState(join(dir, "build-state.json"));
|
|
334
335
|
if (!state) throw new Error("No project data to export.");
|
|
335
|
-
const done =
|
|
336
|
+
const done = completedIds(state);
|
|
336
337
|
const cost = new Map<string, number>();
|
|
337
338
|
for (const o of state.outcomes) cost.set(o.taskId, (cost.get(o.taskId) ?? 0) + o.cost);
|
|
338
339
|
|
|
@@ -358,13 +359,14 @@ export function exportProject(dir: string): { md: string; csv: string } {
|
|
|
358
359
|
const c = cost.has(t.id) ? ` — $${cost.get(t.id)!.toFixed(2)}` : "";
|
|
359
360
|
const dep = (t.dependsOn ?? []).length ? ` _(after ${(t.dependsOn ?? []).join(", ")})_` : "";
|
|
360
361
|
md.push(`- [${mark}] \`${t.id}\` **${t.capability}/${t.difficulty}** — ${t.title}${c}${dep}`);
|
|
362
|
+
if (t.notes) md.push(` - ✎ ${t.notes}`);
|
|
361
363
|
}
|
|
362
364
|
md.push("");
|
|
363
365
|
}
|
|
364
366
|
|
|
365
367
|
const esc = (s: string) => `"${String(s).replace(/"/g, '""')}"`;
|
|
366
368
|
const csv = [
|
|
367
|
-
"id,epic,capability,difficulty,status,cost,dependsOn,title",
|
|
369
|
+
"id,epic,capability,difficulty,status,cost,dependsOn,title,notes",
|
|
368
370
|
...state.tasks.map((t) =>
|
|
369
371
|
[
|
|
370
372
|
t.id,
|
|
@@ -375,6 +377,7 @@ export function exportProject(dir: string): { md: string; csv: string } {
|
|
|
375
377
|
(cost.get(t.id) ?? 0).toFixed(2),
|
|
376
378
|
esc((t.dependsOn ?? []).join(" ")),
|
|
377
379
|
esc(t.title),
|
|
380
|
+
esc(t.notes ?? ""),
|
|
378
381
|
].join(","),
|
|
379
382
|
),
|
|
380
383
|
].join("\n");
|
|
@@ -390,7 +393,7 @@ export function exportProject(dir: string): { md: string; csv: string } {
|
|
|
390
393
|
function projectRows(dir: string) {
|
|
391
394
|
const state = loadState(join(dir, "build-state.json"));
|
|
392
395
|
if (!state) throw new Error("No project data to export.");
|
|
393
|
-
const done =
|
|
396
|
+
const done = completedIds(state);
|
|
394
397
|
const cost = new Map<string, number>();
|
|
395
398
|
for (const o of state.outcomes) cost.set(o.taskId, (cost.get(o.taskId) ?? 0) + o.cost);
|
|
396
399
|
return { state, done, cost };
|
|
@@ -623,7 +626,7 @@ export async function breakdownEpic(
|
|
|
623
626
|
|
|
624
627
|
export interface RunHandle {
|
|
625
628
|
workspace: string;
|
|
626
|
-
promise: Promise<{ totalCost: number; halted: boolean; files: string[] }>;
|
|
629
|
+
promise: Promise<{ totalCost: number; halted: boolean; haltReason?: string; files: string[] }>;
|
|
627
630
|
}
|
|
628
631
|
|
|
629
632
|
/** Run a planned build, streaming orchestrator events to the UI. Spends money.
|
|
@@ -635,6 +638,7 @@ export function startBuild(
|
|
|
635
638
|
opts: {
|
|
636
639
|
concurrency: number;
|
|
637
640
|
budgetCapUSD: number;
|
|
641
|
+
taskLimits: TaskLimits;
|
|
638
642
|
onEvent: (e: OrchestratorEvent) => void;
|
|
639
643
|
workspace?: string;
|
|
640
644
|
seedOutcomes?: TaskOutcome[];
|
|
@@ -658,7 +662,7 @@ export function startBuild(
|
|
|
658
662
|
state.budgetCapUSD = opts.budgetCapUSD; // remember this project's cap
|
|
659
663
|
|
|
660
664
|
const executor = makePiExecutor({ workspace, backend: "api" });
|
|
661
|
-
const policy = { ...DEFAULT_POLICY, backendMode: "api" as const, budgetCapUSD: opts.budgetCapUSD };
|
|
665
|
+
const policy = { ...DEFAULT_POLICY, backendMode: "api" as const, budgetCapUSD: opts.budgetCapUSD, taskLimits: opts.taskLimits };
|
|
662
666
|
|
|
663
667
|
// Version the workspace: init a repo, then commit after each finished task.
|
|
664
668
|
initRepo(workspace);
|
|
@@ -691,6 +695,7 @@ export function startBuild(
|
|
|
691
695
|
return {
|
|
692
696
|
totalCost: result.totalCost,
|
|
693
697
|
halted: result.halted,
|
|
698
|
+
haltReason: result.haltReason,
|
|
694
699
|
files: (result.outcomes.at(-1)?.files ?? []).filter((f) => f !== "build-state.json"),
|
|
695
700
|
};
|
|
696
701
|
});
|
package/src/tui/panels.tsx
CHANGED
|
@@ -76,7 +76,7 @@ export function ListView({ tasks }: { tasks: BoardTask[] }): React.ReactElement
|
|
|
76
76
|
<Box width={3}><Text>{ROLE_META[t.capability].emoji}</Text></Box>
|
|
77
77
|
<Box flexGrow={1}><Text color={C.text} wrap="truncate-end">{t.title}</Text></Box>
|
|
78
78
|
<Box width={10} justifyContent="flex-end">
|
|
79
|
-
{t.verdict ? <Text color={t.verdict === "PASS" ? C.
|
|
79
|
+
{t.verdict ? <Text color={t.verdict === "FAIL" ? C.bad : t.verdict === "PASS*" ? C.warn : C.good}>{t.verdict}</Text>
|
|
80
80
|
: t.cost ? <Text color={C.dim}>${t.cost.toFixed(2)}</Text> : <Text> </Text>}
|
|
81
81
|
</Box>
|
|
82
82
|
</Box>
|
package/src/types.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export type Backend = "api" | "web";
|
|
7
7
|
|
|
8
8
|
/** What kind of work a task needs. Roles bind to capabilities, never to model names. */
|
|
9
|
-
export type Capability = "plan" | "design" | "code" | "test" | "ops";
|
|
9
|
+
export type Capability = "plan" | "design" | "code" | "review" | "test" | "ops";
|
|
10
10
|
|
|
11
11
|
/** How hard the task is. Drives which tier of model we spend on. */
|
|
12
12
|
export type Difficulty = "trivial" | "low" | "medium" | "high";
|
|
@@ -90,6 +90,8 @@ export interface Task {
|
|
|
90
90
|
epic?: string;
|
|
91
91
|
story?: string;
|
|
92
92
|
dependsOn?: string[];
|
|
93
|
+
/** Human-only annotation shown on the board. Never sent to any model. */
|
|
94
|
+
notes?: string;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
// ---------------------------------------------------------------------------
|
|
@@ -105,6 +107,29 @@ export interface RoutingPolicy {
|
|
|
105
107
|
difficultyToTier: Record<Difficulty, Tier>;
|
|
106
108
|
/** Tester -> Developer feedback loop stop condition (used later, in the orchestrator). */
|
|
107
109
|
maxFeedbackRounds: number;
|
|
110
|
+
/** Per-task limits. A task that runs longer or spends more is aborted, recorded as
|
|
111
|
+
* failed, and the build halts (resumable). 0 = unlimited. */
|
|
112
|
+
taskLimits: TaskLimits;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface TaskLimits {
|
|
116
|
+
timeoutMs: number;
|
|
117
|
+
costCapUSD: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Thrown by an executor when a task breaches its limits. The orchestrator turns it
|
|
121
|
+
* into a failed outcome + halt instead of letting it escape as a crash. */
|
|
122
|
+
export class TaskLimitError extends Error {
|
|
123
|
+
constructor(
|
|
124
|
+
public readonly kind: "timeout" | "cost",
|
|
125
|
+
public readonly taskId: string,
|
|
126
|
+
/** Money already spent on the aborted attempt (must still be billed). */
|
|
127
|
+
public readonly costSoFar: number,
|
|
128
|
+
message: string,
|
|
129
|
+
) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.name = "TaskLimitError";
|
|
132
|
+
}
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
// ---------------------------------------------------------------------------
|
|
@@ -141,6 +166,9 @@ export interface Bug {
|
|
|
141
166
|
export interface Verdict {
|
|
142
167
|
passed: boolean;
|
|
143
168
|
bugs: Bug[];
|
|
169
|
+
/** True when the tester actually ran the app (headless Chromium). False = it could
|
|
170
|
+
* only read the code, so a PASS is weaker than it looks. */
|
|
171
|
+
runtimeChecked: boolean;
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
/** What a role produces when it runs a task. */
|
|
@@ -153,6 +181,9 @@ export interface RoleResult {
|
|
|
153
181
|
cost: number;
|
|
154
182
|
/** Only for test tasks — the pass/fail judgement. */
|
|
155
183
|
verdict?: Verdict;
|
|
184
|
+
/** Set when the task was aborted (limit breach). A failed outcome is billed but
|
|
185
|
+
* never counts as "done": resume rebuilds it. */
|
|
186
|
+
error?: string;
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
/** Executor injected into the orchestrator. Real impl runs Pi; tests pass a fake. */
|
|
@@ -163,6 +194,8 @@ export type RoleExecutor = (input: {
|
|
|
163
194
|
contextText: string;
|
|
164
195
|
/** 0 on first attempt, 1+ during a Tester→Developer feedback round. */
|
|
165
196
|
round: number;
|
|
197
|
+
/** Per-task limits the executor must enforce (throw TaskLimitError on breach). */
|
|
198
|
+
limits: TaskLimits;
|
|
166
199
|
}) => Promise<RoleResult>;
|
|
167
200
|
|
|
168
201
|
/** One recorded step of a build run. */
|