faberun 0.3.0 → 0.7.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 +10 -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/campaign.mjs +2 -0
- package/src/cli/contract.mjs +2 -0
- package/src/cli/manual.mjs +341 -0
- package/src/cli/seat.mjs +2 -0
- package/src/cli/setup.mjs +109 -30
- package/src/cli/skills.mjs +310 -8
- package/src/cli.mjs +3 -2
- package/src/contract/final-verification.mjs +31 -2
- package/src/contract/index.mjs +28 -25
- package/src/contract/runtime.mjs +5 -1
- package/src/contract/snapshot.mjs +7 -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 +31 -4
- 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/final.mjs +3 -2
- package/src/report/render.mjs +134 -51
- 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, declaredReadBytes: number|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,8 @@ export function renderStatus(runDir) {
|
|
|
89
92
|
node.note ?? "-",
|
|
90
93
|
]));
|
|
91
94
|
}
|
|
92
|
-
|
|
95
|
+
const readBytes = payload.nodes.reduce((total, node) => total + (node.declaredReadBytes ?? 0), 0);
|
|
96
|
+
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)} · read ${compactTokens(readBytes)}`);
|
|
93
97
|
return `${lines.join("\n")}\n`;
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -97,7 +101,9 @@ export function renderStatus(runDir) {
|
|
|
97
101
|
* The node the operator should look at right now, formatted the same way
|
|
98
102
|
* the dashboard's now strip is (TECH-SPEC lean, section 4, item 1): the
|
|
99
103
|
* active node's elapsed time and cost so far, or an idle line once every
|
|
100
|
-
* node has settled.
|
|
104
|
+
* node has settled. A node awaiting a controller verification command
|
|
105
|
+
* appends which one, `k/n`, and its argv, so a minutes-long suite is not
|
|
106
|
+
* silent between ticks.
|
|
101
107
|
*
|
|
102
108
|
* @param {StatusPayload} payload
|
|
103
109
|
* @param {number} now epoch ms
|
|
@@ -105,11 +111,29 @@ export function renderStatus(runDir) {
|
|
|
105
111
|
*/
|
|
106
112
|
function nowLine(payload, now) {
|
|
107
113
|
const active = activeStatusNode(payload.nodes);
|
|
108
|
-
if (active)
|
|
114
|
+
if (active) {
|
|
115
|
+
const progress = active.verificationProgress;
|
|
116
|
+
const verification = progress ? ` · verification ${progress.index}/${progress.total} · ${progress.argv}` : "";
|
|
117
|
+
return `now: ${active.id} ${active.status} (${formatElapsed(active, now)}) · ${active.runtime ?? "-"} · ${compactCost(active.costUsd)}${verification}`;
|
|
118
|
+
}
|
|
109
119
|
const allTerminal = payload.nodes.every((node) => SUCCESS.has(node.status));
|
|
110
120
|
return allTerminal ? `now: idle · run done · ${compactCost(payload.usage.costUsd)}` : "now: idle";
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The `verificationProgress` a controller verification pass leaves on a
|
|
125
|
+
* running node's persisted `verification` record — nested there rather than
|
|
126
|
+
* as its own node-snapshot field, since `VerificationState`'s own validator
|
|
127
|
+
* accepts extra keys where the node snapshot's does not.
|
|
128
|
+
*
|
|
129
|
+
* @param {NodeSnapshot} node
|
|
130
|
+
* @returns {VerificationProgress|null}
|
|
131
|
+
*/
|
|
132
|
+
function verificationProgress(node) {
|
|
133
|
+
const verification = /** @type {{progress?: VerificationProgress}|null|undefined} */ (node.verification);
|
|
134
|
+
return verification?.progress ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
/**
|
|
114
138
|
* A node's wall-clock elapsed time: `startedAt` to `updatedAt` once it has
|
|
115
139
|
* settled into a terminal state, `startedAt` to `now` while it is still
|
|
@@ -179,30 +203,39 @@ function buildStatusPayload(runDir, contract, nodes, identityWarnings, usage) {
|
|
|
179
203
|
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
180
204
|
costUsd: usage.costUsd,
|
|
181
205
|
},
|
|
206
|
+
roles: roleUsage(nodes),
|
|
182
207
|
controller: controllerStatus(runDir, nodes).status,
|
|
183
208
|
identityWarnings,
|
|
184
209
|
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
|
-
|
|
210
|
+
nodes: nodes.map((node) => {
|
|
211
|
+
const progress = verificationProgress(node);
|
|
212
|
+
return {
|
|
213
|
+
id: node.id,
|
|
214
|
+
status: node.status,
|
|
215
|
+
phase: contract.nodes.find((candidate) => candidate.id === node.id)?.phase ?? null,
|
|
216
|
+
// A running controller verification command does not move
|
|
217
|
+
// `node.phase` (it stays `worker`, the phase that dispatched it), so
|
|
218
|
+
// the surface that shows it is computed here rather than persisted.
|
|
219
|
+
executionPhase: progress ? "verification" : node.phase,
|
|
220
|
+
runtime: node.runtime ? `${node.runtime.harness}/${node.runtime.model}` : null,
|
|
221
|
+
workerRuntime: workerRuntimeLabel(node),
|
|
222
|
+
continuation: continuationMode(node),
|
|
223
|
+
attempt: node.attempt,
|
|
224
|
+
revisions: node.revisions,
|
|
225
|
+
startedAt: node.startedAt ?? null,
|
|
226
|
+
updatedAt: node.updatedAt ?? null,
|
|
227
|
+
usage: node.usage ? { inputTokens: node.usage.inputTokens ?? null, outputTokens: node.usage.outputTokens ?? null, cacheReadInputTokens: node.usage.cacheReadInputTokens ?? null } : null,
|
|
228
|
+
costUsd: typeof node.costUsd === "number" ? node.costUsd : null,
|
|
229
|
+
verdict: node.gate?.verdict ?? null,
|
|
230
|
+
pendingHandoff: pendingHandoff(node),
|
|
231
|
+
note: statusNote(node),
|
|
232
|
+
scopeFindings: node.scopeFindings?.unexpectedPaths ?? null,
|
|
233
|
+
errorCode: node.error?.code ?? null,
|
|
234
|
+
blockedBy: node.blockedBy ?? [],
|
|
235
|
+
verificationProgress: progress,
|
|
236
|
+
declaredReadBytes: typeof node.declaredReadBytes === "number" ? node.declaredReadBytes : null,
|
|
237
|
+
};
|
|
238
|
+
}),
|
|
206
239
|
};
|
|
207
240
|
}
|
|
208
241
|
|
|
@@ -361,8 +394,8 @@ export function renderReport(runDir) {
|
|
|
361
394
|
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
395
|
}
|
|
363
396
|
totals.costUsd = aggregateCost.costUsd;
|
|
364
|
-
const roles =
|
|
365
|
-
lines.push("```", "", `totals · in ${compactTokens(totals.inputTokens)} · out ${compactTokens(totals.outputTokens)} · cache ${compactTokens(totals.cacheReadInputTokens)} · worker ${
|
|
397
|
+
const roles = roleUsage(nodes);
|
|
398
|
+
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
399
|
return `${lines.join("\n")}\n`;
|
|
367
400
|
}
|
|
368
401
|
|
|
@@ -376,13 +409,14 @@ export function renderReportJson(runDir) {
|
|
|
376
409
|
const { contract, nodes } = loadRun(runDir);
|
|
377
410
|
const counts = new Map();
|
|
378
411
|
for (const node of nodes) counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
379
|
-
/** @type {{inputTokens: number, outputTokens: number, cacheReadInputTokens: number, costUsd: number|null, costStatus: string, workerCostUsd: number|null, judgeCostUsd: number|null}} */
|
|
380
|
-
const totals = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, costUsd: null, costStatus: "ambiguous", workerCostUsd: null, judgeCostUsd: null };
|
|
412
|
+
/** @type {{inputTokens: number, outputTokens: number, cacheReadInputTokens: number, costUsd: number|null, costStatus: string, workerCostUsd: number|null, judgeCostUsd: number|null, declaredReadBytes: number}} */
|
|
413
|
+
const totals = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, costUsd: null, costStatus: "ambiguous", workerCostUsd: null, judgeCostUsd: null, declaredReadBytes: 0 };
|
|
381
414
|
const costs = nodes.map(costProjection);
|
|
382
415
|
const listed = nodes.map((node, index) => {
|
|
383
416
|
const usage = node.usage ?? { inputTokens: null, outputTokens: null, cacheReadInputTokens: null };
|
|
384
417
|
for (const key of /** @type {("inputTokens"|"outputTokens"|"cacheReadInputTokens")[]} */ (["inputTokens", "outputTokens", "cacheReadInputTokens"])) totals[key] = (totals[key] ?? 0) + (usage[key] ?? 0);
|
|
385
418
|
const cost = costs[index];
|
|
419
|
+
totals.declaredReadBytes += typeof node.declaredReadBytes === "number" ? node.declaredReadBytes : 0;
|
|
386
420
|
return {
|
|
387
421
|
id: node.id,
|
|
388
422
|
status: node.status,
|
|
@@ -396,14 +430,15 @@ export function renderReportJson(runDir) {
|
|
|
396
430
|
costStatus: cost.status,
|
|
397
431
|
continuation: continuationMode(node),
|
|
398
432
|
note: nodeNote(node),
|
|
433
|
+
declaredReadBytes: typeof node.declaredReadBytes === "number" ? node.declaredReadBytes : null,
|
|
399
434
|
};
|
|
400
435
|
});
|
|
401
436
|
const aggregateCost = aggregateCostProjection(costs);
|
|
402
437
|
totals.costUsd = aggregateCost.costUsd;
|
|
403
438
|
totals.costStatus = aggregateCost.status;
|
|
404
|
-
const roles =
|
|
405
|
-
totals.workerCostUsd = roles.worker;
|
|
406
|
-
totals.judgeCostUsd = roles.judge;
|
|
439
|
+
const roles = roleUsage(nodes);
|
|
440
|
+
totals.workerCostUsd = roles.worker.costUsd;
|
|
441
|
+
totals.judgeCostUsd = roles.judge.costUsd;
|
|
407
442
|
const payload = {
|
|
408
443
|
schemaVersion: 1,
|
|
409
444
|
run: basename(runDir),
|
|
@@ -411,6 +446,7 @@ export function renderReportJson(runDir) {
|
|
|
411
446
|
campaignId: contract.campaignId,
|
|
412
447
|
summary: [...counts].map(([status, count]) => `${count} ${status}`).join(" · "),
|
|
413
448
|
totals,
|
|
449
|
+
roles,
|
|
414
450
|
nodes: listed,
|
|
415
451
|
};
|
|
416
452
|
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
@@ -621,39 +657,86 @@ function nodeNote(node) {
|
|
|
621
657
|
/** @typedef {{costUsd: number|null, status: "known"|"estimated"|"ambiguous"}} CostProjection */
|
|
622
658
|
|
|
623
659
|
/**
|
|
624
|
-
* Per-role
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* cannot double-count.
|
|
660
|
+
* Per-role usage from the invocation ledger alone, with the provenance that
|
|
661
|
+
* says whether the role's cost total is honest. A role whose invocations all
|
|
662
|
+
* carry a provider cost is `priced` and its `costUsd` is the sum; a role with
|
|
663
|
+
* any unpriced invocation is `partial` (some priced) or `unpriced` (none), and
|
|
664
|
+
* its `costUsd` stays null rather than summing the known half and understating
|
|
665
|
+
* it. A role with no invocation is `none`. Token totals are always carried, so
|
|
666
|
+
* a role the provider would not price still reads as work.
|
|
632
667
|
*
|
|
633
668
|
* @param {NodeSnapshot[]} nodes
|
|
634
|
-
* @returns {{worker:
|
|
669
|
+
* @returns {{worker: RoleUsage, judge: RoleUsage}}
|
|
635
670
|
*/
|
|
636
|
-
export function
|
|
637
|
-
/** @type {Record<"worker"|"judge", {total: number,
|
|
671
|
+
export function roleUsage(nodes) {
|
|
672
|
+
/** @type {Record<"worker"|"judge", {total: number, priced: number, unpriced: number, inputTokens: number, outputTokens: number, cacheReadInputTokens: number}>} */
|
|
638
673
|
const roles = {
|
|
639
|
-
worker: { total: 0,
|
|
640
|
-
judge: { total: 0,
|
|
674
|
+
worker: { total: 0, priced: 0, unpriced: 0, inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0 },
|
|
675
|
+
judge: { total: 0, priced: 0, unpriced: 0, inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0 },
|
|
641
676
|
};
|
|
642
677
|
for (const node of nodes) {
|
|
643
678
|
for (const invocation of node.invocations ?? []) {
|
|
644
679
|
if (invocation.role !== "worker" && invocation.role !== "judge") continue;
|
|
645
680
|
const bucket = roles[invocation.role];
|
|
646
681
|
const cost = finite(invocation.costUsd);
|
|
647
|
-
if (cost === null) bucket.
|
|
682
|
+
if (cost === null) bucket.unpriced += 1;
|
|
648
683
|
else {
|
|
649
|
-
bucket.
|
|
684
|
+
bucket.priced += 1;
|
|
650
685
|
bucket.total += cost;
|
|
651
686
|
}
|
|
687
|
+
bucket.inputTokens += finite(invocation.usage?.inputTokens) ?? 0;
|
|
688
|
+
bucket.outputTokens += finite(invocation.usage?.outputTokens) ?? 0;
|
|
689
|
+
bucket.cacheReadInputTokens += finite(invocation.usage?.cacheReadInputTokens) ?? 0;
|
|
652
690
|
}
|
|
653
691
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
692
|
+
return { worker: summarizeRole(roles.worker), judge: summarizeRole(roles.judge) };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* @param {{total: number, priced: number, unpriced: number, inputTokens: number, outputTokens: number, cacheReadInputTokens: number}} bucket
|
|
697
|
+
* @returns {RoleUsage}
|
|
698
|
+
*/
|
|
699
|
+
function summarizeRole(bucket) {
|
|
700
|
+
const costProvenance = bucket.priced > 0
|
|
701
|
+
? (bucket.unpriced > 0 ? "partial" : "priced")
|
|
702
|
+
: (bucket.unpriced > 0 ? "unpriced" : "none");
|
|
703
|
+
return {
|
|
704
|
+
costUsd: costProvenance === "priced" ? bucket.total : null,
|
|
705
|
+
costProvenance,
|
|
706
|
+
inputTokens: bucket.inputTokens,
|
|
707
|
+
outputTokens: bucket.outputTokens,
|
|
708
|
+
cacheReadInputTokens: bucket.cacheReadInputTokens,
|
|
709
|
+
pricedInvocations: bucket.priced,
|
|
710
|
+
unpricedInvocations: bucket.unpriced,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Per-role cost, summed from the invocation ledger alone. Kept for the closing
|
|
716
|
+
* artifacts, which render only the dollar cell; `roleUsage` carries the
|
|
717
|
+
* provenance that cell cannot express.
|
|
718
|
+
*
|
|
719
|
+
* @param {NodeSnapshot[]} nodes
|
|
720
|
+
* @returns {{worker: number|null, judge: number|null}}
|
|
721
|
+
*/
|
|
722
|
+
export function roleCosts(nodes) {
|
|
723
|
+
const roles = roleUsage(nodes);
|
|
724
|
+
return { worker: roles.worker.costUsd, judge: roles.judge.costUsd };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* A role's cell in a totals line: the dollar total when every invocation is
|
|
729
|
+
* priced, `unpriced` with the token totals it did record when any invocation
|
|
730
|
+
* has no cost, and `-` only when the role has no invocation at all. An
|
|
731
|
+
* unpriced role reads as real work instead of vanishing into a dash.
|
|
732
|
+
*
|
|
733
|
+
* @param {RoleUsage} role
|
|
734
|
+
* @returns {string}
|
|
735
|
+
*/
|
|
736
|
+
function formatRole(role) {
|
|
737
|
+
if (role.costUsd !== null) return compactCost(role.costUsd);
|
|
738
|
+
if (role.costProvenance === "none") return "-";
|
|
739
|
+
return `unpriced (in ${compactTokens(role.inputTokens)} · out ${compactTokens(role.outputTokens)} · cache ${compactTokens(role.cacheReadInputTokens)})`;
|
|
657
740
|
}
|
|
658
741
|
|
|
659
742
|
/**
|
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
|
|