@bongos/core 1.19.36 → 1.19.37

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.36",
6
- "core_contract": "1.19.36",
7
- "source_commit": "f97b75c015a281b9492699354142dadb85b5458c",
5
+ "core_version": "1.19.37",
6
+ "core_contract": "1.19.37",
7
+ "source_commit": "606cc770b09032c6ab385e1326f79b08c19e0083",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-11T15:56:45.212Z",
9
+ "built_at": "2026-08-11T16:06:38.933Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 287,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1210,
14
+ "functional_verbatim": 1211,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1518,
20
- "tree_sha256": "59a933fb3e3a4fdc442f52821778e28dc3c5ca384160bcdeb6639512f861a0fc",
19
+ "file_count": 1519,
20
+ "tree_sha256": "faa022ab6fb5e04c311cd4d24d2991b07d15fb5fa565dffa0862d87fdf1e546d",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -3367,7 +3367,7 @@
3367
3367
  {
3368
3368
  "path": "modules/lifecycle/db.js",
3369
3369
  "mode": "0000644",
3370
- "sha256": "77528182c408edf2b8f3e07382bb83f97a7d668b979e97e60e4ff0c2e1bc7109"
3370
+ "sha256": "c7dfc49df888857c8975077a069e8730d16926d8e92776f09489f69a40068f31"
3371
3371
  },
