@bongos/core 1.19.870 → 1.19.872
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/.bongos-core.json +164 -94
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +10 -0
- package/docs/api/openapi.json +61 -3
- package/docs/api-reference.md +7 -1
- package/docs/architecture.md +7 -7
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +190 -189
- package/docs/copy-registry.json +264 -255
- package/docs/design/hall-direction-v2.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/README.md +4 -4
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/onboarding/primer.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/bongos-cli-release.md +4 -4
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/ops-gotchas.md +2 -2
- package/docs/recipes/search-index-ingestion.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +218 -0
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/blockers.js +138 -34
- package/modules/hall-ui/public/builders.js +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/collab.js +6 -3
- package/modules/hall-ui/public/dom-utils.js +46 -5
- package/modules/hall-ui/public/goals-page.js +7 -3
- package/modules/hall-ui/public/goals-render.js +4 -3
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/people.js +3 -3
- package/modules/hall-ui/public/profile-charts.js +5 -2
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.js +6 -6
- package/modules/hall-ui/public/settings-specialities.js +2 -2
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +70 -0
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/work.js +12 -14
- package/modules/provisioning/module.json +3 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-loop.js +170 -0
- package/scripts/gds/autobongos-run.js +317 -0
- package/scripts/gds/autobongos-verify.js +377 -0
- package/scripts/gds/autonomy-gauge-statusline.js +124 -0
- package/scripts/gds/autonomy-gauge.js +84 -0
- package/scripts/gds/run-unit-tests.js +120 -2
- package/scripts/gds/sequence.js +3 -0
- package/scripts/gds/start.js +16 -10
- package/src/bongos/core-update.js +147 -0
- package/src/bongos/routes/core-update.js +49 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +3 -1
- package/tests/autobongos_loop.mjs +446 -0
- package/tests/autobongos_verify.mjs +325 -0
- package/tests/autonomy_gauge.mjs +344 -0
- package/tests/blockers_hall.mjs +20 -6
- package/tests/blockers_kit_pager.mjs +260 -0
- package/tests/blockers_page_ui.mjs +5 -4
- package/tests/box_ship_permissions.mjs +30 -0
- package/tests/core_update_banner.mjs +308 -0
- package/tests/eviction_policy.mjs +22 -1
- package/tests/git_guard.mjs +68 -0
- package/tests/goal_criterion_confirm_ui.mjs +7 -0
- package/tests/goal_invite_consent.mjs +7 -0
- package/tests/goal_sort_staleness.mjs +6 -2
- package/tests/goal_subtitle.mjs +48 -4
- package/tests/hall_board_world.mjs +4 -1
- package/tests/hall_builders_page_pass.mjs +7 -0
- package/tests/hall_landing_boot.mjs +7 -0
- package/tests/hall_profile_world.mjs +20 -2
- package/tests/hall_record_world.mjs +7 -0
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +7 -0
- package/tests/hook_shell_tools.mjs +23 -1
- package/tests/idea_detail_page.mjs +7 -0
- package/tests/idea_spark_hall.mjs +7 -0
- package/tests/integration_parked_ratchet.mjs +79 -0
- package/tests/module_contributions.mjs +134 -8
- package/tests/people_directory_clip.mjs +7 -0
- package/tests/profile_activity.mjs +7 -0
- package/tests/profile_ui_cross_project.mjs +9 -1
- package/tests/provision_disconnect.mjs +7 -0
- package/tests/provision_restart.mjs +7 -0
- package/tests/provision_settings_apply.mjs +7 -0
- package/tests/sessions_kit_pager.mjs +7 -0
- package/tests/watch_applications_queue.mjs +7 -0
- package/tests/watch_roster_self_row.mjs +7 -0
- package/tests/watch_sealed_floor.mjs +7 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// scripts/gds/autobongos-run.js — the Autobongos supervisor's I/O half
|
|
5
|
+
// (task 1003902; ADR 0307 §1). One iteration is: gauge → pick → claim → spawn
|
|
6
|
+
// one worker → record. No model anywhere in this file; every branch it takes is
|
|
7
|
+
// a branch a person can read.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/gds/autobongos-run.js --goal 1000119 # one task
|
|
10
|
+
// node scripts/gds/autobongos-run.js --goal 1000119 --max-tasks 5
|
|
11
|
+
// node scripts/gds/autobongos-run.js --goal 1000119 --dry-run # pick only, claim nothing
|
|
12
|
+
//
|
|
13
|
+
// SCOPE, and what deliberately is NOT here:
|
|
14
|
+
// · the ALLOWLIST is `--goal` / AUTOBONGOS_GOALS for now. Task 1003908 moves it
|
|
15
|
+
// into the database where the machine cannot edit it, and adds the kill
|
|
16
|
+
// switch. Until then this refuses to run with no goal named, rather than
|
|
17
|
+
// defaulting to "anything claimable".
|
|
18
|
+
// · CONFIRMING a ship against the ledger is autobongos-verify.js (task
|
|
19
|
+
// 1003903), called at step 6. A worker's `shipped` is a CLAIM — recorded here
|
|
20
|
+
// as `claims_shipped` so nothing can mistake it for a fact — and the ledger
|
|
21
|
+
// read is what turns it into one, or into a released claim and a blocker.
|
|
22
|
+
// · BACKOFF and never-terminally-stopping is task 1003904. This exits after
|
|
23
|
+
// its budget; it does not yet decide when to come back.
|
|
24
|
+
|
|
25
|
+
const { execFile, spawn } = require('node:child_process');
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const crypto = require('node:crypto');
|
|
29
|
+
|
|
30
|
+
const seq = require('./sequence.js');
|
|
31
|
+
const gauge = require('./autonomy-gauge.js');
|
|
32
|
+
const {
|
|
33
|
+
buildWorkerArgs, buildWorkerPrompt, classifyRun, DEFAULT_MODEL, DEFAULT_WORKER_TIMEOUT_MS,
|
|
34
|
+
} = require('./autobongos-loop.js');
|
|
35
|
+
const { verifyShip } = require('./autobongos-verify.js');
|
|
36
|
+
|
|
37
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
38
|
+
|
|
39
|
+
function readArgv(argv) {
|
|
40
|
+
const out = { goals: [], maxTasks: 1, dryRun: false, json: false, model: DEFAULT_MODEL, timeoutMs: DEFAULT_WORKER_TIMEOUT_MS };
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
if (a === '--goal') { out.goals.push(Number(argv[++i])); continue; }
|
|
44
|
+
if (a.startsWith('--goal=')) { out.goals.push(Number(a.slice(7))); continue; }
|
|
45
|
+
if (a === '--max-tasks') { out.maxTasks = Number(argv[++i]); continue; }
|
|
46
|
+
if (a === '--model') { out.model = argv[++i]; continue; }
|
|
47
|
+
if (a === '--timeout-min') { out.timeoutMs = Number(argv[++i]) * 60_000; continue; }
|
|
48
|
+
if (a === '--dry-run') { out.dryRun = true; continue; }
|
|
49
|
+
if (a === '--json') { out.json = true; continue; }
|
|
50
|
+
}
|
|
51
|
+
if (!out.goals.length && process.env.AUTOBONGOS_GOALS) {
|
|
52
|
+
out.goals = String(process.env.AUTOBONGOS_GOALS).split(',').map((g) => Number(g.trim())).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const run = (file, args, opts = {}) => new Promise((resolve) => {
|
|
58
|
+
execFile(file, args, { cwd: REPO_ROOT, maxBuffer: 32 * 1024 * 1024, ...opts },
|
|
59
|
+
(err, stdout, stderr) => resolve({ ok: !err, code: err ? (err.code ?? 1) : 0, stdout: stdout || '', stderr: stderr || '' }));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// The run log is the supervisor's own record. It is NOT the ledger — Bongos is —
|
|
63
|
+
// but a machine nobody is watching must be able to say afterwards what it did,
|
|
64
|
+
// and an append-only JSONL survives a kill -9 mid-iteration.
|
|
65
|
+
function logPath() {
|
|
66
|
+
if (process.env.AUTOBONGOS_LOG_FILE) return process.env.AUTOBONGOS_LOG_FILE;
|
|
67
|
+
try { return require('../../src/instance-config.js').configPath('autobongos-runs.jsonl'); }
|
|
68
|
+
catch (_) { return path.join(REPO_ROOT, 'autobongos-runs.jsonl'); }
|
|
69
|
+
}
|
|
70
|
+
function record(entry) {
|
|
71
|
+
const row = { at: new Date().toISOString(), ...entry };
|
|
72
|
+
try {
|
|
73
|
+
const p = logPath();
|
|
74
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
75
|
+
fs.appendFileSync(p, `${JSON.stringify(row)}\n`);
|
|
76
|
+
} catch (_) { /* a runner that cannot log must still run */ }
|
|
77
|
+
return row;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// pickTask — the allowlisted goals, in order, through sequence.js's halt matrix.
|
|
81
|
+
// The matrix is reused rather than re-derived: it already refuses thin specs,
|
|
82
|
+
// protected paths, judgement kinds and unmet dependencies, from facts on the row.
|
|
83
|
+
// The assembly below mirrors sequence.js's own --goal --next path step for step,
|
|
84
|
+
// so the runner and a person running /builder-sequence cannot disagree about
|
|
85
|
+
// which task is next.
|
|
86
|
+
//
|
|
87
|
+
// `claimableIds` is fetched because the matrix's `not_claimable` reason defers to
|
|
88
|
+
// the server's own feed rather than re-deriving its filter here. A null (feed
|
|
89
|
+
// unreadable) makes that one reason stay QUIET rather than halt everything —
|
|
90
|
+
// ADR 0153 records the bug where the opposite turned an empty read into a
|
|
91
|
+
// universal halt.
|
|
92
|
+
async function pickTask(goals, deps = {}) {
|
|
93
|
+
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
94
|
+
let rank = deps.rank;
|
|
95
|
+
if (rank === undefined) {
|
|
96
|
+
const me = await api.me.getMe();
|
|
97
|
+
rank = me.ok ? me.data.builder?.rank : null;
|
|
98
|
+
}
|
|
99
|
+
const claimableIds = await seq.fetchClaimableIds(api);
|
|
100
|
+
|
|
101
|
+
for (const goalId of goals) {
|
|
102
|
+
let byId;
|
|
103
|
+
try { byId = await seq.fetchGoalTasks(api, goalId, seq.GOAL_PAGE); }
|
|
104
|
+
catch (e) { return { error: `goal ${goalId}: ${e.message}` }; }
|
|
105
|
+
|
|
106
|
+
const candidates = Array.from(byId.values()).filter((t) => !seq.TERMINAL.has(t.status));
|
|
107
|
+
const { order } = seq.topoOrder(candidates);
|
|
108
|
+
const { next, skipped } = seq.pickNext(order, { rank, claimableIds });
|
|
109
|
+
if (next) return { task: next, goalId, skipped };
|
|
110
|
+
}
|
|
111
|
+
return { none: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// WORKER_ENV_ALLOW — what a worker is allowed to inherit (grader finding, task
|
|
115
|
+
// 1003902). Spreading `process.env` handed a bypassPermissions session every
|
|
116
|
+
// credential the supervisor happened to hold — DB URLs, npm and git tokens — to a
|
|
117
|
+
// process whose only injection defence is a prompt fence. The fence is good and
|
|
118
|
+
// it is not a technical barrier, so the blast radius of a jailbreak has to be
|
|
119
|
+
// bounded by something that is.
|
|
120
|
+
//
|
|
121
|
+
// The worker needs: to run node and claude (PATH/HOME/SHELL and the platform's
|
|
122
|
+
// own required vars), to reach the instance (BONGOS_API_BASE), and to be told it
|
|
123
|
+
// is a subagent. It does NOT need a git push credential — the road to main is the
|
|
124
|
+
// server-mediated pipeline (ADR 0307 §6) — so none is passed.
|
|
125
|
+
const WORKER_ENV_ALLOW = [
|
|
126
|
+
'PATH', 'HOME', 'SHELL', 'LANG', 'LC_ALL', 'TZ', 'TMPDIR', 'USER', 'LOGNAME',
|
|
127
|
+
'SystemRoot', 'windir', 'COMSPEC', 'PATHEXT', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP',
|
|
128
|
+
'BONGOS_API_BASE', 'CLOUDBONGOS_API_BASE',
|
|
129
|
+
'OTB_CLAUDE_BIN', 'CLAUDE_BIN',
|
|
130
|
+
'XDG_CONFIG_HOME',
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
function workerEnv(source = process.env, extra = {}) {
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const k of WORKER_ENV_ALLOW) if (source[k] !== undefined) out[k] = source[k];
|
|
136
|
+
// An operator can widen this deliberately and visibly, never by accident.
|
|
137
|
+
const widen = String(source.AUTOBONGOS_WORKER_ENV_EXTRA || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
138
|
+
for (const k of widen) if (source[k] !== undefined) out[k] = source[k];
|
|
139
|
+
return { ...out, OTB_SUBAGENT: '1', ...extra };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const MAX_WORKER_OUTPUT = 8 * 1024 * 1024; // a misbehaving worker must not grow memory until the timeout
|
|
143
|
+
|
|
144
|
+
// spawnWorker — one headless session, bounded by WALL CLOCK, in the task's own
|
|
145
|
+
// working tree.
|
|
146
|
+
async function spawnWorker({ task, model, timeoutMs, cwd, deps = {} }) {
|
|
147
|
+
const spawnFn = deps.spawn || spawn;
|
|
148
|
+
const sessionId = deps.sessionId || crypto.randomUUID(); // pinned, so the session can be resumed
|
|
149
|
+
const bin = process.env.OTB_CLAUDE_BIN || process.env.CLAUDE_BIN || 'claude';
|
|
150
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
151
|
+
const args = buildWorkerArgs({ sessionId, model, isRoot });
|
|
152
|
+
const prompt = buildWorkerPrompt({ task });
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
let child;
|
|
156
|
+
try {
|
|
157
|
+
child = spawnFn(bin, args, {
|
|
158
|
+
cwd,
|
|
159
|
+
// stdin is a PIPE we write the prompt into and close. Never inherit: a
|
|
160
|
+
// `claude -p` with an open inherited stdin waits seconds for input that
|
|
161
|
+
// never comes, on every single spawn.
|
|
162
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
|
+
shell: process.platform === 'win32', // `claude` is a .cmd shim on Windows
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
env: workerEnv(),
|
|
166
|
+
});
|
|
167
|
+
} catch (e) { return resolve({ spawnError: e.message }); }
|
|
168
|
+
|
|
169
|
+
let stdout = ''; let stderr = ''; let timedOut = false; let truncated = false;
|
|
170
|
+
const cap = (cur, d) => {
|
|
171
|
+
if (cur.length >= MAX_WORKER_OUTPUT) { truncated = true; return cur; }
|
|
172
|
+
return cur + String(d).slice(0, MAX_WORKER_OUTPUT - cur.length);
|
|
173
|
+
};
|
|
174
|
+
const timer = setTimeout(() => { timedOut = true; try { child.kill('SIGKILL'); } catch (_) {} }, timeoutMs);
|
|
175
|
+
child.stdout.on('data', (d) => { stdout = cap(stdout, d); });
|
|
176
|
+
child.stderr.on('data', (d) => { stderr = cap(stderr, d); });
|
|
177
|
+
child.on('error', (e) => { clearTimeout(timer); resolve({ spawnError: e.message, sessionId }); });
|
|
178
|
+
child.on('close', (code) => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
resolve({ envelope: stdout, exitCode: code, timedOut, truncated, sessionId, stderr: stderr.slice(-2000) });
|
|
181
|
+
});
|
|
182
|
+
child.stdin.end(prompt);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// worktreeFor — the task gets its OWN linked working tree, and this is not a
|
|
187
|
+
// nicety (grader finding, task 1003902).
|
|
188
|
+
//
|
|
189
|
+
// worktree-per-claim is an ENFORCED project default: worktree-claim-guard.js
|
|
190
|
+
// refuses any claim made from the main checkout, with no escape but `--here`.
|
|
191
|
+
// The natural shape for a long-lived unattended supervisor is one persistent
|
|
192
|
+
// checkout — which is the main checkout — so the first cut of this loop would
|
|
193
|
+
// have taken `claim_failed` on every single iteration and never done any work.
|
|
194
|
+
// Reaching for `--here` would "fix" that by defeating the guard: the worker's
|
|
195
|
+
// branch would land in the supervisor's own tree, which is precisely the strand
|
|
196
|
+
// the guard exists to prevent.
|
|
197
|
+
//
|
|
198
|
+
// The name carries the task id plus a random suffix. Unique by construction, so
|
|
199
|
+
// it cannot collide with another active claim's tree — a collision makes the
|
|
200
|
+
// server store a disambiguated name the ship lock can never match (task 1004031).
|
|
201
|
+
function worktreeName(taskId) {
|
|
202
|
+
return `autobongos-${taskId}-${crypto.randomBytes(3).toString('hex')}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function iteration(opts, deps = {}) {
|
|
206
|
+
const runCmd = deps.run || run;
|
|
207
|
+
const spawnWork = deps.spawnWorker || spawnWorker;
|
|
208
|
+
const readGauge = deps.gauge || (() => gauge.evaluate());
|
|
209
|
+
const pick = deps.pickTask || pickTask;
|
|
210
|
+
const log = deps.record || record;
|
|
211
|
+
|
|
212
|
+
// 1. The gauge. Never start a task we cannot pay for out of the window.
|
|
213
|
+
const g = readGauge();
|
|
214
|
+
if (!g.decision.go) {
|
|
215
|
+
return log({ event: 'hold', go: false, unknown: !!g.decision.unknown, reason: g.decision.reason, sleep_until: g.decision.sleepUntil ?? null });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. Pick.
|
|
219
|
+
const picked = await pick(opts.goals, deps.pickDeps || {});
|
|
220
|
+
if (picked.error) return log({ event: 'pick_failed', reason: picked.error });
|
|
221
|
+
if (picked.none) return log({ event: 'nothing_claimable', goals: opts.goals });
|
|
222
|
+
const task = picked.task;
|
|
223
|
+
if (opts.dryRun) return log({ event: 'dry_run', task_id: task.id, title: task.title, goal_id: picked.goalId });
|
|
224
|
+
|
|
225
|
+
// 3. The task's own working tree.
|
|
226
|
+
const wtName = deps.worktreeName ? deps.worktreeName(task.id) : worktreeName(task.id);
|
|
227
|
+
const made = await runCmd(process.execPath, [path.join('scripts', 'gds', 'worktree.js'), 'add', wtName, '--base', 'origin/main']);
|
|
228
|
+
if (!made.ok) {
|
|
229
|
+
return log({ event: 'worktree_failed', task_id: task.id, worktree: wtName, reason: (made.stderr || made.stdout).slice(-400) });
|
|
230
|
+
}
|
|
231
|
+
const wtPath = path.join(REPO_ROOT, '.claude', 'worktrees', wtName);
|
|
232
|
+
|
|
233
|
+
// 4. Claim FROM that tree, so claim.js records it as the claim's tree and the
|
|
234
|
+
// ship lock later agrees with where the work actually is.
|
|
235
|
+
const claimed = await runCmd(process.execPath, [path.join(REPO_ROOT, 'scripts', 'gds', 'claim.js'), String(task.id)], { cwd: wtPath });
|
|
236
|
+
if (!claimed.ok) {
|
|
237
|
+
await runCmd(process.execPath, [path.join('scripts', 'gds', 'worktree.js'), 'remove', wtName]);
|
|
238
|
+
return log({ event: 'claim_failed', task_id: task.id, worktree: wtName, reason: (claimed.stderr || claimed.stdout).slice(-400) });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 5. Work, in that tree.
|
|
242
|
+
const started = Date.now();
|
|
243
|
+
const res = await spawnWork({ task, model: opts.model, timeoutMs: opts.timeoutMs, cwd: wtPath, deps });
|
|
244
|
+
const verdict = classifyRun(res);
|
|
245
|
+
|
|
246
|
+
// 6. VERIFY FROM THE LEDGER (task 1003903). Read the task back from Bongos and
|
|
247
|
+
// let IT say what happened — for every outcome, not only a claimed ship. The
|
|
248
|
+
// worker's own verdict becomes context in the blocker body, never the branch: a
|
|
249
|
+
// worker that stopped dead needing a decision and one that finished the job
|
|
250
|
+
// return structurally identical envelopes, and the ledger is the only thing
|
|
251
|
+
// that can tell them apart.
|
|
252
|
+
//
|
|
253
|
+
// The release decision moved HERE from the worker's verdict, and that move is
|
|
254
|
+
// the point of the step rather than a tidy-up. `still_held` is a fact about the
|
|
255
|
+
// ledger; `worker_failed` is a report from the thing being checked. The TREE is
|
|
256
|
+
// left alone either way — it may hold committed work, and the land/ship
|
|
257
|
+
// pipeline owns worktree cleanup.
|
|
258
|
+
const ledger = await verifyShip(
|
|
259
|
+
{ taskId: task.id, taskTitle: task.title, worker: { outcome: verdict.outcome, reason: verdict.reason } },
|
|
260
|
+
{
|
|
261
|
+
api: deps.api,
|
|
262
|
+
cwd: wtPath,
|
|
263
|
+
release: deps.release || ((id, reason) => runCmd(process.execPath,
|
|
264
|
+
[path.join(REPO_ROOT, 'scripts', 'gds', 'release.js'), String(id), '--reason', reason], { cwd: wtPath })),
|
|
265
|
+
...(deps.verifyDeps || {}),
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const row = {
|
|
270
|
+
event: 'worked', task_id: task.id, title: task.title, goal_id: picked.goalId,
|
|
271
|
+
worktree: wtName,
|
|
272
|
+
outcome: verdict.outcome, reason: verdict.reason,
|
|
273
|
+
session_id: res.sessionId || null, duration_s: Math.round((Date.now() - started) / 1000),
|
|
274
|
+
cost_usd: verdict.costUsd ?? null,
|
|
275
|
+
output_truncated: !!res.truncated,
|
|
276
|
+
undetermined_decisions: verdict.verdict ? verdict.verdict.undetermined_decisions : null,
|
|
277
|
+
// What the LEDGER says, kept separate from what the worker said, so the run
|
|
278
|
+
// log can be read afterwards without having to trust either one alone.
|
|
279
|
+
ledger_status: ledger.status, ledger_shape: ledger.shape, verified: !!ledger.verified,
|
|
280
|
+
artifact_on_main: ledger.artifact && ledger.artifact.checked ? !!ledger.artifact.onMain : null,
|
|
281
|
+
claim_released: !!ledger.released,
|
|
282
|
+
blocker_id: ledger.blocker && ledger.blocker.blockerId ? ledger.blocker.blockerId : null,
|
|
283
|
+
task_parked: !!(ledger.blocker && ledger.blocker.parked),
|
|
284
|
+
};
|
|
285
|
+
// An unreadable ledger stops the run. Picking another task while unable to see
|
|
286
|
+
// what happened to the last one is how a machine nobody is watching digs.
|
|
287
|
+
if (ledger.halt) row.event = 'verify_halt';
|
|
288
|
+
return log(row);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function main(argv) {
|
|
292
|
+
const opts = readArgv(argv);
|
|
293
|
+
if (!opts.goals.length) {
|
|
294
|
+
console.error('autobongos-run: name at least one allowlisted goal (--goal N, or AUTOBONGOS_GOALS).');
|
|
295
|
+
console.error(' Refusing to run against "anything claimable" — the allowlist is half of what makes this safe to leave alone.');
|
|
296
|
+
return 2;
|
|
297
|
+
}
|
|
298
|
+
for (let i = 0; i < opts.maxTasks; i++) {
|
|
299
|
+
const row = await iteration(opts);
|
|
300
|
+
if (opts.json) console.log(JSON.stringify(row));
|
|
301
|
+
else console.log(`${row.event}${row.task_id ? ` task ${row.task_id}` : ''}${row.outcome ? ` → ${row.outcome}` : ''}${row.reason ? `: ${row.reason}` : ''}`);
|
|
302
|
+
// Anything that is not a completed piece of work ends this run — including
|
|
303
|
+
// `verify_halt`, which is the ledger being unreadable. Deciding when to come
|
|
304
|
+
// back is task 1003904's job, not this one's.
|
|
305
|
+
if (row.event !== 'worked') break;
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (require.main === module) {
|
|
311
|
+
main(process.argv.slice(2)).then((c) => process.exit(c)).catch((e) => {
|
|
312
|
+
console.error(`autobongos-run: ${e && e.message ? e.message : e}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = { readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, worktreeName };
|