@bongos/core 1.19.291 → 1.19.292

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.291",
6
- "core_contract": "1.19.291",
7
- "source_commit": "c05eeaf6f713e548b9a30460ce153d4388041ebc",
5
+ "core_version": "1.19.292",
6
+ "core_contract": "1.19.292",
7
+ "source_commit": "57bc876c876c64bf07069a3646e253006a917fc6",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-22T15:00:29.764Z",
9
+ "built_at": "2026-08-22T15:49:03.336Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 320,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1389,
14
+ "functional_verbatim": 1391,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1731,
20
- "tree_sha256": "d46fa302ab0b4aba74a3c947cfc01de0cf9bc482d5fec65df8ea2c518b29153b",
19
+ "file_count": 1733,
20
+ "tree_sha256": "ca47008669375b8c24cbecd5f6142a05b189fce9c02e8c3d2bdc29a2da237f87",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2434,6 +2434,11 @@
2434
2434
  "mode": "0000644",
2435
2435
  "sha256": "9939ee2c641caa8ae4f5055c60e3208a6ed4e379b1bfd1aaa67247a8ed4a4fd8"
2436
2436
  },
2437
+ {
2438
+ "path": "migrations/core_223_blocker_resolve_respects_other_open_blockers.sql",
2439
+ "mode": "0000644",
2440
+ "sha256": "8fc38c0353811699e19d861321d4fca8fe8fd5cca1f66ff96312d7f44fb0290e"
2441
+ },
2437
2442
  {
2438
2443
  "path": "modules/autonomy/CLAUDE.md",
2439
2444
  "mode": "0000644",
@@ -4522,12 +4527,12 @@
4522
4527
  {
4523
4528
  "path": "package-lock.json",
4524
4529
  "mode": "0000644",
4525
- "sha256": "7dc929fd01010d29e076564007cdb27b3cfecbf5b2f318f1b045a742d405ab04"
4530
+ "sha256": "417cc856bf6eca0ed8c214b12624c6bc7567bb96b07a5999ebf5b72ab6b4ca05"
4526
4531
  },
4527
4532
  {
4528
4533
  "path": "package.json",
4529
4534
  "mode": "0000644",
4530
- "sha256": "ccb290e054afd40d91c5058a10cbaae6c3d7c7d18a4afdad65f593825ee1ec4c"
4535
+ "sha256": "06e136123405fad423973fd79250b7baae689b122c0975e41f369ae87cdfe9e9"
4531
4536
  },
4532
4537
  {
4533
4538
  "path": "public-docs/index.html",
@@ -6172,7 +6177,7 @@
6172
6177
  {
6173
6178
  "path": "src/module-api.js",
6174
6179
  "mode": "0000644",
6175
- "sha256": "4699911082270a5de6b6692df442c0901c6153e2eef39139fdd2118bd5cb4869"
6180
+ "sha256": "a00525dccc2576669f8793418e40f7232dd2f8662163900142b561ce3c93a2db"
6176
6181
  },
6177
6182
  {
6178
6183
  "path": "src/module-loader/catalog.js",
@@ -6449,6 +6454,11 @@
6449
6454
  "mode": "0000644",
6450
6455
  "sha256": "0ade9bbcb1782ffa2dd01799002dee7d88f6b17dab8ee418fe58e1a06ffb925e"
6451
6456
  },
6457
+ {
6458
+ "path": "tests/blocker_resolve_guard.mjs",
6459
+ "mode": "0000644",
6460
+ "sha256": "3a85b9e9bee4258aa64d26ca025d52c605f5f33938d3fdd5a31c393f1ec3f249"
6461
+ },
6452
6462
  {
6453
6463
  "path": "tests/bongos_box_connect.mjs",
6454
6464
  "mode": "0000644",
@@ -0,0 +1,74 @@
1
+ -- core_223_blocker_resolve_respects_other_open_blockers.sql — resolving ONE of a
2
+ -- task's blockers must not release it while others are still open (task 1003149).
3
+ --
4
+ -- Bug: 009's resolve trigger promotes on ANY resolve, with no check for the
5
+ -- task's OTHER blockers:
6
+ --
7
+ -- UPDATE tasks SET status = 'ready'
8
+ -- WHERE id IN (SELECT task_id FROM task_blockers WHERE blocker_id = NEW.id)
9
+ -- AND status = 'blocked';
10
+ --
11
+ -- So a task blocked by N blockers becomes claimable the moment the FIRST one
12
+ -- resolves, with N-1 owner decisions still outstanding.
13
+ --
14
+ -- It is the ASYMMETRY that makes this a bug rather than a policy. Migration 176
15
+ -- (V4.R15 / task 953) fixed exactly this hazard on the DEPENDENCY promotion path
16
+ -- by folding a "no open blocker" check into task_is_fully_unblocked, and its own
17
+ -- header defers to this trigger — "009's blocker-resolve trigger is what should
18
+ -- release it, not a dep ship". But 009 releases on the first resolve, not the
19
+ -- last. One path fail-closes; the other did not. This aligns them.
20
+ --
21
+ -- Live at the time of writing: task 1000594 was blocked by BOTH blocker 1000065
22
+ -- and blocker 1000093 — duplicates covering the same two owner decisions — so
23
+ -- closing either as a duplicate would have released the task with the decision
24
+ -- still open. That is what made a routine duplicate-close unsafe, and there is
25
+ -- no unlink route to detach one first.
26
+ --
27
+ -- `b.id <> NEW.id` IS LOAD-BEARING, NOT COSMETIC. This is a BEFORE UPDATE
28
+ -- trigger, so the blockers row for NEW.id still reads status = 'open' in the
29
+ -- table while the function body runs. Without the exclusion the guard would see
30
+ -- the blocker being resolved as still open and NOTHING would ever promote —
31
+ -- turning an over-eager release into a permanent stall. For the same reason this
32
+ -- cannot simply call task_is_fully_unblocked(): that predicate reads the live
33
+ -- table and would always be false here.
34
+ --
35
+ -- Deliberately NOT also checking dependencies. A task left in 'blocked' because
36
+ -- its deps are unshipped would be STRANDED: the dep-promotion triggers (163)
37
+ -- only ever flip 'backlog' -> 'ready', never 'blocked' -> 'ready', so no path
38
+ -- would release it. Blocker-clearing and dep-clearing stay separate concerns.
39
+ --
40
+ -- Legacy markdown-anchor links (task_blockers.blocker_ref with no blocker_id
41
+ -- row) are not tracked here and behave exactly as before — same scope as 176.
42
+ --
43
+ -- Idempotent: pure CREATE OR REPLACE of the function. The trigger created in 009
44
+ -- resolves the function by name, so it needs no redefinition.
45
+
46
+ BEGIN;
47
+
48
+ CREATE OR REPLACE FUNCTION trg_promote_tasks_on_blocker_resolve()
49
+ RETURNS TRIGGER AS $$
50
+ BEGIN
51
+ IF NEW.status = 'resolved' AND (OLD.status IS NULL OR OLD.status <> 'resolved') THEN
52
+ IF NEW.resolved_at IS NULL THEN
53
+ NEW.resolved_at := now();
54
+ END IF;
55
+
56
+ UPDATE tasks
57
+ SET status = 'ready', updated_at = now()
58
+ WHERE id IN (SELECT task_id FROM task_blockers WHERE blocker_id = NEW.id)
59
+ AND status = 'blocked'
60
+ -- … and no OTHER blocker on this task is still open (task 1003149).
61
+ AND NOT EXISTS (
62
+ SELECT 1
63
+ FROM task_blockers tb
64
+ JOIN blockers b ON b.id = tb.blocker_id
65
+ WHERE tb.task_id = tasks.id
66
+ AND b.id <> NEW.id
67
+ AND b.status = 'open'
68
+ );
69
+ END IF;
70
+ RETURN NEW;
71
+ END;
72
+ $$ LANGUAGE plpgsql;
73
+
74
+ COMMIT;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.291",
3
+ "version": "1.19.292",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.291",
9
+ "version": "1.19.292",
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.291",
3
+ "version": "1.19.292",
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",
package/src/module-api.js CHANGED
@@ -1009,8 +1009,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1009
1009
  // landed since 1.19.289 with no explicit bump. run 32552538814. (task 1002620)
1010
1010
  // 1.19.291 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1011
1011
  // landed since 1.19.290 with no explicit bump. run 32580353000. (task 1002620)
1012
+ // 1.19.292 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1013
+ // landed since 1.19.291 with no explicit bump. run 32582800234. (task 1002620)
1012
1014
  // ---------------------------------------------------------------------------
1013
- const CORE_VERSION = '1.19.291'; // CI auto-patch carrier (ADR 0161); see changelog above
1015
+ const CORE_VERSION = '1.19.292'; // CI auto-patch carrier (ADR 0161); see changelog above
1014
1016
 
1015
1017
  // A namespaced logger so a module's log lines are attributable + consistent.
1016
1018
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,96 @@
1
+ // tests/blocker_resolve_guard.mjs
2
+ //
3
+ // Task 1003149 — resolving ONE of a task's blockers must not release it while
4
+ // others are still open. Static shape guard for core_223, in the style of
5
+ // dependencies_migration.mjs: no live DB, assert the SQL TEXT so a future edit
6
+ // cannot silently drop the guard or "simplify" it back into the bug. The runtime
7
+ // behaviour is proven against real Postgres at deploy; this pins the contract.
8
+ //
9
+ // Run: node tests/blocker_resolve_guard.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { readFileSync, readdirSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { dirname, join } from 'node:path';
15
+ import { createRequire } from 'node:module';
16
+ import { makeRunner } from './helpers.mjs';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const repoRoot = join(__dirname, '..');
20
+ const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
21
+ const require = createRequire(import.meta.url);
22
+
23
+ const MIG = 'core_223_blocker_resolve_respects_other_open_blockers.sql';
24
+ const mig = read('migrations', MIG);
25
+ const migN = mig.replace(/\s+/g, ' ');
26
+ // Comments carry the load-bearing rationale; strip them when asserting on the
27
+ // executable body so a claim in prose can never stand in for the real SQL.
28
+ const body = mig.replace(/^\s*--.*$/gm, '').replace(/\s+/g, ' ');
29
+
30
+ const { test, summary } = makeRunner();
31
+
32
+ await test('223 replaces the resolve trigger function (and only replaces it)', () => {
33
+ assert.match(mig, /CREATE OR REPLACE FUNCTION trg_promote_tasks_on_blocker_resolve\(\)/);
34
+ // The trigger from 009 binds by name, so redefining it here would be noise —
35
+ // and a DROP TRIGGER would leave a window where resolves promote nothing.
36
+ assert.doesNotMatch(body, /DROP TRIGGER/i);
37
+ assert.doesNotMatch(body, /CREATE TRIGGER/i);
38
+ });
39
+
40
+ await test('the promote UPDATE is guarded by a NOT EXISTS over the task\'s OTHER open blockers', () => {
41
+ assert.match(body, /UPDATE tasks SET status = 'ready'/);
42
+ assert.match(body, /AND status = 'blocked'/);
43
+ assert.match(body, /AND NOT EXISTS \( SELECT 1 FROM task_blockers tb JOIN blockers b ON b\.id = tb\.blocker_id/);
44
+ assert.match(body, /WHERE tb\.task_id = tasks\.id/);
45
+ assert.match(body, /AND b\.status = 'open'/);
46
+ });
47
+
48
+ await test("`b.id <> NEW.id` is present — without it a BEFORE trigger stalls forever", () => {
49
+ // The blockers row for NEW.id still reads 'open' while a BEFORE UPDATE trigger
50
+ // body runs, so dropping this exclusion would mean NOTHING ever promotes —
51
+ // trading an over-eager release for a permanent stall. This is the single
52
+ // most important character sequence in the migration.
53
+ assert.match(body, /AND b\.id <> NEW\.id/);
54
+ });
55
+
56
+ await test('223 does NOT also gate on dependencies — that would strand blocked tasks', () => {
57
+ // The dep-promotion triggers (163) only flip backlog -> ready, never
58
+ // blocked -> ready, so a task held in 'blocked' for unshipped deps would have
59
+ // no release path at all.
60
+ assert.doesNotMatch(body, /task_is_fully_unblocked/);
61
+ assert.doesNotMatch(body, /FROM dependencies/);
62
+ });
63
+
64
+ await test('the resolved_at capture from 009 is preserved', () => {
65
+ assert.match(body, /IF NEW\.resolved_at IS NULL THEN NEW\.resolved_at := now\(\);/);
66
+ assert.match(body, /IF NEW\.status = 'resolved' AND \(OLD\.status IS NULL OR OLD\.status <> 'resolved'\) THEN/);
67
+ });
68
+
69
+ await test('223 is transactional and idempotent (pure CREATE OR REPLACE)', () => {
70
+ assert.match(migN, /BEGIN;.*COMMIT;/);
71
+ assert.doesNotMatch(body, /ALTER TABLE/i);
72
+ assert.doesNotMatch(body, /DROP FUNCTION/i);
73
+ });
74
+
75
+ await test('009 remains the only OTHER definition — 223 is the last word', () => {
76
+ const defs = readdirSync(join(repoRoot, 'migrations'))
77
+ .filter((f) => f.endsWith('.sql'))
78
+ .filter((f) => /FUNCTION trg_promote_tasks_on_blocker_resolve/.test(read('migrations', f)))
79
+ .sort();
80
+ assert.deepEqual(defs, ['009_pms_v2_blockers_table.sql', MIG]);
81
+ // Ordering matters: migrate.sh applies in filename order, so a lower-numbered
82
+ // redefinition landing later would silently reinstate the bug.
83
+ assert.ok(MIG > '009_pms_v2_blockers_table.sql', 'core_223 must sort after 009');
84
+ });
85
+
86
+ await test('the JS resolve path still only OBSERVES — promotion stays in the trigger', () => {
87
+ const src = read('modules', 'ideas', 'blockers.js');
88
+ // resolveBlocker flips the blocker and then reads back which tasks are ready;
89
+ // if it ever started UPDATE-ing tasks.status itself, this guard would be
90
+ // bypassable and the fix would need to live in two places.
91
+ assert.doesNotMatch(src, /UPDATE\s+tasks[\s\S]{0,200}?status\s*=\s*'ready'/);
92
+ const blockers = require('../modules/ideas/blockers.js');
93
+ assert.equal(typeof blockers.resolveBlocker, 'function');
94
+ });
95
+
96
+ summary('blocker_resolve_guard');