faberun 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/faberun.mjs +25 -0
- package/integrations/claude-code/statusline-bench.sh +42 -0
- package/integrations/claude-code/statusline.sh +80 -0
- package/package.json +33 -0
- package/skills/faberun/SKILL.md +24 -0
- package/skills/faberun/references/contract.md +380 -0
- package/skills/faberun/references/engineering.md +29 -0
- package/skills/faberun/references/handoffs.md +26 -0
- package/skills/faberun/references/operations.md +184 -0
- package/skills/faberun/references/rules.md +35 -0
- package/skills/faberun/references/workflow.md +23 -0
- package/skills/init-agentkit/SKILL.md +108 -0
- package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
- package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
- package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
- package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
- package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
- package/skills/init-agentkit/templates/AGENTS.md +110 -0
- package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
- package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
- package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
- package/skills/init-agentkit/templates/docs/VISION.md +33 -0
- package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
- package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
- package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
- package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
- package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
- package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
- package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
- package/src/campaign/brief.mjs +394 -0
- package/src/campaign/chain.mjs +555 -0
- package/src/campaign/handoff.mjs +516 -0
- package/src/campaign/index.mjs +300 -0
- package/src/campaign/journal.mjs +347 -0
- package/src/campaign/layout.mjs +51 -0
- package/src/campaign/metrics-evals.mjs +25 -0
- package/src/campaign/metrics.mjs +517 -0
- package/src/campaign/projection.mjs +250 -0
- package/src/campaign/record.mjs +102 -0
- package/src/campaign/unpark.mjs +56 -0
- package/src/cli/brand.mjs +205 -0
- package/src/cli/campaign.mjs +730 -0
- package/src/cli/contract.mjs +67 -0
- package/src/cli/init.mjs +170 -0
- package/src/cli/launch.mjs +239 -0
- package/src/cli/seat.mjs +139 -0
- package/src/cli/setup.mjs +294 -0
- package/src/cli/skills.mjs +105 -0
- package/src/cli/update.mjs +216 -0
- package/src/cli.mjs +525 -0
- package/src/contract/articles.mjs +12 -0
- package/src/contract/assert.mjs +162 -0
- package/src/contract/definition-of-done.mjs +97 -0
- package/src/contract/final-verification.mjs +96 -0
- package/src/contract/index.mjs +641 -0
- package/src/contract/judge-envelope.mjs +25 -0
- package/src/contract/review-modes.mjs +151 -0
- package/src/contract/runtime.mjs +204 -0
- package/src/contract/schema-version.mjs +25 -0
- package/src/contract/scope-findings.mjs +77 -0
- package/src/contract/snapshot.mjs +639 -0
- package/src/contract/task-packet.mjs +495 -0
- package/src/contract/untrusted.mjs +75 -0
- package/src/contract/verification.mjs +185 -0
- package/src/contract/worker-result.mjs +138 -0
- package/src/engine/assignment.mjs +63 -0
- package/src/engine/backoff.mjs +492 -0
- package/src/engine/bulk-read.mjs +361 -0
- package/src/engine/cancel.mjs +177 -0
- package/src/engine/detach.mjs +101 -0
- package/src/engine/dispatch.mjs +752 -0
- package/src/engine/failover.mjs +192 -0
- package/src/engine/gate.mjs +183 -0
- package/src/engine/judge-gate.mjs +517 -0
- package/src/engine/lifecycle.mjs +772 -0
- package/src/engine/live-preflight.mjs +299 -0
- package/src/engine/mutation.mjs +146 -0
- package/src/engine/notify-queue.mjs +327 -0
- package/src/engine/process-identity.mjs +72 -0
- package/src/engine/process.mjs +774 -0
- package/src/engine/prompts.mjs +289 -0
- package/src/engine/recover.mjs +300 -0
- package/src/engine/result-file.mjs +222 -0
- package/src/engine/resume.mjs +635 -0
- package/src/engine/retry.mjs +334 -0
- package/src/engine/review.mjs +228 -0
- package/src/engine/run-command.mjs +287 -0
- package/src/engine/run-identity.mjs +411 -0
- package/src/engine/runtime-discovery.mjs +235 -0
- package/src/engine/scheduler.mjs +526 -0
- package/src/engine/scope.mjs +378 -0
- package/src/engine/settle.mjs +207 -0
- package/src/engine/state.mjs +148 -0
- package/src/engine/supervise.mjs +713 -0
- package/src/engine/verify.mjs +167 -0
- package/src/harnesses/agy/index.mjs +62 -0
- package/src/harnesses/catalogue.mjs +509 -0
- package/src/harnesses/claude/index.mjs +90 -0
- package/src/harnesses/codex/index.mjs +87 -0
- package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
- package/src/harnesses/dsh/index.mjs +210 -0
- package/src/harnesses/dsh/runner.mjs +259 -0
- package/src/harnesses/exec-jsonl/index.mjs +788 -0
- package/src/harnesses/index.mjs +508 -0
- package/src/harnesses/protocol.mjs +531 -0
- package/src/harnesses/replay/bin.mjs +386 -0
- package/src/harnesses/replay/index.mjs +238 -0
- package/src/harnesses/zcode/index.mjs +276 -0
- package/src/host/config.mjs +87 -0
- package/src/host/home.mjs +149 -0
- package/src/host/package.mjs +23 -0
- package/src/host/preflight.mjs +520 -0
- package/src/host/tool-policy-decisions.mjs +341 -0
- package/src/host/tool-policy-hook.mjs +270 -0
- package/src/notify/index.mjs +359 -0
- package/src/notify/os-macos.mjs +81 -0
- package/src/repo/declared-paths.mjs +220 -0
- package/src/repo/integrate.mjs +546 -0
- package/src/repo/scope-closure.mjs +665 -0
- package/src/repo/signal-block.mjs +16 -0
- package/src/repo/signal.mjs +222 -0
- package/src/repo/source-identity.mjs +295 -0
- package/src/repo/workspace.mjs +557 -0
- package/src/repo/worktree.mjs +352 -0
- package/src/report/final.mjs +200 -0
- package/src/report/metrics-report.mjs +99 -0
- package/src/report/next.mjs +383 -0
- package/src/report/render.mjs +716 -0
- package/src/run/disk-gc.mjs +251 -0
- package/src/run/lock.mjs +329 -0
- package/src/run/node-store.mjs +62 -0
- package/src/run/operations.mjs +286 -0
- package/src/run/store.mjs +187 -0
- package/src/run/usage.mjs +337 -0
- package/src/seat/harnesses.mjs +83 -0
- package/src/seat/index.mjs +239 -0
- package/src/seat/tmux.mjs +208 -0
- package/src/util.mjs +0 -0
- package/src/web/api.mjs +371 -0
- package/src/web/boundary.mjs +88 -0
- package/src/web/index.html +299 -0
- package/src/web/server.mjs +552 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `operator-brief.md`: the smallest set of durable facts a fresh seat needs to
|
|
3
|
+
* take over a campaign, inside a hard 4 KiB budget.
|
|
4
|
+
*
|
|
5
|
+
* It is a pure function of recorded facts -- the projected state plus each
|
|
6
|
+
* linked run's `status.json` -- so the same facts always produce the same bytes
|
|
7
|
+
* and a brief can be rebuilt from the append-only journal alone. No model
|
|
8
|
+
* writes it: a generated summary could not be reconstructed after the seat that
|
|
9
|
+
* would have generated it died, which is the only case the brief exists for.
|
|
10
|
+
*
|
|
11
|
+
* The budget is a hard ceiling, not a target. A brief that overflows is the
|
|
12
|
+
* symptom of incomplete durable state, so what does not fit is dropped as whole
|
|
13
|
+
* records with a visible omission note, never by slicing the document.
|
|
14
|
+
*/
|
|
15
|
+
import { BRIEF_BYTES, BRIEF_FILE, ID_CAP_FLOOR } from "./layout.mjs";
|
|
16
|
+
import { boundedText, compactCost, compactTokens, finite, readJsonTolerant } from "../util.mjs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { readCampaign } from "./record.mjs";
|
|
19
|
+
import { readProjectionState } from "./projection.mjs";
|
|
20
|
+
import { writeTextAtomic } from "../run/store.mjs";
|
|
21
|
+
|
|
22
|
+
/** @typedef {import("./index.mjs").Campaign} Campaign */
|
|
23
|
+
/** @typedef {import("./index.mjs").JournalEntry} JournalEntry */
|
|
24
|
+
/** @typedef {import("./index.mjs").Projection} Projection */
|
|
25
|
+
/** @typedef {{id: string, exists: boolean, summary: string, controller: string, phase: string|null, done: number, total: number, costUsd: number|null, inputTokens: number|null, outputTokens: number|null, attention: {id: string, status: string, note: string}[]}} BriefRun */
|
|
26
|
+
/** @typedef {{campaign: Campaign, updatedAt: string, phase: string|null, checkpoints: {done: number, total: number}, runs: BriefRun[], openQuestions: JournalEntry[], transitions: JournalEntry[], budget: {costUsd: number|null, inputTokens: number|null, outputTokens: number|null}, notes: JournalEntry[], commands: string[]}} Brief */
|
|
27
|
+
/** @typedef {{limit: number, used: number, lines: string[]}} BriefBudget */
|
|
28
|
+
|
|
29
|
+
const BRIEF_TEXT_BYTES = 512;
|
|
30
|
+
const TEXT_CAPS = [BRIEF_TEXT_BYTES, 256, 128, 64, 0];
|
|
31
|
+
const SECTION_FLOOR_BYTES = 256;
|
|
32
|
+
const TRANSITION_LIMIT = 3;
|
|
33
|
+
const OPERATOR_NOTE_LIMIT = 5;
|
|
34
|
+
const RUN_LINE_LIMIT = 8;
|
|
35
|
+
const QUESTION_LINE_LIMIT = 8;
|
|
36
|
+
const ATTENTION_LINE_LIMIT = 3;
|
|
37
|
+
const QUIET_NODE_STATUSES = ["pending", "running", "done", "no-op"];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The facts of one brief, split from its rendering so the same facts always
|
|
41
|
+
* render the same markdown.
|
|
42
|
+
*
|
|
43
|
+
* @param {Campaign} campaign
|
|
44
|
+
* @param {Projection} state
|
|
45
|
+
* @param {BriefRun[]} runSummaries
|
|
46
|
+
* @returns {Brief}
|
|
47
|
+
*/
|
|
48
|
+
export function briefFromState(campaign, state, runSummaries) {
|
|
49
|
+
return {
|
|
50
|
+
campaign,
|
|
51
|
+
updatedAt: state.updatedAt ?? campaign.updatedAt,
|
|
52
|
+
phase: runSummaries.find((run) => run.phase !== null)?.phase ?? null,
|
|
53
|
+
checkpoints: {
|
|
54
|
+
done: runSummaries.reduce((total, run) => total + run.done, 0),
|
|
55
|
+
total: runSummaries.reduce((total, run) => total + run.total, 0),
|
|
56
|
+
},
|
|
57
|
+
runs: runSummaries,
|
|
58
|
+
openQuestions: sortEntries(Object.values(state.questions)),
|
|
59
|
+
transitions: recentTransitions(state),
|
|
60
|
+
budget: totalBudget(runSummaries),
|
|
61
|
+
notes: state.intents.slice(-OPERATOR_NOTE_LIMIT),
|
|
62
|
+
commands: briefCommands(campaign.id),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} campaignPath
|
|
68
|
+
* @param {Brief} brief
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function materializeBrief(campaignPath, brief) {
|
|
72
|
+
const text = fitBrief(brief);
|
|
73
|
+
writeTextAtomic(join(campaignPath, BRIEF_FILE), text);
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read the campaign's live projection and each linked run's `status.json`, then
|
|
79
|
+
* materialize the brief. `heartbeat.json` does not exist in this tree -- the
|
|
80
|
+
* projection and per-run status are the durable sources.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} campaignPath
|
|
83
|
+
* @param {string} runsDir
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function renderBrief(campaignPath, runsDir) {
|
|
87
|
+
const campaign = readCampaign(campaignPath);
|
|
88
|
+
const { state } = readProjectionState(campaignPath, campaign);
|
|
89
|
+
return materializeBrief(campaignPath, briefFromState(campaign, state, readRunBriefs(runsDir, campaign.linkedRunIds)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} runsDir
|
|
94
|
+
* @param {string[]} runIds
|
|
95
|
+
* @returns {BriefRun[]}
|
|
96
|
+
*/
|
|
97
|
+
function readRunBriefs(runsDir, runIds) {
|
|
98
|
+
return runIds.map((runId) => runBrief(runId, readJsonTolerant(join(runsDir, runId, "status.json"))));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} runId
|
|
103
|
+
* @param {unknown} status
|
|
104
|
+
* @returns {BriefRun}
|
|
105
|
+
*/
|
|
106
|
+
function runBrief(runId, status) {
|
|
107
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return missingRunBrief(runId);
|
|
108
|
+
const record = /** @type {Record<string, any>} */ (status);
|
|
109
|
+
const nodes = Array.isArray(record.nodes) ? record.nodes : [];
|
|
110
|
+
const usage = record.usage && typeof record.usage === "object" ? /** @type {Record<string, unknown>} */ (record.usage) : {};
|
|
111
|
+
const active = nodes.find((node) => node.status === "running")
|
|
112
|
+
?? nodes.find((node) => !QUIET_NODE_STATUSES.includes(String(node.status)));
|
|
113
|
+
/** @type {string|null} */
|
|
114
|
+
let phase = null;
|
|
115
|
+
if (active) {
|
|
116
|
+
if (typeof active.phase === "string") phase = active.phase;
|
|
117
|
+
else if (typeof active.executionPhase === "string") phase = active.executionPhase;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: runId,
|
|
121
|
+
exists: true,
|
|
122
|
+
summary: typeof record.summary === "string" ? record.summary : "",
|
|
123
|
+
controller: typeof record.controller?.state === "string" ? record.controller.state : "none",
|
|
124
|
+
phase,
|
|
125
|
+
done: nodes.filter((node) => node.status === "done" || node.status === "no-op").length,
|
|
126
|
+
total: nodes.length,
|
|
127
|
+
costUsd: finite(usage.costUsd),
|
|
128
|
+
inputTokens: finite(usage.inputTokens),
|
|
129
|
+
outputTokens: finite(usage.outputTokens),
|
|
130
|
+
attention: nodes
|
|
131
|
+
.filter((node) => !QUIET_NODE_STATUSES.includes(String(node.status)))
|
|
132
|
+
.map((node) => ({
|
|
133
|
+
id: String(node.id ?? ""),
|
|
134
|
+
status: String(node.status ?? ""),
|
|
135
|
+
note: typeof node.note === "string" ? node.note : typeof node.errorCode === "string" ? node.errorCode : "",
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} runId
|
|
142
|
+
* @returns {BriefRun}
|
|
143
|
+
*/
|
|
144
|
+
function missingRunBrief(runId) {
|
|
145
|
+
return { id: runId, exists: false, summary: "no status.json yet", controller: "none", phase: null, done: 0, total: 0, costUsd: null, inputTokens: null, outputTokens: null, attention: [] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {Projection} state
|
|
150
|
+
* @returns {JournalEntry[]}
|
|
151
|
+
*/
|
|
152
|
+
function recentTransitions(state) {
|
|
153
|
+
/** @type {JournalEntry[]} */
|
|
154
|
+
const entries = [
|
|
155
|
+
...state.sessions,
|
|
156
|
+
...Object.values(state.decisions),
|
|
157
|
+
...state.constraints,
|
|
158
|
+
...state.outcomes,
|
|
159
|
+
...Object.values(state.questions),
|
|
160
|
+
];
|
|
161
|
+
if (state.next) entries.push(state.next);
|
|
162
|
+
return sortEntries(entries).slice(-TRANSITION_LIMIT);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {BriefRun[]} runs
|
|
167
|
+
* @returns {{costUsd: number|null, inputTokens: number|null, outputTokens: number|null}}
|
|
168
|
+
*/
|
|
169
|
+
function totalBudget(runs) {
|
|
170
|
+
/** @type {{costUsd: number|null, inputTokens: number|null, outputTokens: number|null}} */
|
|
171
|
+
const budget = { costUsd: null, inputTokens: null, outputTokens: null };
|
|
172
|
+
for (const run of runs) {
|
|
173
|
+
if (typeof run.costUsd === "number") budget.costUsd = (budget.costUsd ?? 0) + run.costUsd;
|
|
174
|
+
if (typeof run.inputTokens === "number") budget.inputTokens = (budget.inputTokens ?? 0) + run.inputTokens;
|
|
175
|
+
if (typeof run.outputTokens === "number") budget.outputTokens = (budget.outputTokens ?? 0) + run.outputTokens;
|
|
176
|
+
}
|
|
177
|
+
return budget;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {JournalEntry[]} entries
|
|
182
|
+
* @returns {JournalEntry[]}
|
|
183
|
+
*/
|
|
184
|
+
function sortEntries(entries) {
|
|
185
|
+
return [...entries].sort(compareEntries);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {JournalEntry} left
|
|
190
|
+
* @param {JournalEntry} right
|
|
191
|
+
* @returns {number}
|
|
192
|
+
*/
|
|
193
|
+
function compareEntries(left, right) {
|
|
194
|
+
if (left.at !== right.at) return left.at < right.at ? -1 : 1;
|
|
195
|
+
if (left.eventId !== right.eventId) return left.eventId < right.eventId ? -1 : 1;
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} campaignId
|
|
201
|
+
* @returns {string[]}
|
|
202
|
+
*/
|
|
203
|
+
function briefCommands(campaignId) {
|
|
204
|
+
return [
|
|
205
|
+
`faberun campaign show ${campaignId}`,
|
|
206
|
+
`faberun campaign sync ${campaignId} --session-id <session>`,
|
|
207
|
+
`faberun campaign ack ${campaignId} --session-id <session> --event-id <event>`,
|
|
208
|
+
`faberun campaign attach ${campaignId} --tool <tool> --session-id <session> (--transcript <path> | --no-transcript)`,
|
|
209
|
+
`faberun campaign note ${campaignId} --session-id <session> --kind <kind> --text <text>`,
|
|
210
|
+
`faberun campaign resolve ${campaignId} --session-id <session> --question-id <question> --text <text>`,
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The ceiling is enforced line by line against BRIEF_BYTES, and every section
|
|
215
|
+
// after the current one keeps a floor so a flood of run lines or questions can
|
|
216
|
+
// never starve the budget, the notes or the commands. Entry text shrinks
|
|
217
|
+
// through descending caps (like fitHandoff) until no section is lost entirely.
|
|
218
|
+
/**
|
|
219
|
+
* @param {Brief} brief
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function fitBrief(brief) {
|
|
223
|
+
for (const cap of TEXT_CAPS) {
|
|
224
|
+
const rendered = renderBudgeted(brief, cap);
|
|
225
|
+
if (rendered.criticalLost === 0) return rendered.text;
|
|
226
|
+
}
|
|
227
|
+
return renderBudgeted(brief, 0).text;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {Brief} brief
|
|
232
|
+
* @param {number} cap
|
|
233
|
+
* @returns {{text: string, criticalLost: number}}
|
|
234
|
+
*/
|
|
235
|
+
function renderBudgeted(brief, cap) {
|
|
236
|
+
const idCap = Math.max(cap, ID_CAP_FLOOR);
|
|
237
|
+
const budget = /** @type {BriefBudget} */ ({ limit: BRIEF_BYTES, used: 0, lines: [] });
|
|
238
|
+
addLine(budget, `# campaign ${boundedText(brief.campaign.id, idCap)} brief`);
|
|
239
|
+
addLine(budget, "");
|
|
240
|
+
addLine(budget, `Updated: ${brief.updatedAt}`);
|
|
241
|
+
addLine(budget, "");
|
|
242
|
+
addLine(budget, `Goal: ${boundedText(brief.campaign.goal, cap)}`);
|
|
243
|
+
addLine(budget, "");
|
|
244
|
+
addLine(budget, `Phase: ${brief.phase === null ? "unknown" : boundedText(brief.phase, cap)} · checkpoints: ${brief.checkpoints.done}/${brief.checkpoints.total}`);
|
|
245
|
+
addLine(budget, "");
|
|
246
|
+
const runs = brief.runs.slice(-RUN_LINE_LIMIT).map((run) => runLine(run, cap, idCap));
|
|
247
|
+
const questions = brief.openQuestions.slice(-QUESTION_LINE_LIMIT)
|
|
248
|
+
.map((entry) => `- [${boundedText(entry.questionId ?? "-", idCap)}] ${boundedText(entry.text ?? "", cap)}`);
|
|
249
|
+
const transitions = brief.transitions.map((entry) => `- ${transitionLine(entry, cap, idCap)}`);
|
|
250
|
+
const notes = brief.notes.map((entry) => `- ${boundedText(entry.text ?? "", cap)} · ${boundedText(entry.sessionId ?? "-", idCap)} · ${boundedText(entry.at, idCap)}`);
|
|
251
|
+
const commands = brief.commands.map((command) => `- ${command}`);
|
|
252
|
+
let criticalLost = 0;
|
|
253
|
+
criticalLost += addLineSection(budget, "Current state", [`- status: ${brief.campaign.status}`, `- linked runs: ${brief.runs.length}`], runs, "run summaries", 6);
|
|
254
|
+
criticalLost += addLineSection(budget, "Open decisions", [], questions, "open decisions", 5);
|
|
255
|
+
criticalLost += addLineSection(budget, "Recent transitions", [], transitions, "transitions", 4);
|
|
256
|
+
criticalLost += addLineSection(budget, "Budget consumed", [], [`- ${budgetLine(brief.budget)}`], "budget lines", 3);
|
|
257
|
+
criticalLost += addLineSection(budget, "Operator notes", [], notes, "operator notes", 2);
|
|
258
|
+
criticalLost += addLineSection(budget, "Commands", [], commands, "commands", 1);
|
|
259
|
+
return { text: `${budget.lines.join("\n")}\n`, criticalLost };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {BriefBudget} budget
|
|
264
|
+
* @param {string} title
|
|
265
|
+
* @param {string[]} prefix
|
|
266
|
+
* @param {string[]} lines
|
|
267
|
+
* @param {string} label
|
|
268
|
+
* @param {number} sectionsLeft
|
|
269
|
+
* @returns {0|1}
|
|
270
|
+
*/
|
|
271
|
+
function addLineSection(budget, title, prefix, lines, label, sectionsLeft) {
|
|
272
|
+
const limit = sectionLimit(sectionsLeft);
|
|
273
|
+
addLine(budget, `## ${title}`);
|
|
274
|
+
addLine(budget, "");
|
|
275
|
+
for (const line of prefix) addLine(budget, line);
|
|
276
|
+
if (!lines.length) {
|
|
277
|
+
addLine(budget, "- none");
|
|
278
|
+
addLine(budget, "");
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
const kept = fitLines(budget, lines, label, limit);
|
|
282
|
+
addLine(budget, "");
|
|
283
|
+
return kept === 0 ? 1 : 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The absolute byte ceiling for a section: every section still to come keeps
|
|
288
|
+
* its floor, so the last section is never starved by the first.
|
|
289
|
+
*
|
|
290
|
+
* @param {number} sectionsLeft
|
|
291
|
+
* @returns {number}
|
|
292
|
+
*/
|
|
293
|
+
function sectionLimit(sectionsLeft) {
|
|
294
|
+
return BRIEF_BYTES - SECTION_FLOOR_BYTES * Math.max(0, sectionsLeft - 1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Keep the latest whole lines that fit; older ones are summarized, never
|
|
299
|
+
* silently dropped. Returns the number of lines actually added.
|
|
300
|
+
*
|
|
301
|
+
* @param {BriefBudget} budget
|
|
302
|
+
* @param {string[]} lines
|
|
303
|
+
* @param {string} label
|
|
304
|
+
* @param {number} limit
|
|
305
|
+
* @returns {number}
|
|
306
|
+
*/
|
|
307
|
+
function fitLines(budget, lines, label, limit) {
|
|
308
|
+
/** @type {string[]} */
|
|
309
|
+
const kept = [];
|
|
310
|
+
let projected = budget.used;
|
|
311
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
312
|
+
const bytes = byteLength(lines[index]) + 1;
|
|
313
|
+
if (projected + bytes > limit) break;
|
|
314
|
+
projected += bytes;
|
|
315
|
+
kept.unshift(lines[index]);
|
|
316
|
+
}
|
|
317
|
+
while (kept.length) {
|
|
318
|
+
const note = `- ${lines.length - kept.length} earlier ${label} omitted`;
|
|
319
|
+
if (projected + byteLength(note) + 1 <= limit) break;
|
|
320
|
+
const removed = kept.shift();
|
|
321
|
+
if (removed === undefined) break;
|
|
322
|
+
projected -= byteLength(removed) + 1;
|
|
323
|
+
}
|
|
324
|
+
for (const line of kept) addLine(budget, line);
|
|
325
|
+
const dropped = lines.length - kept.length;
|
|
326
|
+
if (dropped > 0) addLine(budget, `- ${dropped} earlier ${label} omitted`);
|
|
327
|
+
return kept.length;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {BriefBudget} budget
|
|
332
|
+
* @param {string} line
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
function addLine(budget, line) {
|
|
336
|
+
const bytes = byteLength(line) + 1;
|
|
337
|
+
if (budget.used + bytes > budget.limit) return false;
|
|
338
|
+
budget.lines.push(line);
|
|
339
|
+
budget.used += bytes;
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} value
|
|
345
|
+
* @returns {number}
|
|
346
|
+
*/
|
|
347
|
+
function byteLength(value) {
|
|
348
|
+
return Buffer.byteLength(value, "utf8");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* A run is one line so it is kept or dropped whole, never cut between its
|
|
353
|
+
* summary and its attention states.
|
|
354
|
+
*
|
|
355
|
+
* @param {BriefRun} run
|
|
356
|
+
* @param {number} cap
|
|
357
|
+
* @param {number} idCap
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
function runLine(run, cap, idCap) {
|
|
361
|
+
const id = boundedText(run.id, idCap);
|
|
362
|
+
if (!run.exists) return `- ${id}: no status.json yet`;
|
|
363
|
+
const attention = run.attention.slice(0, ATTENTION_LINE_LIMIT).map((node) => {
|
|
364
|
+
const note = node.note ? ` ${boundedText(node.note, cap)}` : "";
|
|
365
|
+
return `${boundedText(node.id, idCap)}: ${boundedText(node.status, cap)}${note}`;
|
|
366
|
+
});
|
|
367
|
+
const hidden = run.attention.length - ATTENTION_LINE_LIMIT;
|
|
368
|
+
if (hidden > 0) attention.push(`${hidden} more`);
|
|
369
|
+
const detail = attention.length ? ` · ${attention.join("; ")}` : "";
|
|
370
|
+
return `- ${id}: ${boundedText(run.summary, cap)} · checkpoints ${run.done}/${run.total} · controller ${boundedText(run.controller, cap)}${detail}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {JournalEntry} entry
|
|
375
|
+
* @param {number} cap
|
|
376
|
+
* @param {number} idCap
|
|
377
|
+
* @returns {string}
|
|
378
|
+
*/
|
|
379
|
+
function transitionLine(entry, cap, idCap) {
|
|
380
|
+
const id = entry.decisionId ?? entry.questionId ?? entry.runId;
|
|
381
|
+
const label = id === undefined ? "" : `[${boundedText(id, idCap)}] `;
|
|
382
|
+
const text = entry.type === "session.attached"
|
|
383
|
+
? `session ${boundedText(entry.sessionId ?? "-", idCap)} (${boundedText(entry.tool ?? "-", idCap)})`
|
|
384
|
+
: boundedText(entry.text ?? "", cap);
|
|
385
|
+
return `${boundedText(entry.at, idCap)} ${entry.type} ${label}${text}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @param {{costUsd: number|null, inputTokens: number|null, outputTokens: number|null}} budget
|
|
390
|
+
* @returns {string}
|
|
391
|
+
*/
|
|
392
|
+
function budgetLine(budget) {
|
|
393
|
+
return `cost ${compactCost(budget.costUsd)} · input ${compactTokens(budget.inputTokens)} · output ${compactTokens(budget.outputTokens)} tokens`;
|
|
394
|
+
}
|