@bongos/core 1.19.874 → 1.19.875
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 +49 -29
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +18 -0
- package/docs/api/openapi.json +218 -3
- package/docs/api-reference.md +6 -2
- package/docs/copy-inventory.md +35 -26
- package/docs/copy-registry.json +108 -27
- package/docs/module-api-changelog.md +2 -0
- package/modules/autonomy/db.js +74 -0
- package/modules/autonomy/fence.js +172 -0
- package/modules/autonomy/migrations/autonomy_001_autobongos_fence.sql +71 -0
- package/modules/autonomy/routes/autonomy.js +91 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +8 -0
- package/modules/government/catalog.js +11 -0
- package/modules/government/migrations/government_016_autonomy_fence_manage.sql +37 -0
- package/modules/hall-ui/public/gate.html +14 -1
- package/modules/hall-ui/public/gate.js +117 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-run.js +90 -15
- package/src/module-api.js +1 -1
- package/tests/autobongos_fence.mjs +262 -0
- package/tests/autobongos_loop.mjs +120 -1
|
@@ -253,7 +253,15 @@ test('a goal whose tasks cannot be read is reported, not silently skipped', asyn
|
|
|
253
253
|
// running. These pin the shape that fixes it.
|
|
254
254
|
|
|
255
255
|
const goodGauge = () => ({ decision: { go: true, unknown: false, sleepUntil: null, reason: 'headroom' } });
|
|
256
|
-
const aTask = { id: 4242, title: 'Do the thing', kind: 'feature', description: 'd'.repeat(300) };
|
|
256
|
+
const aTask = { id: 4242, title: 'Do the thing', kind: 'feature', description: 'd'.repeat(300), goal_id: 1000119, touches: [] };
|
|
257
|
+
// The FENCE, faked (task 1003908). Step 0 of every iteration now reads the kill
|
|
258
|
+
// switch, the allowlist and the grader's availability from the server; a test that
|
|
259
|
+
// does not say what the fence holds is not describing a run. The fence's own
|
|
260
|
+
// behaviour is tests/autobongos_fence.mjs — here it is a precondition.
|
|
261
|
+
const openFence = () => ({
|
|
262
|
+
raw: { enabled: true, paused_reason: null, goals: [{ goal_id: 1000119 }] },
|
|
263
|
+
graderBypassed: false,
|
|
264
|
+
});
|
|
257
265
|
|
|
258
266
|
function harness(over = {}) {
|
|
259
267
|
const calls = [];
|
|
@@ -261,6 +269,7 @@ function harness(over = {}) {
|
|
|
261
269
|
const blockers = [];
|
|
262
270
|
const deps = {
|
|
263
271
|
gauge: over.gauge || goodGauge,
|
|
272
|
+
readFence: over.readFence || (async () => openFence()),
|
|
264
273
|
// The LEDGER, faked. iteration() now reads the task back from Bongos after
|
|
265
274
|
// every worker (task 1003903) instead of believing the worker's own verdict,
|
|
266
275
|
// so a test that does not say what the ledger holds is not describing a run.
|
|
@@ -288,6 +297,56 @@ function harness(over = {}) {
|
|
|
288
297
|
}
|
|
289
298
|
const opts = { goals: [1000119], maxTasks: 1, dryRun: false, model: 'm', timeoutMs: 1000 };
|
|
290
299
|
|
|
300
|
+
test('the fence is read BEFORE the gauge, and a closed one claims nothing', async () => {
|
|
301
|
+
// Order matters: three of the fence's four gates make every task refusable, so
|
|
302
|
+
// paying for a capacity read first would be wasted — and a fence checked after
|
|
303
|
+
// something is claimed is not a fence.
|
|
304
|
+
let gaugeRead = false;
|
|
305
|
+
const h = harness({
|
|
306
|
+
gauge: () => { gaugeRead = true; return goodGauge(); },
|
|
307
|
+
readFence: async () => ({ raw: { enabled: false, paused_reason: 'owner stopped it' }, graderBypassed: false }),
|
|
308
|
+
});
|
|
309
|
+
const row = await runner.iteration(opts, h.deps);
|
|
310
|
+
assert.equal(row.event, 'fenced');
|
|
311
|
+
assert.equal(row.code, 'kill_switch');
|
|
312
|
+
assert.match(row.reason, /owner stopped it/);
|
|
313
|
+
assert.equal(gaugeRead, false, 'the gauge must not be consulted for a run the fence already refused');
|
|
314
|
+
assert.deepEqual(h.calls, [], 'nothing may be created or claimed behind a closed fence');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('a bypassed grader stops the runner rather than shipping unreviewed', async () => {
|
|
318
|
+
const h = harness({ readFence: async () => ({ ...openFence(), graderBypassed: true }) });
|
|
319
|
+
const row = await runner.iteration(opts, h.deps);
|
|
320
|
+
assert.equal(row.event, 'fenced');
|
|
321
|
+
assert.equal(row.code, 'grader_unavailable');
|
|
322
|
+
assert.deepEqual(h.calls, []);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('the loop picks from the ALLOWLIST, not from what the command line asked for', async () => {
|
|
326
|
+
let sawGoals = null;
|
|
327
|
+
const h = harness({ pickTask: async (goals) => { sawGoals = goals; return { task: aTask, goalId: 1000119, skipped: [] }; } });
|
|
328
|
+
// The operator asks for an allowlisted goal AND one that is not on the list.
|
|
329
|
+
await runner.iteration({ ...opts, goals: [1000119, 9999999] }, h.deps);
|
|
330
|
+
assert.deepEqual(sawGoals, [1000119], 'a goal the database does not allow must never reach the picker');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('a picked task in a non-allowlisted goal is refused before it is claimed', async () => {
|
|
334
|
+
const h = harness({ pickTask: async () => ({ task: { ...aTask, goal_id: 9999999 }, goalId: 9999999, skipped: [] }) });
|
|
335
|
+
const row = await runner.iteration(opts, h.deps);
|
|
336
|
+
assert.equal(row.event, 'fenced');
|
|
337
|
+
assert.equal(row.code, 'goal_not_allowlisted');
|
|
338
|
+
assert.deepEqual(h.calls, [], 'no worktree, no claim, no worker');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('a picked task touching a protected path is refused before it is claimed', async () => {
|
|
342
|
+
const h = harness({ pickTask: async () => ({ task: { ...aTask, touches: ['migrations/999_x.sql', 'docs/canonical-permissions.md'] }, goalId: 1000119, skipped: [] }) });
|
|
343
|
+
const row = await runner.iteration(opts, h.deps);
|
|
344
|
+
assert.equal(row.event, 'fenced');
|
|
345
|
+
assert.equal(row.code, 'protected_path');
|
|
346
|
+
assert.ok(row.globs.length, 'the refusal must name the surface it hit');
|
|
347
|
+
assert.deepEqual(h.calls, []);
|
|
348
|
+
});
|
|
349
|
+
|
|
291
350
|
test('an unknown gauge holds before anything is claimed', async () => {
|
|
292
351
|
const h = harness({ gauge: () => ({ decision: { go: false, unknown: true, reason: 'no gauge file', sleepUntil: null } }) });
|
|
293
352
|
const row = await runner.iteration(opts, h.deps);
|
|
@@ -444,3 +503,63 @@ test('each task gets a worktree name that cannot collide with another claim', ()
|
|
|
444
503
|
assert.notEqual(a, b, 'a collision makes the server store a name the ship lock can never match (task 1004031)');
|
|
445
504
|
});
|
|
446
505
|
|
|
506
|
+
|
|
507
|
+
// ── the production call shape ───────────────────────────────────────────────────
|
|
508
|
+
//
|
|
509
|
+
// THE GAP THIS CLOSES, and it is the point of the test rather than a footnote.
|
|
510
|
+
// `harness()` always injects deps.run, so `const runCmd = deps.run || run` at the
|
|
511
|
+
// top of iteration() short-circuits and never evaluates the module-level `run`
|
|
512
|
+
// helper. A later `const run = …` in the same function shadowed that helper
|
|
513
|
+
// through the whole body via its Temporal Dead Zone, so every real invocation —
|
|
514
|
+
// main() calls iteration(opts) with NO deps — threw `ReferenceError: Cannot
|
|
515
|
+
// access 'run' before initialization` on iteration's first meaningful line, while
|
|
516
|
+
// the entire suite stayed green. Shipped code that no test ever calls the way
|
|
517
|
+
// production calls it is untested code, whatever the count says.
|
|
518
|
+
//
|
|
519
|
+
// These drive iteration() with the MINIMUM injection: the two reads that would
|
|
520
|
+
// otherwise touch the network, and nothing else. Anything that resolves from the
|
|
521
|
+
// module scope must resolve for real.
|
|
522
|
+
|
|
523
|
+
test('iteration() runs with no injected run/spawn — the shape main() actually calls', async () => {
|
|
524
|
+
const rows = [];
|
|
525
|
+
const row = await runner.iteration(opts, {
|
|
526
|
+
readFence: async () => ({ raw: { enabled: false, paused_reason: 'off for the test' }, graderBypassed: false }),
|
|
527
|
+
record: (r) => { rows.push(r); return r; },
|
|
528
|
+
});
|
|
529
|
+
assert.equal(row.event, 'fenced');
|
|
530
|
+
assert.equal(row.code, 'kill_switch');
|
|
531
|
+
assert.equal(rows.length, 1);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('the fenced paths reach their decision with nothing injected but the reads', async () => {
|
|
535
|
+
// Every refusal that can be reached before the gauge, driven through the real
|
|
536
|
+
// module-scope bindings. A regression in ANY of them surfaces here as a throw
|
|
537
|
+
// rather than as a passing test against a fully-mocked function.
|
|
538
|
+
const cases = [
|
|
539
|
+
[{ raw: { enabled: true, goals: [{ goal_id: 1000119 }] }, graderBypassed: true }, 'grader_unavailable'],
|
|
540
|
+
// `opts` asks for goal 1000119, so an EMPTY allowlist refuses it by name.
|
|
541
|
+
[{ raw: { enabled: true, goals: [] }, graderBypassed: false }, 'goal_not_allowlisted'],
|
|
542
|
+
[{ raw: null, graderBypassed: false }, 'fence_unreadable'],
|
|
543
|
+
[{ raw: { enabled: false }, graderBypassed: false }, 'kill_switch'],
|
|
544
|
+
];
|
|
545
|
+
for (const [fenceRead, code] of cases) {
|
|
546
|
+
const row = await runner.iteration(opts, { readFence: async () => fenceRead, record: (r) => r });
|
|
547
|
+
assert.equal(row.event, 'fenced', code);
|
|
548
|
+
assert.equal(row.code, code);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('the module-level exec helper is reachable from inside iteration()', async () => {
|
|
553
|
+
// The narrow assertion the bug would have failed: get PAST the fence, so
|
|
554
|
+
// `deps.run || run` has to resolve the module-level helper for real, and stop at
|
|
555
|
+
// the next injected seam. A name that shadows `run` reds this immediately.
|
|
556
|
+
let picked = false;
|
|
557
|
+
const row = await runner.iteration(opts, {
|
|
558
|
+
readFence: async () => openFence(),
|
|
559
|
+
gauge: goodGauge,
|
|
560
|
+
pickTask: async () => { picked = true; return { none: true }; },
|
|
561
|
+
record: (r) => r,
|
|
562
|
+
});
|
|
563
|
+
assert.equal(picked, true, 'iteration must reach the picker without touching an un-injected binding');
|
|
564
|
+
assert.equal(row.event, 'nothing_claimable');
|
|
565
|
+
});
|