3372
3372
  {
3373
3373
  "path": "modules/lifecycle/done-when.js",
@@ -3442,7 +3442,7 @@
3442
3442
  {
3443
3443
  "path": "modules/lifecycle/routes/claims.js",
3444
3444
  "mode": "0000644",
3445
- "sha256": "6e30cf7169736737ea7aeac0aa1af9e5f31ed8ea79f08796cc2da9ed9ae03577"
3445
+ "sha256": "92065e0a5cfd2a5d76bbf04f4efafe0e2b3bc31cf616136963d4c07fad25047e"
3446
3446
  },
3447
3447
  {
3448
3448
  "path": "modules/lifecycle/routes/dependencies.js",
@@ -4017,12 +4017,12 @@
4017
4017
  {
4018
4018
  "path": "package-lock.json",
4019
4019
  "mode": "0000644",
4020
- "sha256": "ba0c267dd729a028da03734f1aa527c2fcae016932cdbaea2d1650bc184ef8e4"
4020
+ "sha256": "acab5dbbe8ab00da496f534457882de0c115d441e9f0e38fae6f5a901a4b3f47"
4021
4021
  },
4022
4022
  {
4023
4023
  "path": "package.json",
4024
4024
  "mode": "0000644",
4025
- "sha256": "30c2010f68e93023955954526ed962a366d4150e5b9a07a78ff0b1ac04f7b27c"
4025
+ "sha256": "c33f5baf3eddd6b7bdcf80af532dc04d290e394b5c51b90931aa52d0ead94e6a"
4026
4026
  },
4027
4027
  {
4028
4028
  "path": "public-docs/index.html",
@@ -4547,7 +4547,7 @@
4547
4547
  {
4548
4548
  "path": "scripts/gds/claim.js",
4549
4549
  "mode": "0000644",
4550
- "sha256": "23f9932dd79c63915679eeb175e2fc6d95e43b840dba78336c23504c85617987"
4550
+ "sha256": "6f09d9344f57039c07d2de943dabb48dbf717226aa2667f4163dab47fb74428a"
4551
4551
  },
4552
4552
  {
4553
4553
  "path": "scripts/gds/classify-discipline.js",
@@ -5057,7 +5057,7 @@
5057
5057
  {
5058
5058
  "path": "scripts/gds/run-unit-tests.js",
5059
5059
  "mode": "0000644",
5060
- "sha256": "7f0ad4e54c0e5c251f00c78c7a6065d4a8db7bc88f0492c75bc63f8929108203"
5060
+ "sha256": "ce8856194aa805d1f62d47d4b04bac05168c0c748f869a094476a28b9db2a30a"
5061
5061
  },
5062
5062
  {
5063
5063
  "path": "scripts/gds/sandbox-stage.js",
@@ -5642,7 +5642,7 @@
5642
5642
  {
5643
5643
  "path": "src/module-api.js",
5644
5644
  "mode": "0000644",
5645
- "sha256": "b7a1d663d910c88b285298b741d8fe6b7edb6b6c7644a59ea81f087549182ef0"
5645
+ "sha256": "8066ee5e98e151dfc62228626a99a86f362697410aac483de27c75851cc3bdea"
5646
5646
  },
5647
5647
  {
5648
5648
  "path": "src/module-loader/loader.js",
@@ -6042,7 +6042,12 @@
6042
6042
  {
6043
6043
  "path": "tests/claim_error_surface.mjs",
6044
6044
  "mode": "0000644",
6045
- "sha256": "6273b5b86f0fecd24bcb54a36a673ec9d42f81180d602d5d374fc9d07a511072"
6045
+ "sha256": "365f498d290298e0cd74888aff080cfbf3439e5e0cf2dd883291f39dface27e7"
6046
+ },
6047
+ {
6048
+ "path": "tests/claim_gate_open_blocker.mjs",
6049
+ "mode": "0000644",
6050
+ "sha256": "e62324e8e504e4f2788355b1ed1ffc5b1d434ef0ef0e72c18751236436e10c49"
6046
6051
  },
6047
6052
  {
6048
6053
  "path": "tests/claim_gate_rebase.mjs",
@@ -2586,6 +2586,23 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
2586
2586
  throw { code: 'TASK_NOT_READY', currentStatus: task.status };
2587
2587
  }
2588
2588
 
2589
+ // task 1002650 (idea 1000644): the claimable FEED filters out ready tasks
2590
+ // with an open blocker link, but the direct claim-by-id path never checked —
2591
+ // the feed hid the trap and this path walked into it. Mirror migration 176's
2592
+ // task_is_fully_unblocked clause so both gates enforce the same rule.
2593
+ const { rows: openBlockers } = await client.query(
2594
+ `SELECT b.id, b.title
2595
+ FROM task_blockers tb
2596
+ JOIN blockers b ON b.id = tb.blocker_id
2597
+ WHERE tb.task_id = $1
2598
+ AND b.status = 'open'
2599
+ ORDER BY b.id`,
2600
+ [taskId]
2601
+ );
2602
+ if (openBlockers.length > 0) {
2603
+ throw { code: 'OPEN_BLOCKER', blockers: openBlockers };
2604
+ }
2605
+
2589
2606
  // Three-rank model (task #360 / ADR 0018; V3.R15 #239): a Xenos is the
2590
2607
  // sandboxed entry rank and may claim ONLY tasks explicitly opened to them
2591
2608
  // (newcomer_friendly). Metic+ (the approved working rank) and Archon claim
@@ -2893,6 +2910,27 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
2893
2910
  }
2894
2911
  }
2895
2912
 
2913
+ // task 1002650 (idea 1000644): same open-blocker rule as claimTask — the
2914
+ // batch path must not sidestep the gate the feed and single claim enforce.
2915
+ const { rows: blockedRows } = await client.query(
2916
+ `SELECT tb.task_id, b.id, b.title
2917
+ FROM task_blockers tb
2918
+ JOIN blockers b ON b.id = tb.blocker_id
2919
+ WHERE tb.task_id = ANY($1::bigint[])
2920
+ AND b.status = 'open'
2921
+ ORDER BY b.id`,
2922
+ [uniqIds]
2923
+ );
2924
+ const blockersByTask = new Map();
2925
+ for (const r of blockedRows) {
2926
+ const list = blockersByTask.get(Number(r.task_id)) || [];
2927
+ list.push({ id: r.id, title: r.title });
2928
+ blockersByTask.set(Number(r.task_id), list);
2929
+ }
2930
+ for (const [blockedTaskId, blockers] of blockersByTask) {
2931
+ failures.push({ task_id: blockedTaskId, reason: 'OPEN_BLOCKER', blockers });
2932
+ }
2933
+
2896
2934
  // Three-rank model (task #360 / ADR 0018; V3.R15 #239): a Xenos may
2897
2935
  // batch-claim only tasks explicitly opened to them (newcomer_friendly).
2898
2936
  // Mirrors the single claimTask gate so the batch path can't be used to
@@ -250,6 +250,10 @@ module.exports = function buildClaimsRouter() {
250
250
  err.code === 'TOUCHES_CONFLICT' ? 409 :
251
251
  err.code === 'MODULE_CONFLICT' ? 409 :
252
252
  err.code === 'DEPS_NOT_SHIPPED' ? 409 :
253
+ // task 1002650 (idea 1000644): a ready task with an open blocker link is
254
+ // hidden from the claimable feed; the direct claim-by-id gate now
255
+ // enforces the same rule instead of letting the claim walk into the trap.
256
+ err.code === 'OPEN_BLOCKER' ? 409 :
253
257
  // ADR 0120 part 3: the builder owes a rebase on an un-landed confirmed
254
258
  // task (reconciler-flagged needs_rebase); NEW claims are gated until
255
259
  // they land or release it. Existing claims are unaffected.
@@ -285,6 +289,12 @@ module.exports = function buildClaimsRouter() {
285
289
  const ids = (err.rebaseTasks || []).map((t) => `task ${t.id}`);
286
290
  err.hint = `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Land each (rebase its branch + re-ship, or /merge-mode) — or /builder-release it — then claim new work.`;
287
291
  }
292
+ // task 1002650: name the blocker(s) and the way forward — the same rule
293
+ // the claimable feed enforces, spoken at the gate instead of a bare 409.
294
+ if (err.code === 'OPEN_BLOCKER') {
295
+ const names = (err.blockers || []).map((b) => `blocker ${b.id} ("${b.title}")`);
296
+ err.hint = `This task is waiting on ${names.join(', ') || 'an open blocker'}. Resolve it first (/blocker-solve N drives one to done; resolving auto-promotes its linked tasks) — or pick another task from /builder-start.`;
297
+ }
288
298
  return res.fail(err.code, status, { ...err });
289
299
  }
290
300
  console.error('[gds] POST /claims', err);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.36",
3
+ "version": "1.19.37",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.36",
9
+ "version": "1.19.37",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.36",
3
+ "version": "1.19.37",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -256,6 +256,13 @@ function formatClaimFailure(r, taskId) {
256
256
  lines.push('Ship those first (or release them) and try again.');
257
257
  return { lines, exit: 1 };
258
258
  }
259
+ if (code === 'OPEN_BLOCKER') {
260
+ // task 1002650: the same rule the claimable feed enforces, spoken at the gate.
261
+ lines.push(`Task #${taskId} is waiting on an open blocker:`);
262
+ for (const b of d.blockers || []) lines.push(` - blocker ${b.id} ${b.title}`);
263
+ lines.push('Resolve it first (/blocker-solve N drives one to done; resolving auto-promotes its linked tasks) — or pick another task from /builder-start.');
264
+ return { lines, exit: 1 };
265
+ }
259
266
  // Fallback — still readable: the message + code, never a raw [object Object].
260
267
  lines.push(`Claim failed (${r ? r.status : '?'})${code ? ` [${code}]` : ''}${message ? `: ${message}` : ''}.`);
261
268
  return { lines, exit: 1 };
@@ -73,7 +73,7 @@ const INTEGRATION = new Set([
73
73
  // platform-server — network + package build + a spawned server, run by its own
74
74
  // .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
75
75
  'consumer_layout_boot',
76
- 'claim_gate_rebase', 'clawback_confirmed_backlog', 'discord_autojoin', 'discord_oauth',
76
+ 'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog', 'discord_autojoin', 'discord_oauth',
77
77
  'governance_fold_bench', 'governance_live_proof', 'linkify_refs', 'memory_isolation',
78
78
  'memory_pull', 'needs_rebase', 'parallel-load', 'reward_mode_cost_plus_only',
79
79
  'session_records', 'setup_smoke',
package/src/module-api.js CHANGED
@@ -499,8 +499,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
499
499
  // landed since 1.19.34 with no explicit bump. run 31507293725. (task 1002620)
500
500
  // 1.19.36 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
501
501
  // landed since 1.19.35 with no explicit bump. run 31509715311. (task 1002620)
502
+ // 1.19.37 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
503
+ // landed since 1.19.36 with no explicit bump. run 31510590459. (task 1002620)
502
504
  // ---------------------------------------------------------------------------
503
- const CORE_VERSION = '1.19.36'; // CI auto-patch carrier (ADR 0161); see changelog above
505
+ const CORE_VERSION = '1.19.37'; // CI auto-patch carrier (ADR 0161); see changelog above
504
506
 
505
507
  // A namespaced logger so a module's log lines are attributable + consistent.
506
508
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -36,6 +36,7 @@ t('never dumps [object Object] for any known code', () => {
36
36
  'session_already_has_bound_claim',
37
37
  'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
38
38
  'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
39
+ 'OPEN_BLOCKER',
39
40
  'some_unmapped_future_code',
40
41
  ];
41
42
  for (const c of codes) {
@@ -87,6 +88,17 @@ t('DEPS_NOT_SHIPPED: lists each blocking dependency', () => {
87
88
  assert.match(out, /Dep A/);
88
89
  });
89
90
 
91
+ // task 1002650: the wave-5 speech standard — the refusal names the blocker
92
+ // (id + title) and the remedy, not a bare 409.
93
+ t('OPEN_BLOCKER: names the blocker and the way forward', () => {
94
+ const out = text(formatClaimFailure(
95
+ envelope('OPEN_BLOCKER', { blockers: [{ id: 1000094, title: 'Owner must verify the installer' }] }), 9));
96
+ assert.match(out, /blocker 1000094/);
97
+ assert.match(out, /Owner must verify the installer/);
98
+ assert.match(out, /blocker-solve/);
99
+ assert.match(out, /pick another task/);
100
+ });
101
+
90
102
  t('MODULE_CONFLICT + TOUCHES_CONFLICT: list conflicting task titles', () => {
91
103
  const mc = text(formatClaimFailure(
92
104
  envelope('MODULE_CONFLICT', { module_key: 'economy', conflictTaskTitles: ['Bar'] }), 4));
@@ -0,0 +1,93 @@
1
+ // tests/claim_gate_open_blocker.mjs — task 1002650 (idea 1000644, real DB).
2
+ //
3
+ // The open-blocker claim gate: a ready task linked to an OPEN blocker is
4
+ // hidden from the claimable feed (CLAIMABLE_SQL), but the direct claim-by-id
5
+ // path never checked — POST /claims walked straight into the trap the feed was
6
+ // hiding. claimTask and claimTasksBatch now refuse with OPEN_BLOCKER, naming
7
+ // the blocker, mirroring migration 176's task_is_fully_unblocked clause.
8
+ // Real-DB integration test (self-skips without a reachable Postgres; in the
9
+ // INTEGRATION set so the default DB-free lane never runs it).
10
+ //
11
+ // Run: DATABASE_URL=... node tests/claim_gate_open_blocker.mjs
12
+
13
+ import { createRequire } from 'node:module';
14
+ import { strict as assert } from 'node:assert';
15
+ const require = createRequire(import.meta.url);
16
+ const { Pool } = require('pg');
17
+
18
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
19
+ try {
20
+ await pool.query('SELECT 1');
21
+ } catch (e) {
22
+ console.log(`claim_gate_open_blocker.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
23
+ await pool.end().catch(() => {});
24
+ process.exit(0);
25
+ }
26
+
27
+ const seams = require('../src/module-seams.js');
28
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
29
+ const db = require('../modules/lifecycle/db.js');
30
+
31
+ const VER = 'ZZZ-OB', WORKER = 90000301, TRAP = 90000302, CONTROL = 90000303, BLOCKER = 90000304;
32
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
33
+
34
+ async function cleanup() {
35
+ await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
36
+ await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id = ANY($2::bigint[])`, [WORKER, [TRAP, CONTROL]]).catch(() => {});
37
+ await q(`DELETE FROM claims WHERE task_id = ANY($1::bigint[])`, [[TRAP, CONTROL]]).catch(() => {});
38
+ await q(`DELETE FROM task_blockers WHERE task_id = ANY($1::bigint[])`, [[TRAP, CONTROL]]).catch(() => {});
39
+ await q(`DELETE FROM blockers WHERE id=$1`, [BLOCKER]).catch(() => {});
40
+ await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [[TRAP, CONTROL]]).catch(() => {});
41
+ await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
42
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
43
+ }
44
+
45
+ let pass = 0;
46
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
47
+
48
+ try {
49
+ await cleanup();
50
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'ob','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
51
+ // Archon so no rank/newcomer gate interferes; the gate under test is OPEN_BLOCKER.
52
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'ob-w','ob-worker','OB Worker','archon')`, [WORKER]);
53
+ // TRAP: ready (feed-hidden) with an open blocker linked. CONTROL: ready, clean.
54
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind) VALUES ($1,$2,'trap','ready',10,'feature')`, [TRAP, VER]);
55
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind) VALUES ($1,$2,'control','ready',10,'feature')`, [CONTROL, VER]);
56
+ await q(`INSERT INTO blockers (id,title,status) VALUES ($1,'Owner must flip the switch','open')`, [BLOCKER]);
57
+ await q(`INSERT INTO task_blockers (task_id,blocker_id) VALUES ($1,$2)`, [TRAP, BLOCKER]);
58
+
59
+ // 1) Direct claim-by-id is refused with OPEN_BLOCKER, naming the blocker.
60
+ let threw = null;
61
+ try {
62
+ await db.claimTask({ taskId: String(TRAP), builderId: String(WORKER), worktreeName: 'ob-trap' });
63
+ } catch (e) { threw = e; }
64
+ ok(threw && threw.code === 'OPEN_BLOCKER', `claim refused with OPEN_BLOCKER (got ${threw && (threw.code || threw.message)})`);
65
+ ok(Array.isArray(threw.blockers) && threw.blockers.some((b) => Number(b.id) === BLOCKER),
66
+ 'the 409 names the blocker id');
67
+ ok(threw.blockers.every((b) => typeof b.title === 'string' && b.title.length > 0),
68
+ 'each named blocker carries its title (the wave-5 speech payload)');
69
+ const trapStatus = (await q(`SELECT status FROM tasks WHERE id=$1`, [TRAP]))[0].status;
70
+ ok(trapStatus === 'ready', 'the refused task stays ready (no partial claim)');
71
+
72
+ // 2) The batch path enforces the same rule — no sidestep via /builder-sequence.
73
+ let batchThrew = null;
74
+ try {
75
+ await db.claimTasksBatch({ taskIds: [TRAP, CONTROL], builderId: String(WORKER), worktreePrefix: 'ob-batch' });
76
+ } catch (e) { batchThrew = e; }
77
+ ok(batchThrew && batchThrew.code === 'BATCH_FAILED', `batch claim refused (got ${batchThrew && (batchThrew.code || batchThrew.message)})`);
78
+ ok((batchThrew.failures || []).some((f) => Number(f.task_id) === TRAP && f.reason === 'OPEN_BLOCKER'),
79
+ 'the batch failure names the trapped task with reason OPEN_BLOCKER');
80
+
81
+ // 3) Resolving the blocker opens the gate.
82
+ await q(`UPDATE blockers SET status='resolved', resolved_at=now() WHERE id=$1`, [BLOCKER]);
83
+ const claim = await db.claimTask({ taskId: String(TRAP), builderId: String(WORKER), worktreeName: 'ob-trap' });
84
+ ok(claim && claim.id && Number(claim.task_id) === TRAP, 'claim succeeds once the blocker is resolved');
85
+
86
+ console.log(`\nclaim_gate_open_blocker (task 1002650): ${pass} passed, 0 failed`);
87
+ } catch (e) {
88
+ console.error('claim_gate_open_blocker.mjs FAILED:', e.stack || e.message);
89
+ process.exitCode = 1;
90
+ } finally {
91
+ await cleanup();
92
+ await pool.end();
93
+ }