faberun 0.3.0 → 0.6.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 +152 -100
- package/package.json +8 -2
- package/skills/faberun/SKILL.md +6 -5
- package/skills/faberun/references/contract.md +23 -11
- package/skills/faberun/references/engineering.md +3 -1
- package/skills/faberun/references/operations.md +19 -12
- package/skills/faberun/references/rules.md +3 -1
- package/src/campaign/chain.mjs +6 -2
- package/src/campaign/index.mjs +17 -1
- package/src/campaign/metrics.mjs +3 -3
- package/src/cli/brand.mjs +2 -1
- package/src/cli/setup.mjs +109 -30
- package/src/cli/skills.mjs +308 -8
- package/src/cli.mjs +2 -1
- package/src/contract/final-verification.mjs +31 -2
- package/src/contract/index.mjs +27 -24
- package/src/contract/runtime.mjs +5 -1
- package/src/contract/task-packet.mjs +20 -9
- package/src/contract/verification.mjs +1 -1
- package/src/engine/backoff.mjs +1 -1
- package/src/engine/dispatch.mjs +5 -3
- package/src/engine/gate.mjs +12 -0
- package/src/engine/process-identity.mjs +39 -0
- package/src/engine/prompts.mjs +18 -0
- package/src/engine/resume.mjs +2 -2
- package/src/engine/review.mjs +9 -1
- package/src/engine/run-command.mjs +23 -2
- package/src/engine/run-identity.mjs +14 -0
- package/src/engine/scheduler.mjs +45 -12
- package/src/engine/settle.mjs +29 -0
- package/src/engine/supervise.mjs +32 -6
- package/src/engine/verify.mjs +98 -9
- package/src/harnesses/agy/index.mjs +3 -0
- package/src/harnesses/claude/index.mjs +5 -0
- package/src/harnesses/codex/index.mjs +3 -0
- package/src/harnesses/dsh/index.mjs +26 -0
- package/src/harnesses/exec-jsonl/index.mjs +2 -0
- package/src/harnesses/index.mjs +10 -3
- package/src/harnesses/replay/index.mjs +2 -0
- package/src/harnesses/zcode/index.mjs +3 -0
- package/src/host/preflight.mjs +5 -1
- package/src/notify/index.mjs +45 -2
- package/src/repo/source-identity.mjs +4 -3
- package/src/report/render.mjs +128 -49
- package/src/web/index.html +1 -1
package/src/report/render.mjs
CHANGED
|
@@ -22,9 +22,12 @@ const POINTER_ATTENTION_CHARS = 80;
|
|
|
22
22
|
/** @typedef {import("../contract/index.mjs").NodeSnapshot} NodeSnapshot */
|
|
23
23
|
/** @typedef {import("../contract/index.mjs").NodeStatus} NodeStatus */
|
|
24
24
|
/** @typedef {Record<string, unknown>} JsonObject */
|
|
25
|
+
/** @typedef {"priced"|"partial"|"unpriced"|"none"} CostProvenance */
|
|
26
|
+
/** @typedef {{costUsd: number|null, costProvenance: CostProvenance, inputTokens: number, outputTokens: number, cacheReadInputTokens: number, pricedInvocations: number, unpricedInvocations: number}} RoleUsage */
|
|
25
27
|
/** @typedef {{inputTokens: number|null, outputTokens: number|null, cacheReadInputTokens: number|null}} StatusPayloadUsage */
|
|
26
|
-
/** @typedef {{
|
|
27
|
-
/** @typedef {{
|
|
28
|
+
/** @typedef {{index: number, total: number, argv: string}} VerificationProgress */
|
|
29
|
+
/** @typedef {{id: string, status: NodeStatus, phase: string|null, executionPhase: string|null, runtime: string|null, workerRuntime: string|null, continuation: string, attempt: number, revisions: number, startedAt: string|null, updatedAt: string|null, usage: StatusPayloadUsage|null, costUsd: number|null, verdict: string|null, pendingHandoff: {runtime: string, reason: string}|null, note: string|null, scopeFindings: string[]|null, errorCode: string|null, blockedBy: string[], verificationProgress: VerificationProgress|null}} StatusPayloadNode */
|
|
30
|
+
/** @typedef {{schemaVersion: 1, run: string, contractId: string, campaignId: string, goal: string, usage: {inputTokens: number, outputTokens: number, cacheReadInputTokens: number, costUsd: number|null}, roles: {worker: RoleUsage, judge: RoleUsage}, controller: JsonObject, identityWarnings: string[], summary: string, nodes: StatusPayloadNode[]}} StatusPayload */
|
|
28
31
|
|
|
29
32
|
/** The glyph each terminal state prints in a status table. */
|
|
30
33
|
export const MARK = {
|
|
@@ -89,7 +92,7 @@ export function renderStatus(runDir) {
|
|
|
89
92
|
node.note ?? "-",
|
|
90
93
|
]));
|
|
91
94
|
}
|
|
92
|
-
lines.push("```", "", "## Cost", "", `in ${compactTokens(usage.inputTokens)} · out ${compactTokens(usage.outputTokens)} · cache ${compactTokens(usage.cacheReadInputTokens)} · cost ${compactCost(usage.costUsd)}`);
|
|
95
|
+
lines.push("```", "", "## Cost", "", `in ${compactTokens(usage.inputTokens)} · out ${compactTokens(usage.outputTokens)} · cache ${compactTokens(usage.cacheReadInputTokens)} · worker ${formatRole(payload.roles.worker)} · judge ${formatRole(payload.roles.judge)} · cost ${compactCost(usage.costUsd)}`);
|
|
93
96
|
return `${lines.join("\n")}\n`;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -97,7 +100,9 @@ export function renderStatus(runDir) {
|
|
|
97
100
|
* The node the operator should look at right now, formatted the same way
|
|
98
101
|
* the dashboard's now strip is (TECH-SPEC lean, section 4, item 1): the
|
|
99
102
|
* active node's elapsed time and cost so far, or an idle line once every
|
|
100
|
-
* node has settled.
|
|
103
|
+
* node has settled. A node awaiting a controller verification command
|
|
104
|
+
* appends which one, `k/n`, and its argv, so a minutes-long suite is not
|
|
105
|
+
* silent between ticks.
|
|
101
106
|
*
|
|
102
107
|
* @param {StatusPayload} payload
|
|
103
108
|
* @param {number} now epoch ms
|
|
@@ -105,11 +110,29 @@ export function renderStatus(runDir) {
|
|
|
105
110
|
*/
|
|
106
111
|
function nowLine(payload, now) {
|
|
107
112
|
const active = activeStatusNode(payload.nodes);
|
|
108
|
-
if (active)
|
|
113
|
+
if (active) {
|
|
114
|
+
const progress = active.verificationProgress;
|
|
115
|
+
const verification = progress ? ` · verification ${progress.index}/${progress.total} · ${progress.argv}` : "";
|
|
116
|
+
return `now: ${active.id} ${active.status} (${formatElapsed(active, now)}) · ${active.runtime ?? "-"} · ${compactCost(active.costUsd)}${verification}`;
|
|
117
|
+
}
|
|
109
118
|
const allTerminal = payload.nodes.every((node) => SUCCESS.has(node.status));
|
|
110
119
|
return allTerminal ? `now: idle · run done · ${compactCost(payload.usage.costUsd)}` : "now: idle";
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The `verificationProgress` a controller verification pass leaves on a
|
|
124
|
+
* running node's persisted `verification` record — nested there rather than
|
|
125
|
+
* as its own node-snapshot field, since `VerificationState`'s own validator
|
|
126
|
+
* accepts extra keys where the node snapshot's does not.
|
|
127
|
+
*
|
|
128
|
+
* @param {NodeSnapshot} node
|
|
129
|
+
* @returns {VerificationProgress|null}
|
|
130
|
+
*/
|
|
131
|
+
function verificationProgress(node) {
|
|
132
|
+
const verification = /** @type {{progress?: VerificationProgress}|null|undefined} */ (node.verification);
|
|
133
|
+
return verification?.progress ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
/**
|
|
114
137
|
* A node's wall-clock elapsed time: `startedAt` to `updatedAt` once it has
|
|
115
138
|
* settled into a terminal state, `startedAt` to `now` while it is still
|
|
@@ -179,30 +202,38 @@ function buildStatusPayload(runDir, contract, nodes, identityWarnings, usage) {
|
|
|
179
202
|
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
180
203
|
costUsd: usage.costUsd,
|
|
181
204
|
},
|
|
205
|
+
roles: roleUsage(nodes),
|
|
182
206
|
controller: controllerStatus(runDir, nodes).status,
|
|
183
207
|
identityWarnings,
|
|
184
208
|
summary: [...counts].map(([status, count]) => `${count} ${status}`).join(" · "),
|
|
185
|
-
nodes: nodes.map((node) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
nodes: nodes.map((node) => {
|
|
210
|
+
const progress = verificationProgress(node);
|
|
211
|
+
return {
|
|
212
|
+
id: node.id,
|
|
213
|
+
status: node.status,
|
|
214
|
+
phase: contract.nodes.find((candidate) => candidate.id === node.id)?.phase ?? null,
|
|
215
|
+
// A running controller verification command does not move
|
|
216
|
+
// `node.phase` (it stays `worker`, the phase that dispatched it), so
|
|
217
|
+
// the surface that shows it is computed here rather than persisted.
|
|
218
|
+
executionPhase: progress ? "verification" : node.phase,
|
|
219
|
+
runtime: node.runtime ? `${node.runtime.harness}/${node.runtime.model}` : null,
|
|
220
|
+
workerRuntime: workerRuntimeLabel(node),
|
|
221
|
+
continuation: continuationMode(node),
|
|
222
|
+
attempt: node.attempt,
|
|
223
|
+
revisions: node.revisions,
|
|
224
|
+
startedAt: node.startedAt ?? null,
|
|
225
|
+
updatedAt: node.updatedAt ?? null,
|
|
226
|
+
usage: node.usage ? { inputTokens: node.usage.inputTokens ?? null, outputTokens: node.usage.outputTokens ?? null, cacheReadInputTokens: node.usage.cacheReadInputTokens ?? null } : null,
|
|
227
|
+
costUsd: typeof node.costUsd === "number" ? node.costUsd : null,
|
|
228
|
+
verdict: node.gate?.verdict ?? null,
|
|
229
|
+
pendingHandoff: pendingHandoff(node),
|
|
230
|
+
note: statusNote(node),
|
|
231
|
+
scopeFindings: node.scopeFindings?.unexpectedPaths ?? null,
|
|
232
|
+
errorCode: node.error?.code ?? null,
|
|
233
|
+
blockedBy: node.blockedBy ?? [],
|
|
234
|
+
verificationProgress: progress,
|
|
235
|
+
};
|
|
236
|
+
}),
|
|
206
237
|
};
|
|
207
238
|
}
|
|
208
239
|
|
|
@@ -361,8 +392,8 @@ export function renderReport(runDir) {
|
|
|
361
392
|
lines.push(row([MARK[node.status] ?? "[?]", node.id, node.status, node.attempt ?? 0, node.revisions ?? 0, runtime, compactTokens(usage.inputTokens), compactTokens(usage.outputTokens), compactTokens(usage.cacheReadInputTokens), formatCost(cost), note]));
|
|
362
393
|
}
|
|
363
394
|
totals.costUsd = aggregateCost.costUsd;
|
|
364
|
-
const roles =
|
|
365
|
-
lines.push("```", "", `totals · in ${compactTokens(totals.inputTokens)} · out ${compactTokens(totals.outputTokens)} · cache ${compactTokens(totals.cacheReadInputTokens)} · worker ${
|
|
395
|
+
const roles = roleUsage(nodes);
|
|
396
|
+
lines.push("```", "", `totals · in ${compactTokens(totals.inputTokens)} · out ${compactTokens(totals.outputTokens)} · cache ${compactTokens(totals.cacheReadInputTokens)} · worker ${formatRole(roles.worker)} · judge ${formatRole(roles.judge)} · cost ${formatCost(aggregateCost)}`);
|
|
366
397
|
return `${lines.join("\n")}\n`;
|
|
367
398
|
}
|
|
368
399
|
|
|
@@ -401,9 +432,9 @@ export function renderReportJson(runDir) {
|
|
|
401
432
|
const aggregateCost = aggregateCostProjection(costs);
|
|
402
433
|
totals.costUsd = aggregateCost.costUsd;
|
|
403
434
|
totals.costStatus = aggregateCost.status;
|
|
404
|
-
const roles =
|
|
405
|
-
totals.workerCostUsd = roles.worker;
|
|
406
|
-
totals.judgeCostUsd = roles.judge;
|
|
435
|
+
const roles = roleUsage(nodes);
|
|
436
|
+
totals.workerCostUsd = roles.worker.costUsd;
|
|
437
|
+
totals.judgeCostUsd = roles.judge.costUsd;
|
|
407
438
|
const payload = {
|
|
408
439
|
schemaVersion: 1,
|
|
409
440
|
run: basename(runDir),
|
|
@@ -411,6 +442,7 @@ export function renderReportJson(runDir) {
|
|
|
411
442
|
campaignId: contract.campaignId,
|
|
412
443
|
summary: [...counts].map(([status, count]) => `${count} ${status}`).join(" · "),
|
|
413
444
|
totals,
|
|
445
|
+
roles,
|
|
414
446
|
nodes: listed,
|
|
415
447
|
};
|
|
416
448
|
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
@@ -621,39 +653,86 @@ function nodeNote(node) {
|
|
|
621
653
|
/** @typedef {{costUsd: number|null, status: "known"|"estimated"|"ambiguous"}} CostProjection */
|
|
622
654
|
|
|
623
655
|
/**
|
|
624
|
-
* Per-role
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* cannot double-count.
|
|
656
|
+
* Per-role usage from the invocation ledger alone, with the provenance that
|
|
657
|
+
* says whether the role's cost total is honest. A role whose invocations all
|
|
658
|
+
* carry a provider cost is `priced` and its `costUsd` is the sum; a role with
|
|
659
|
+
* any unpriced invocation is `partial` (some priced) or `unpriced` (none), and
|
|
660
|
+
* its `costUsd` stays null rather than summing the known half and understating
|
|
661
|
+
* it. A role with no invocation is `none`. Token totals are always carried, so
|
|
662
|
+
* a role the provider would not price still reads as work.
|
|
632
663
|
*
|
|
633
664
|
* @param {NodeSnapshot[]} nodes
|
|
634
|
-
* @returns {{worker:
|
|
665
|
+
* @returns {{worker: RoleUsage, judge: RoleUsage}}
|
|
635
666
|
*/
|
|
636
|
-
export function
|
|
637
|
-
/** @type {Record<"worker"|"judge", {total: number,
|
|
667
|
+
export function roleUsage(nodes) {
|
|
668
|
+
/** @type {Record<"worker"|"judge", {total: number, priced: number, unpriced: number, inputTokens: number, outputTokens: number, cacheReadInputTokens: number}>} */
|
|
638
669
|
const roles = {
|
|
639
|
-
worker: { total: 0,
|
|
640
|
-
judge: { total: 0,
|
|
670
|
+
worker: { total: 0, priced: 0, unpriced: 0, inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0 },
|
|
671
|
+
judge: { total: 0, priced: 0, unpriced: 0, inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0 },
|
|
641
672
|
};
|
|
642
673
|
for (const node of nodes) {
|
|
643
674
|
for (const invocation of node.invocations ?? []) {
|
|
644
675
|
if (invocation.role !== "worker" && invocation.role !== "judge") continue;
|
|
645
676
|
const bucket = roles[invocation.role];
|
|
646
677
|
const cost = finite(invocation.costUsd);
|
|
647
|
-
if (cost === null) bucket.
|
|
678
|
+
if (cost === null) bucket.unpriced += 1;
|
|
648
679
|
else {
|
|
649
|
-
bucket.
|
|
680
|
+
bucket.priced += 1;
|
|
650
681
|
bucket.total += cost;
|
|
651
682
|
}
|
|
683
|
+
bucket.inputTokens += finite(invocation.usage?.inputTokens) ?? 0;
|
|
684
|
+
bucket.outputTokens += finite(invocation.usage?.outputTokens) ?? 0;
|
|
685
|
+
bucket.cacheReadInputTokens += finite(invocation.usage?.cacheReadInputTokens) ?? 0;
|
|
652
686
|
}
|
|
653
687
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
688
|
+
return { worker: summarizeRole(roles.worker), judge: summarizeRole(roles.judge) };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* @param {{total: number, priced: number, unpriced: number, inputTokens: number, outputTokens: number, cacheReadInputTokens: number}} bucket
|
|
693
|
+
* @returns {RoleUsage}
|
|
694
|
+
*/
|
|
695
|
+
function summarizeRole(bucket) {
|
|
696
|
+
const costProvenance = bucket.priced > 0
|
|
697
|
+
? (bucket.unpriced > 0 ? "partial" : "priced")
|
|
698
|
+
: (bucket.unpriced > 0 ? "unpriced" : "none");
|
|
699
|
+
return {
|
|
700
|
+
costUsd: costProvenance === "priced" ? bucket.total : null,
|
|
701
|
+
costProvenance,
|
|
702
|
+
inputTokens: bucket.inputTokens,
|
|
703
|
+
outputTokens: bucket.outputTokens,
|
|
704
|
+
cacheReadInputTokens: bucket.cacheReadInputTokens,
|
|
705
|
+
pricedInvocations: bucket.priced,
|
|
706
|
+
unpricedInvocations: bucket.unpriced,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Per-role cost, summed from the invocation ledger alone. Kept for the closing
|
|
712
|
+
* artifacts, which render only the dollar cell; `roleUsage` carries the
|
|
713
|
+
* provenance that cell cannot express.
|
|
714
|
+
*
|
|
715
|
+
* @param {NodeSnapshot[]} nodes
|
|
716
|
+
* @returns {{worker: number|null, judge: number|null}}
|
|
717
|
+
*/
|
|
718
|
+
export function roleCosts(nodes) {
|
|
719
|
+
const roles = roleUsage(nodes);
|
|
720
|
+
return { worker: roles.worker.costUsd, judge: roles.judge.costUsd };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* A role's cell in a totals line: the dollar total when every invocation is
|
|
725
|
+
* priced, `unpriced` with the token totals it did record when any invocation
|
|
726
|
+
* has no cost, and `-` only when the role has no invocation at all. An
|
|
727
|
+
* unpriced role reads as real work instead of vanishing into a dash.
|
|
728
|
+
*
|
|
729
|
+
* @param {RoleUsage} role
|
|
730
|
+
* @returns {string}
|
|
731
|
+
*/
|
|
732
|
+
function formatRole(role) {
|
|
733
|
+
if (role.costUsd !== null) return compactCost(role.costUsd);
|
|
734
|
+
if (role.costProvenance === "none") return "-";
|
|
735
|
+
return `unpriced (in ${compactTokens(role.inputTokens)} · out ${compactTokens(role.outputTokens)} · cache ${compactTokens(role.cacheReadInputTokens)})`;
|
|
657
736
|
}
|
|
658
737
|
|
|
659
738
|
/**
|
package/src/web/index.html
CHANGED
|
@@ -208,7 +208,7 @@ function handoffHtml(handoff) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
function renderSectionsHtml(snapshot) {
|
|
211
|
-
if (!snapshot?.selectedCampaignId) return `<p class="empty">no campaign found under .runs; initialize one with:
|
|
211
|
+
if (!snapshot?.selectedCampaignId) return `<p class="empty">no campaign found under .runs; initialize one with: faberun campaign init</p>`;
|
|
212
212
|
return nowHtml(snapshot.now) + needsYouHtml(snapshot.needsYou) + runsHtml(snapshot.runs, state.runId) + drawerHtml(snapshot.drawer, state.tab) + handoffHtml(snapshot.handoff);
|
|
213
213
|
}
|
|
214
214
|
|