@bongos/core 1.19.985 → 1.19.986

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.985",
6
- "core_contract": "1.19.985",
7
- "source_commit": "83b55732b7a45bc4ac27e3f8b4872570c2453a43",
5
+ "core_version": "1.19.986",
6
+ "core_contract": "1.19.986",
7
+ "source_commit": "df54cfbda9edb7e3e5b7f953174e31e6a5b65bc4",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-23T18:23:03.534Z",
9
+ "built_at": "2026-09-23T18:36:30.871Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 518,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2389,
14
+ "functional_verbatim": 2390,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2933,
20
- "tree_sha256": "a3e334e9c129ec8270c4874ec1df68f400624469cfb9652714507b7049ec07f5",
19
+ "file_count": 2934,
20
+ "tree_sha256": "8b898c04436c76259878da047c512541c0d0a03e9de13419b31e0a0d41ffa182",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2712,7 +2712,7 @@
2712
2712
  {
2713
2713
  "path": "docs/module-api-changelog.md",
2714
2714
  "mode": "0000644",
2715
- "sha256": "9a567bebe4a58d5b7efeb164ea93f3e134dc7859c12316e2319787ac3e22235c"
2715
+ "sha256": "e779420483a2e19ea834a5995d1960296dad434fa158c1e46db3ee4e12b6569e"
2716
2716
  },
