@bongos/core 1.19.63 → 1.19.64

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.63",
6
- "core_contract": "1.19.63",
7
- "source_commit": "aecb337d2bcfb665111227e9aebaffa93f867699",
5
+ "core_version": "1.19.64",
6
+ "core_contract": "1.19.64",
7
+ "source_commit": "ce3ba6421d4c20ca1f0b218a1b09d8825db932b1",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-12T01:29:46.616Z",
9
+ "built_at": "2026-08-12T03:26:56.981Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 291,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1532,
20
- "tree_sha256": "165231bc8a6ec61199f57070fc8267562e283cff9b5804d340d4f3b57cc98de2",
20
+ "tree_sha256": "c8e496c8bd2e61bc44e0ccb85fcceadb92acbeaa5ac0c7db1f856004a36f2728",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -947,7 +947,7 @@
947
947
  {
948
948
  "path": "docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md",
949
949
  "mode": "0000644",
950
- "sha256": "9613664f0f3906aeda7c384c3a6dfaacb8a0a048b0010da8a011831846d7911e"
950
+ "sha256": "3aa1e9074bd62168f999139533d672b0e0369186e2a78bbc966bca940df8d012"
951
951
  },
952
952
  {
953
953
  "path": "docs/adr/0097-retroactive-reward-backfill.md",
@@ -3382,7 +3382,7 @@
3382
3382
  {
3383
3383
  "path": "modules/lifecycle/db.js",
3384
3384
  "mode": "0000644",
3385
- "sha256": "37303ffa7a795ea41db83f346bc736898720a3d1521434f566a71bbeb7b4dc15"
3385
+ "sha256": "bc1526d068698e489fc2a698c04418e60097f18b56fd5c0dcdb6a6f62bbd1c81"
3386
3386
  },
3387
3387
  {
3388
3388
  "path": "modules/lifecycle/done-when.js",
@@ -3457,7 +3457,7 @@
3457
3457
  {
3458
3458
  "path": "modules/lifecycle/routes/claims.js",
3459
3459
  "mode": "0000644",
3460
- "sha256": "92065e0a5cfd2a5d76bbf04f4efafe0e2b3bc31cf616136963d4c07fad25047e"
3460
+ "sha256": "60f698b88fab3b850e2ab7d4350b7192ca57fa932089d0a24a187add08e5c819"
3461
3461
  },
3462
3462
  {
3463
3463
  "path": "modules/lifecycle/routes/dependencies.js",
@@ -4037,12 +4037,12 @@
4037
4037
  {
4038
4038
  "path": "package-lock.json",
4039
4039
  "mode": "0000644",
4040
- "sha256": "ec06847fa5cf675d4847afeed5612e44a0fd336694a1187d342a9f60ef957b09"
4040
+ "sha256": "90fef156fcc7117e3a88064eaab10da019b48fc2b1b5586cbbfe7d4006333682"
4041
4041
  },
4042
4042
  {
4043
4043
  "path": "package.json",
4044
4044
  "mode": "0000644",
4045
- "sha256": "612d7a22ac1c386d80c1c99b9aa52d70363c8a7a4f8ffb0598624c995578f060"
4045
+ "sha256": "9fd4475c4f673be84d4504811890f5ec5339a226e4c738d41b6bb94c808cc76e"
4046
4046
  },
4047
4047
  {
4048
4048
  "path": "public-docs/index.html",
@@ -5662,7 +5662,7 @@
5662
5662
  {
5663
5663
  "path": "src/module-api.js",
5664
5664
  "mode": "0000644",
5665
- "sha256": "dfd274bc93670afa2368e0abea4d79ab5bd415996ed11c56a14cbafb449aafc6"
5665
+ "sha256": "d55be57d6e2c4435e3b1cdab9543b90c449358b26ba1035fdb82b097f41ca7d6"
5666
5666
  },
5667
5667
  {
5668
5668
  "path": "src/module-loader/loader.js",
@@ -7052,7 +7052,7 @@
7052
7052
  {
7053
7053
  "path": "tests/one_active_claim_per_session.mjs",
7054
7054
  "mode": "0000644",
7055
- "sha256": "3a4d6cc704d0ecd7be2f094ad10baf9c725a2045587994f076de76fb26ce90e5"
7055
+ "sha256": "030c7f3958d1fef45977326b52596e21638fe80b4ff9e7ecba31c76063283aea"
7056
7056
  },
7057
7057
  {
7058
7058
  "path": "tests/orphaned_claim_ship.mjs",
@@ -40,6 +40,10 @@ The default claim flow (`.claude/skills/builder-claim/SKILL.md`) ensures the tas
40
40
 
41
41
  > **Amended 2026-08-06 (task 1002570 / `core_204`).** The index as shipped was *stricter* than the worktree-scoped guard above: keyed on `creator_session_id` alone, it refused at the DB layer the second-worktree parallel claim the guard deliberately allows — breaking the many-sessions-per-builder norm this ADR set out to preserve. `migrations/<redacted>.sql` re-keys it to `(creator_session_id, COALESCE(worktree_name, ''))` under the name `uniq_active_claim_per_session_worktree`, same partial predicate, so the backstop now enforces exactly the guard's boundary. The `COALESCE` fold (rather than PG15's `NULLS NOT DISTINCT`) is load-bearing: a plain composite index treats NULLs as distinct, so two worktree-less claims off one shared clone would both insert and re-open the commingling hole. A `23505` from either index generation surfaces as `session_already_has_bound_claim` (task 1002589), never the false `ALREADY_CLAIMED`.
42
42
 
43
+ > **Amended 2026-08-11 (task 1002522) — the guard and the index now fold tree identity identically.** `core_204`'s key folds `NULL` and `''` together (`COALESCE(worktree_name, '')`); the guard compared raw values, where `'' !== null`. So a claim sent with `worktree_name: ""` (the route caps the length but never required non-empty) was waved through beside a worktree-less claim and *then* refused by the index — a `23505` on a task nobody had taken. `db.foldWorktreeName` is now the single normalization: it folds empty/whitespace-only to `NULL` and trims, and it is applied at **every ingest point** (`claimTask`, `claimTasksBatch`'s prefix, and `shouldRefuseSessionClaim`), so stored names are always trimmed-or-`NULL` and the guard's comparison of stored rows is exact by construction. Pinned DB-free in `tests/one_active_claim_per_session.mjs`.
44
+ >
45
+ > Two things were checked and deliberately left alone. **The stale-claim sweep needs no re-tuning** now that one session may hold N claims: both the heartbeat (`session_pulses`, PK `claim_id`) and the sweep (`modules/sessions/pulse.js sweepStaleClaims`) are keyed per *claim*, never per session, so each parallel worktree's claim ages out on its own clock. **One residual divergence remains**, tracked as [#1002709](https://example.com/builders#/task/1002709): V3.R51 disambiguation renames a colliding `worktree_name` (`"wt-a"` → `"wt-a-2"`) while the CLI keeps sending the original, so guard and index both key on a name the caller never used — a second claim in the *same* real tree can slip past both. Closing it needs the requested name persisted (a schema decision), which is why it is a follow-up rather than part of this amendment.
46
+
43
47
  ## Consequences
44
48
 
45
49
  - A session can hold at most one active claim — commingled ships are structurally prevented, not just discouraged.
@@ -335,6 +335,30 @@ function singleRungPromotionError(oldRank, newRank) {
335
335
  };
336
336
  }
337
337
 
338
+ // foldWorktreeName — task 1002522. The ONE normalization of an incoming
339
+ // worktree_name, applied at every ingest point so the app guard
340
+ // (shouldRefuseSessionClaim) and its DB backstop (core_204's index, keyed on
341
+ // (creator_session_id, COALESCE(worktree_name, ''))) can never disagree about
342
+ // which claims share a working tree.
343
+ //
344
+ // The index folds NULL to '' — so to Postgres an EMPTY-string name IS the
345
+ // worktree-less group. The guard compared raw values, where '' !== null: it
346
+ // waved through a ''-named claim beside a worktree-less one, and the INSERT
347
+ // then failed the backstop with a 23505 the caller never earned. Whitespace-only
348
+ // is the same case wearing a coat (`.claude/worktrees/<blank>` is the repo root,
349
+ // not a tree), so it folds too, and surrounding whitespace is trimmed rather
350
+ // than stored — an untrimmed name would key the index differently from the
351
+ // trimmed one the guard compared.
352
+ //
353
+ // Fold ONCE, at ingest: claimTask/claimTasksBatch normalize before the INSERT,
354
+ // so every stored name is already trimmed-or-NULL and the guard's comparison of
355
+ // stored rows is exact. Pure + exported for DB-free tests.
356
+ function foldWorktreeName(name) {
357
+ if (name == null) return null;
358
+ const trimmed = String(name).trim();
359
+ return trimmed === '' ? null : trimmed;
360
+ }
361
+
338
362
  // disambiguateWorktreeName — V3.R51 (#270). Two simultaneously-ACTIVE claims
339
363
  // must never share a worktree_name: `.claude/worktrees/<name>` is a filesystem
340
364
  // path, and two live builders pointing at the same one would stomp each other.
@@ -2762,7 +2786,10 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
2762
2786
  // which is what the overnight builder relies on. We hold FOR UPDATE on the
2763
2787
  // task row already; reading the active names inside the same txn is good
2764
2788
  // enough for the disambiguation to be stable for this insert.
2765
- let finalWorktreeName = worktreeName ?? null;
2789
+ // task 1002522: fold ''/whitespace → NULL before anything keys on the name.
2790
+ // core_204's index folds them together anyway (COALESCE(worktree_name, ''));
2791
+ // normalizing here means the stored row matches what the guard compared.
2792
+ let finalWorktreeName = foldWorktreeName(worktreeName);
2766
2793
  if (finalWorktreeName) {
2767
2794
  const { rows: activeNameRows } = await client.query(
2768
2795
  `SELECT worktree_name FROM claims
@@ -3121,8 +3148,12 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
3121
3148
  );
3122
3149
  reservedMigrationNumber = bumpRows[0].allocated;
3123
3150
  }
3124
- const worktreeName = worktreePrefix
3125
- ? `${worktreePrefix}-${t.id}`
3151
+ // task 1002522: fold the prefix the same way a single claim's name is
3152
+ // folded, so a blank/whitespace prefix yields worktree-less rows rather
3153
+ // than names like " -1234" that the index would key as distinct trees.
3154
+ const foldedPrefix = foldWorktreeName(worktreePrefix);
3155
+ const worktreeName = foldedPrefix
3156
+ ? `${foldedPrefix}-${t.id}`
3126
3157
  : null;
3127
3158
  const rowCreatorSessionId = i === 0 ? (creatorSessionId ?? null) : null;
3128
3159
  try {
@@ -5546,6 +5577,7 @@ module.exports = {
5546
5577
  countShippedTasksForBuilder,
5547
5578
  singleRungPromotionError,
5548
5579
  disambiguateWorktreeName,
5580
+ foldWorktreeName,
5549
5581
  updateTaskParallelSafe,
5550
5582
  updateTaskSecuritySensitive,
5551
5583
  updateTaskAutomationTag,
@@ -80,13 +80,17 @@ function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, wo
80
80
  return { refuse: false, inFlightTaskId: null, inFlightTitle: null, reason: null };
81
81
  }
82
82
  const claims = Array.isArray(activeClaimsForSession) ? activeClaimsForSession : [];
83
- const incomingTree = worktreeName == null ? null : worktreeName;
84
- // Two claims share a WORKING TREE iff their worktree_names match, or both are
85
- // null (a worktree-less clone — the single branch genuinely commingles).
86
- const sameWorkingTree = (name) => {
87
- const t = name == null ? null : name;
88
- return t === incomingTree;
89
- };
83
+ // task 1002522: fold through db.foldWorktreeName — the SAME normalization the
84
+ // INSERT applies — so this decision keys trees exactly the way core_204's
85
+ // backstop index does (it folds NULL and '' into one group via
86
+ // COALESCE(worktree_name, '')). Comparing raw values let a ''-named claim slip
87
+ // past the guard and then trip the index, refusing a claim on a task that was
88
+ // never taken.
89
+ const incomingTree = db.foldWorktreeName(worktreeName);
90
+ // Two claims share a WORKING TREE iff their folded worktree_names match — which
91
+ // includes both being worktree-less (a plain clone, whose single branch
92
+ // genuinely commingles).
93
+ const sameWorkingTree = (name) => db.foldWorktreeName(name) === incomingTree;
90
94
  // Defence in depth: only count claims that belong to THIS session, are genuinely
91
95
  // session-bound (a NULL-session row must never trip the block), AND would land in
92
96
  // the SAME working tree as the incoming claim.
@@ -122,7 +126,9 @@ async function gateOneActiveClaimPerSession(req, res, next) {
122
126
  // The incoming claim's working tree — the hazard boundary the guard keys on. A
123
127
  // second claim in a DIFFERENT worktree is safe (different branch); only a second
124
128
  // claim into the SAME tree (or off one worktree-less clone) commingles.
125
- const worktreeName = req.body?.worktree_name ?? null;
129
+ // Folded (task 1002522) so the guard, the refusal message, and the row the
130
+ // INSERT will actually write all speak about the same tree.
131
+ const worktreeName = db.foldWorktreeName(req.body?.worktree_name);
126
132
  let activeClaimsForSession;
127
133
  try {
128
134
  activeClaimsForSession = await db.getActiveClaimsForSession(creatorSessionId);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.63",
3
+ "version": "1.19.64",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.63",
9
+ "version": "1.19.64",
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.63",
3
+ "version": "1.19.64",
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
@@ -553,8 +553,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
553
553
  // landed since 1.19.61 with no explicit bump. run 31552833331. (task 1002620)
554
554
  // 1.19.63 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
555
555
  // landed since 1.19.62 with no explicit bump. run 31553743979. (task 1002620)
556
+ // 1.19.64 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
557
+ // landed since 1.19.63 with no explicit bump. run 31560059785. (task 1002620)
556
558
  // ---------------------------------------------------------------------------
557
- const CORE_VERSION = '1.19.63'; // CI auto-patch carrier (ADR 0161); see changelog above
559
+ const CORE_VERSION = '1.19.64'; // CI auto-patch carrier (ADR 0161); see changelog above
558
560
 
559
561
  // A namespaced logger so a module's log lines are attributable + consistent.
560
562
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -183,6 +183,80 @@ t('names the in-flight task in the MATCHING worktree, not the first row', () =>
183
183
  assert.equal(d.inFlightTitle, 'Two');
184
184
  });
185
185
 
186
+ // =========================================================================
187
+ // task 1002522 — the guard and core_204's backstop index must agree EXACTLY on
188
+ // which claims share a tree. The index keys (creator_session_id,
189
+ // COALESCE(worktree_name, '')), so to Postgres '' IS the worktree-less group;
190
+ // the guard compared raw values, where '' !== null. db.foldWorktreeName is the
191
+ // single normalization both sides now use (and claimTask applies before the
192
+ // INSERT, so stored rows are already trimmed-or-NULL).
193
+ // =========================================================================
194
+ const { foldWorktreeName } = require('../modules/lifecycle/db.js');
195
+
196
+ t('foldWorktreeName folds null / empty / whitespace to the same NULL key', () => {
197
+ assert.equal(foldWorktreeName(null), null);
198
+ assert.equal(foldWorktreeName(undefined), null);
199
+ assert.equal(foldWorktreeName(''), null);
200
+ assert.equal(foldWorktreeName(' '), null);
201
+ assert.equal(foldWorktreeName('\t\n'), null);
202
+ });
203
+
204
+ t('foldWorktreeName trims but never renames a real worktree', () => {
205
+ assert.equal(foldWorktreeName('wt-a'), 'wt-a');
206
+ assert.equal(foldWorktreeName(' wt-a '), 'wt-a');
207
+ // Distinct trees stay distinct — the fold must not collapse parallel work.
208
+ assert.notEqual(foldWorktreeName('wt-a'), foldWorktreeName('wt-a-2'));
209
+ });
210
+
211
+ t("empty-string worktree is the SAME tree as worktree-less → refused (matches the index)", () => {
212
+ // Before the fold this returned allow, and the INSERT then tripped
213
+ // uniq_active_claim_per_session_worktree — a 23505 on a task nobody had taken.
214
+ const d = shouldRefuseSessionClaim({
215
+ creatorSessionId: 'sess-6',
216
+ worktreeName: '',
217
+ activeClaimsForSession: [
218
+ { claim_id: 50, task_id: 70, creator_session_id: 'sess-6', worktree_name: null, title: 'shared clone' },
219
+ ],
220
+ });
221
+ assert.equal(d.refuse, true);
222
+ assert.equal(d.inFlightTaskId, 70);
223
+ });
224
+
225
+ t('the mirror case: worktree-less incoming vs a stored empty-string name → refused', () => {
226
+ const d = shouldRefuseSessionClaim({
227
+ creatorSessionId: 'sess-6',
228
+ worktreeName: null,
229
+ activeClaimsForSession: [
230
+ { claim_id: 51, task_id: 71, creator_session_id: 'sess-6', worktree_name: ' ', title: 'blank name' },
231
+ ],
232
+ });
233
+ assert.equal(d.refuse, true);
234
+ assert.equal(d.inFlightTaskId, 71);
235
+ });
236
+
237
+ t('whitespace around a real name does not fake a second tree', () => {
238
+ const d = shouldRefuseSessionClaim({
239
+ creatorSessionId: 'sess-6',
240
+ worktreeName: ' wt-a ',
241
+ activeClaimsForSession: [
242
+ { claim_id: 52, task_id: 72, creator_session_id: 'sess-6', worktree_name: 'wt-a', title: 'A' },
243
+ ],
244
+ });
245
+ assert.equal(d.refuse, true);
246
+ assert.equal(d.inFlightTaskId, 72);
247
+ });
248
+
249
+ t('the fold does NOT re-break parallel worktrees (the bug this family fixed)', () => {
250
+ const d = shouldRefuseSessionClaim({
251
+ creatorSessionId: 'sess-6',
252
+ worktreeName: 'wt-b',
253
+ activeClaimsForSession: [
254
+ { claim_id: 53, task_id: 73, creator_session_id: 'sess-6', worktree_name: 'wt-a', title: 'A' },
255
+ ],
256
+ });
257
+ assert.equal(d.refuse, false);
258
+ });
259
+
186
260
  // =========================================================================
187
261
  // task 1002589 (idea 1000638) — the DB backstop's ERROR SURFACE. A claims
188
262
  // INSERT can trip TWO partial unique indexes; claimTask used to map every pg