2717
2717
  {
2718
2718
  "path": "docs/modules-contract.md",
@@ -5802,7 +5802,7 @@
5802
5802
  {
5803
5803
  "path": "modules/lifecycle/db-ship.js",
5804
5804
  "mode": "0000644",
5805
- "sha256": "d280c54d35721d0a24090ab9cc3ac35d311d253d59d4ea57fb542e1232f5c744"
5805
+ "sha256": "c239b8cb88087f7bdb860c876e3e924bc1efef3cdba0ddec0374f71f7f2082d5"
5806
5806
  },
5807
5807
  {
5808
5808
  "path": "modules/lifecycle/db-tasks.js",
@@ -8157,12 +8157,12 @@
8157
8157
  {
8158
8158
  "path": "package-lock.json",
8159
8159
  "mode": "0000644",
8160
- "sha256": "e56dc24d5e58a71b9768ab10c269e8fb9fd40691fd92e8a205e6e0200204393b"
8160
+ "sha256": "2fcebda4add1d874aee7ce5f756b37bb55485b6c001aaccde5b0f9624d4c0192"
8161
8161
  },
8162
8162
  {
8163
8163
  "path": "package.json",
8164
8164
  "mode": "0000644",
8165
- "sha256": "e8489984d076fec6055a6c04a71ea53fe859deac7c8bd7c50acb59e45a00c748"
8165
+ "sha256": "80d5c9a1504e44bfccb0e3bfb0fd38e99eed359118d9d48f12bf9cab895f0533"
8166
8166
  },
8167
8167
  {
8168
8168
  "path": "public-docs/index.html",
@@ -10187,7 +10187,7 @@
10187
10187
  {
10188
10188
  "path": "src/module-api.js",
10189
10189
  "mode": "0000644",
10190
- "sha256": "ee0aba3198c03fa51608e06ce5d5f483335085a1aded69fef7afd71f600dfea5"
10190
+ "sha256": "17399ab568a9343c5c5cead3e1e467af941ec891ee117b424ad0e2ed10151731"
10191
10191
  },
10192
10192
  {
10193
10193
  "path": "src/module-loader/catalog.js",
@@ -14382,7 +14382,12 @@
14382
14382
  {
14383
14383
  "path": "tests/task_merge.mjs",
14384
14384
  "mode": "0000644",
14385
- "sha256": "2a16f120729c641de65cccf79f1583e2d2e68b72bf10277310991c1787dde2bf"
14385
+ "sha256": "716c421719fbf14a8024b7b34090ac50a8384fa3de9d35d1336f302c960c16c6"
14386
+ },
14387
+ {
14388
+ "path": "tests/task_merge_route.mjs",
14389
+ "mode": "0000644",
14390
+ "sha256": "24d411385ae7f8f21a0ba0e785257888052b3539c234a04f5bde797628e05654"
14386
14391
  },
14387
14392
  {
14388
14393
  "path": "tests/task_ownership_gate.mjs",
@@ -2444,5 +2444,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2444
2444
  landed since 1.19.983 with no explicit bump. run 35900593334. (task 1002620)
2445
2445
  1.19.985 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2446
2446
  landed since 1.19.984 with no explicit bump. run 35902000024. (task 1002620)
2447
+ 1.19.986 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2448
+ landed since 1.19.985 with no explicit bump. run 35903379073. (task 1002620)
2447
2449
  ---------------------------------------------------------------------------
2448
2450
  ```
@@ -17,6 +17,7 @@ const seams = api;
17
17
  const doneWhen = require('./done-when.js');
18
18
  const { THETES_GRADUATION_THRESHOLD, maybeAutoGraduateToThetes, foldWorktreeName } = require('./db-rank-authz.js');
19
19
  const { mostRecentClaimHolderId, sealSessionLog, staleClaimHourPredicate } = require('./db-shared.js');
20
+ const { LIVE_STATUSES } = require('./dead-deps.js');
20
21
 
21
22
  // shouldRefuseClaimResolve — may THIS caller resolve THIS claim? (task 1003767)
22
23
  //
@@ -1374,6 +1375,7 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
1374
1375
  return {
1375
1376
  taskId, taskTitle: task.title, taskStatus: 'abandoned',
1376
1377
  mergedIntoTaskId: Number(task.merged_into_task_id), alreadyMerged: true,
1378
+ strandedDependents: [],
1377
1379
  };
1378
1380
  }
1379
1381
  throw Object.assign(
@@ -1391,7 +1393,7 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
1391
1393
  }
1392
1394
 
1393
1395
  const { rows: targetRows } = await client.query(
1394
- `SELECT id, merged_into_task_id FROM tasks WHERE id = $1`,
1396
+ `SELECT id, merged_into_task_id FROM tasks WHERE id = $1 FOR UPDATE`,
1395
1397
  [intoTaskId]
1396
1398
  );
1397
1399
  if (targetRows.length === 0) {
@@ -1424,6 +1426,20 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
1424
1426
  [taskId, intoTaskId, noteText]
1425
1427
  );
1426
1428
 
1429
+ // Abandoned blocks its dependents forever (owner ruling 2026-08-21, see this
1430
+ // module's CLAUDE.md), so the merge must say who it just stranded — the same
1431
+ // report scripts/gds/abandon.js prints. Live dependents only; read inside the
1432
+ // txn so it agrees with the row we just flipped.
1433
+ const { rows: stranded } = await client.query(
1434
+ `SELECT t.id, t.title, t.status
1435
+ FROM dependencies d
1436
+ JOIN tasks t ON t.id = d.from_id
1437
+ WHERE d.from_kind = 'task' AND d.to_kind = 'task' AND d.to_id = $1
1438
+ AND t.status = ANY($2::text[])
1439
+ ORDER BY t.id`,
1440
+ [taskId, [...LIVE_STATUSES]]
1441
+ );
1442
+
1427
1443
  await client.query('COMMIT');
1428
1444
  return {
1429
1445
  taskId,
@@ -1432,6 +1448,7 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
1432
1448
  previousStatus: task.status,
1433
1449
  mergedIntoTaskId: Number(intoTaskId),
1434
1450
  alreadyMerged: false,
1451
+ strandedDependents: stranded.map((r) => ({ id: String(r.id), title: r.title, status: r.status })),
1435
1452
  };
1436
1453
  } catch (err) {
1437
1454
  await client.query('ROLLBACK').catch(() => {});
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.985",
3
+ "version": "1.19.986",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.985",
9
+ "version": "1.19.986",
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.985",
3
+ "version": "1.19.986",
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
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.985'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.986'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -48,7 +48,7 @@ const { test, summary } = makeRunner();
48
48
  function fakePool({
49
49
  status = 'ready', notFound = false, mergedInto = null,
50
50
  hasActiveClaim = false, targetNotFound = false, targetMergedInto = null,
51
- updatedStatus = 'abandoned',
51
+ updatedStatus = 'abandoned', dependents = [],
52
52
  } = {}) {
53
53
  return makeSqlAwareClient((s, params) => {
54
54
  if (/^SELECT id, title, status, merged_into_task_id FROM tasks WHERE id = \$1 FOR UPDATE/.test(s)) {
@@ -57,9 +57,12 @@ function fakePool({
57
57
  if (/^SELECT id FROM claims WHERE task_id = \$1 AND released_at IS NULL/.test(s)) {
58
58
  return { rows: hasActiveClaim ? [{ id: 999 }] : [] };
59
59
  }
60
- if (/^SELECT id, merged_into_task_id FROM tasks WHERE id = \$1/.test(s)) {
60
+ if (/^SELECT id, merged_into_task_id FROM tasks WHERE id = \$1 FOR UPDATE/.test(s)) {
61
61
  return { rows: targetNotFound ? [] : [{ id: params[0], merged_into_task_id: targetMergedInto }] };
62
62
  }
63
+ if (/^SELECT t\.id, t\.title, t\.status FROM dependencies d/.test(s)) {
64
+ return { rows: dependents };
65
+ }
63
66
  if (/^UPDATE tasks/.test(s)) {
64
67
  return { rows: [{ id: params[0], title: 'fixture bug', status: updatedStatus }] };
65
68
  }
@@ -194,5 +197,24 @@ await test('merge: a target that is itself merged elsewhere is refused, naming w
194
197
  assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0);
195
198
  });
196
199
 
200
+ await test('merge: the target row is read FOR UPDATE (no-chains guard is not a TOCTOU)', async () => {
201
+ const pool = fakePool({ status: 'ready' });
202
+ await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool });
203
+ const q = find(pool._client.queries, /^SELECT id, merged_into_task_id FROM tasks WHERE id = \$1/);
204
+ assert.ok(q, 'target read must happen');
205
+ assert.match(String(q.sql).replace(/\s+/g, ' '), /FOR UPDATE/);
206
+ });
207
+
208
+ await test('merge: reports the live dependents it just stranded', async () => {
209
+ const dependents = [{ id: 5, title: 'blocked work', status: 'backlog' }];
210
+ const pool = fakePool({ status: 'ready', dependents });
211
+ const out = await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool });
212
+ assert.deepEqual(out.strandedDependents, [{ id: '5', title: 'blocked work', status: 'backlog' }]);
213
+ const none = await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool: fakePool({ status: 'ready' }) });
214
+ assert.deepEqual(none.strandedDependents, []);
215
+ const again = await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool: fakePool({ status: 'abandoned', mergedInto: TARGET }) });
216
+ assert.deepEqual(again.strandedDependents, [], 'idempotent re-merge keeps the field shape');
217
+ });
218
+
197
219
  // -------------------------------------------------------------------------
198
220
  summary();
@@ -0,0 +1,93 @@
1
+ // tests/task_merge_route.mjs
2
+ //
3
+ // POST /tasks/:id/merge as an HTTP route (task 1003622). tests/task_merge.mjs
4
+ // drives db.mergeTask directly; nothing there proves the route validates input,
5
+ // maps each mergeTask error code to its status, or surfaces strandedDependents.
6
+ // DB-free: db.mergeTask is stubbed on the cached module object.
7
+ //
8
+ // Run: node --test tests/task_merge_route.mjs
9
+
10
+ import { test } from 'node:test';
11
+ import { strict as assert } from 'node:assert';
12
+ import { createRequire } from 'node:module';
13
+ import http from 'node:http';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const db = require('../modules/lifecycle/db.js');
17
+ const api = require('../src/module-api.js');
18
+
19
+ let calls = [];
20
+ let impl = async () => ({});
21
+ db.mergeTask = async (args) => { calls.push(args); return impl(args); };
22
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, rank: 'metic' }; next(); };
23
+ api.requirePermission = () => (_req, _res, next) => next();
24
+
25
+ const express = require('express');
26
+ const buildRouter = require('../modules/lifecycle/routes/tasks.js');
27
+ const app = express();
28
+ app.use(express.json());
29
+ app.use((_req, res, next) => {
30
+ res.fail = (code, opts) => { const o = typeof opts === 'number' ? { status: opts } : (opts || {}); res.status(o.status || 400).json({ error: { code, ...o } }); };
31
+ next();
32
+ });
33
+ app.use('/api/bongos', buildRouter());
34
+ const server = app.listen(0);
35
+ await new Promise((r) => server.once('listening', r));
36
+ const port = server.address().port;
37
+
38
+ function post(path, body) {
39
+ return new Promise((resolve, reject) => {
40
+ const data = JSON.stringify(body);
41
+ const req = http.request({ host: '127.0.0.1', port, path: `/api/bongos${path}`, method: 'POST',
42
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } }, (res) => {
43
+ let d = '';
44
+ res.on('data', (c) => { d += c; });
45
+ res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* */ } resolve({ status: res.statusCode, body: j, raw: d }); });
46
+ });
47
+ req.on('error', reject);
48
+ req.end(data);
49
+ });
50
+ }
51
+
52
+ const fail = (code, extra = {}) => async () => { throw Object.assign(new Error(code), { code, ...extra }); };
53
+
54
+ test('success returns mergeTask result including strandedDependents', async () => {
55
+ calls = [];
56
+ impl = async () => ({ taskId: 10, mergedIntoTaskId: 20, strandedDependents: [{ id: '5', title: 't', status: 'backlog' }] });
57
+ const r = await post('/tasks/10/merge', { into_task_id: 20, reason: 'dup' });
58
+ assert.equal(r.status, 200, r.raw);
59
+ assert.equal(r.body.ok, true);
60
+ assert.deepEqual(r.body.strandedDependents, [{ id: '5', title: 't', status: 'backlog' }]);
61
+ assert.deepEqual(calls, [{ taskId: 10, intoTaskId: 20, reason: 'dup' }]);
62
+ });
63
+
64
+ test('missing into_task_id is rejected before mergeTask runs', async () => {
65
+ calls = [];
66
+ const r = await post('/tasks/10/merge', {});
67
+ assert.equal(r.status, 400, r.raw);
68
+ assert.equal(calls.length, 0);
69
+ });
70
+
71
+ for (const [code, status, extra] of [
72
+ ['MERGE_SELF', 400, {}],
73
+ ['TASK_NOT_FOUND', 404, {}],
74
+ ['CANNOT_MERGE_SHIPPED', 409, {}],
75
+ ['ALREADY_MERGED', 409, { merged_into: '30' }],
76
+ ['TASK_HAS_ACTIVE_CLAIM', 409, {}],
77
+ ['MERGE_TARGET_NOT_FOUND', 404, {}],
78
+ ['MERGE_TARGET_MERGED', 409, { merged_into: '30' }],
79
+ ]) {
80
+ test(`${code} maps to HTTP ${status}`, async () => {
81
+ impl = fail(code, extra);
82
+ const r = await post('/tasks/10/merge', { into_task_id: 20 });
83
+ assert.equal(r.status, status, r.raw);
84
+ });
85
+ }
86
+
87
+ test('an unexpected error is a 500 merge_failed', async () => {
88
+ impl = fail('BOOM');
89
+ const r = await post('/tasks/10/merge', { into_task_id: 20 });
90
+ assert.equal(r.status, 500, r.raw);
91
+ });
92
+
93
+ test.after(() => server.close());