@bongos/core 1.19.1075 → 1.19.1076

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.
Files changed (72) hide show
  1. package/.bongos-core.json +85 -100
  2. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  3. package/README.md +1 -1
  4. package/docs/adr/0031-cloud-dev-environments-for-builders.md +1 -1
  5. package/docs/adr/0035-builder-onboarding-three-paths.md +1 -1
  6. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  7. package/docs/adr/0045-devbox-desktop-app.md +1 -1
  8. package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
  9. package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
  10. package/docs/adr/0053-scoped-dev-box-session.md +1 -1
  11. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  12. package/docs/adr/0057-container-cost-ledger.md +1 -1
  13. package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +1 -1
  14. package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +1 -1
  15. package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +1 -1
  16. package/docs/adr/0072-dev-box-code-staleness-visibility.md +1 -1
  17. package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +1 -1
  18. package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
  19. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  20. package/docs/adr/0145-devbox-app-branding-driven-module.md +1 -1
  21. package/docs/adr/0148-task-scoped-box-source-access.md +2 -2
  22. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +1 -1
  23. package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
  24. package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
  25. package/docs/adr/0346-dev-boxes-are-retired.md +69 -0
  26. package/docs/adr/README.md +1 -0
  27. package/docs/architecture.md +5 -6
  28. package/docs/branding-contract.md +2 -2
  29. package/docs/canonical-permissions.md +10 -18
  30. package/docs/copy-inventory.md +3 -3
  31. package/docs/copy-registry.json +3 -3
  32. package/docs/design/gate-navigation-direction.md +1 -1
  33. package/docs/design/hall-direction-v2.md +3 -3
  34. package/docs/design/modular-architecture/00-research-report.md +1 -1
  35. package/docs/design/modular-architecture/01-architecture.md +3 -3
  36. package/docs/design/modular-architecture/02-module-map.md +1 -2
  37. package/docs/design/modular-architecture/03-builder-flows.md +22 -20
  38. package/docs/design/modular-architecture/04-module-lifecycle.md +1 -1
  39. package/docs/design/modular-architecture/README.md +4 -2
  40. package/docs/design/reviews/hall-v2/README.md +2 -2
  41. package/docs/design/vanilla-hall-ui-redesign-scope.md +4 -4
  42. package/docs/file-map.md +5 -5
  43. package/docs/handoff-template.md +1 -1
  44. package/docs/module-api-changelog.md +6 -0
  45. package/docs/modules-contract.md +11 -14
  46. package/docs/page-readings.json +63 -63
  47. package/docs/recipes/bongos-cli-release.md +1 -2
  48. package/docs/recipes/multi-builder-merge.md +1 -1
  49. package/docs/recipes/ops-gotchas.md +0 -9
  50. package/docs/recipes/self-host.md +1 -1
  51. package/docs/recipes/windows-builders.md +1 -1
  52. package/migrations/core_258_drop_dev_box_tables.sql +3 -0
  53. package/modules/hall-ui/public/palette.js +143 -3
  54. package/package-lock.json +2 -2
  55. package/package.json +1 -1
  56. package/release-notes.json +22 -0
  57. package/scripts/gds/dev-box-guard.js +165 -0
  58. package/scripts/gds/fitness-lib.js +10 -0
  59. package/scripts/gds/fitness.js +2 -10
  60. package/scripts/gds/run-unit-tests.js +4 -0
  61. package/src/bongos/module-scope-map.js +1 -1
  62. package/src/module-api.js +1 -1
  63. package/tests/core_258_box_session_claims_db.mjs +180 -0
  64. package/tests/fitness.mjs +89 -0
  65. package/tests/hall_palette.mjs +224 -11
  66. package/tests/provisioning_recommendations.mjs +1 -1
  67. package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
  68. package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
  69. package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
  70. package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
  71. package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
  72. package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
@@ -2,8 +2,7 @@
2
2
 
3
3
  How the compiled `bongos` binaries get built, published, and installed — the
4
4
  "no Node, no repo" on-ramp (task [#1248](https://example.com/builders#/task/1248), V4.R81).
5
- Mirrors the release flow the retired Dev Box app used:
6
- **CI builds, the operator publishes to a public releases repo, the server serves
5
+ The release flow: **CI builds, the operator publishes to a public releases repo, the server serves
7
6
  branded redirects.** There is no cross-repo write token in CI — publishing is a
8
7
  deliberate operator step.
9
8
 
@@ -1,6 +1,6 @@
1
1
  # Multi-builder merge safety — audit + serialization model
2
2
 
3
- > **Deploy-mode note (current state).** This recipe documents the **`laptop`** merge path (operator merges `main` + SSHes the droplet). Since the 2026-06-13 cutover `config/deploy.json` is **`mode: ci`** — the merge + deploy run **server-side** (PR auto-merge + `.github/workflows/deploy-prod.yml`), and credential-less dev boxes ship via server-mediated publish ([ADR 0055](../adr/0055-server-mediated-branch-publish.md)). The local-`main`-worktree + merge-lock model below applies only if `deploy.json` is reverted to `mode: laptop`.
3
+ > **Deploy-mode note (current state).** This recipe documents the **`laptop`** merge path (operator merges `main` + SSHes the droplet). Since the 2026-06-13 cutover `config/deploy.json` is **`mode: ci`** — the merge + deploy run **server-side** (PR auto-merge + `.github/workflows/deploy-prod.yml`), and a checkout with no GitHub push credential ships via server-mediated publish ([ADR 0055](../adr/0055-server-mediated-branch-publish.md)). The local-`main`-worktree + merge-lock model below applies only if `deploy.json` is reverted to `mode: laptop`.
4
4
 
5
5
  > **Task:** V3.R50 / [#269](https://example.com/builders#/task/269) (criterion **C6 — parallel-safe-under-load**).
6
6
  > **Question it answers:** when N>1 builders ship and merge concurrently, where do `ship.js` and `/merge-mode` assume "I am the only builder," and what serializes the shared state so they don't collide?
@@ -282,15 +282,6 @@ Inside the namespace the file lists as `---------- 1 root root 0`. The runner un
282
282
 
283
283
  ---
284
284
 
285
- ## Dev box
286
-
287
- ### `bypassPermissions` "still not available" even though the box was provisioned for it (task 2022)
288
-
289
- - **Symptom:** you reconnect to the browser terminal (or SSH) on a dev box and `claude` still stops for permission prompts, even though tasks [#1251](https://example.com/builders#/task/1251) and [#1314](https://example.com/builders#/task/1314) default every box to `bypassPermissions`.
290
- - **Root cause:** `otb-box-launch.sh` (behind `box-terminal.service`) only ever runs `claude` once — the first time it creates the tmux `otb` session. Every later connect just reattaches to that **same running process**. The `defaultMode=bypassPermissions` setting is written into `~/.claude/settings.json` by `box-source-fetch.sh`'s cron, on its own `*/10` schedule (and, on an already-provisioned box, only after that box's baked copy of the script has self-updated to even contain the fix). If the `otb` tmux session got created before that write landed — an older box, or a fresh box whose very first connect raced the first cron tick — `claude` started once in normal `auto` mode and is stuck there for the life of the box; reconnecting never re-launches it, so the fix never takes effect even once the setting is correct on disk. (Flagged as an open follow-up in [ADR 0104](../adr/0104-trust-gds-api-channel-in-auto-mode.md).)
291
- - **Fix:** `otb-box-launch.sh` now asserts `defaultMode=bypassPermissions` itself, synchronously, right before it creates a **new** `otb` tmux session — so a fresh session never depends on cron timing. This prevents the race going forward but does **not** repair a box whose `otb` session is already running in the wrong mode: for that, from an SSH/browser-terminal shell run `tmux kill-session -t otb` then reconnect (the launcher creates a fresh session and picks up the corrected setting immediately). Killing the session only drops the terminal pane — it does not touch `/workspace` or in-flight git state.
292
- - **Why it bites:** every layer *looks* fixed — `~/.claude/settings.json` has the right value, `IS_SANDBOX=1` is set — because you're inspecting the box's filesystem, not the already-running process that never re-read it.
293
-
294
285
  ## New-project scaffolds & the core pin (gap 13 · task 1002706 · ADR 0168)
295
286
 
296
287
  ### Do NOT set `PROVISION_CORE_FROM_REGISTRY=1` on the box until the core package goes public
@@ -12,7 +12,7 @@ docker compose up
12
12
 
13
13
  That builds the image, starts Postgres, applies the build-system migrations (game-module migrations are skipped — this is a Bongos-only instance), and serves the builders' hall at **http://localhost:3000**.
14
14
 
15
- - The instance is **vanilla** by default: the neutral Cloud Bongos brand, every feature module (game, art pipeline, Discord, dev boxes) **off**. `.dockerignore` keeps the host's `config/branding.json` and `config/modules.json` out of the image, so it resolves to `config/branding.neutral.json` + `config/modules.neutral.json`.
15
+ - The instance is **vanilla** by default: the neutral Cloud Bongos brand, every feature module (game, art pipeline, Discord) **off**. `.dockerignore` keeps the host's `config/branding.json` and `config/modules.json` out of the image, so it resolves to `config/branding.neutral.json` + `config/modules.neutral.json`.
16
16
  - **No OTB scope-truth is seeded.** OTB/product data (game versions, goals, done-when criteria) lives in opt-in seed migrations under `migrations/instance/` (plus a few guarded blocks in the mixed files 003/006). `scripts/migrate.sh` skips them unless `GDS_APPLY_INSTANCE_SEEDS=1`, so a fresh DB comes up with only the schema and platform-universal defaults (ranks, achievements) — no crash, no foreign-key failures, no leftover versions. Platform system principals (`bfg-system`, `discord-bot-system`) are generic and remain. See task 1704 / [ADR 0105](../adr/0105-instance-seed-migrations-out-of-core.md).
17
17
  - **No prod topology** is referenced: the database is a local container, the credentials are local demo values, and the app reaches Postgres over TCP via `PG*` env (see `docker-compose.yml`).
18
18
 
@@ -227,7 +227,7 @@ here, suspect these before assuming the platform is at fault:
227
227
  is a short path (`C:\Users\RUNNER~1\…`), `os.tmpdir()` keeps the short form while git and the
228
228
  shell answer with the long one — so two spellings of one directory compare unequal. This alone
229
229
  explained failures in `ship_preflight`, `migrate_root_resolution` and `sandbox_stage`, and the
230
- last of those was a genuine bug: the dev box would have git-synced a checkout onto itself.
230
+ last of those was a genuine bug: the since-retired dev box ([ADR 0346](../adr/0346-dev-boxes-are-retired.md)) would have git-synced a checkout onto itself.
231
231
  - **A REPO-relative path must be POSIX on every platform** — it goes into a task's `touches[]`, a
232
232
  version row, or a generated manifest. `path.join`/`path.relative` emit `\` here. Use
233
233
  `path.posix.*`, or the house spelling `.split(path.sep).join('/')`. This was the bug in
@@ -46,6 +46,9 @@ DROP TABLE IF EXISTS builder_boxes;
46
46
  DROP TABLE IF EXISTS box_intents;
47
47
  DROP TABLE IF EXISTS builder_ssh_keys;
48
48
 
49
+ -- claims.creator_session_id REFERENCES builder_sessions with no ON DELETE, so unlink box sessions first (task 1004362).
50
+ UPDATE claims SET creator_session_id = NULL
51
+ WHERE creator_session_id IN (SELECT id FROM builder_sessions WHERE source = 'box');
49
52
  DELETE FROM builder_sessions WHERE source = 'box';
50
53
  ALTER TABLE builder_sessions
51
54
  DROP CONSTRAINT IF EXISTS builder_sessions_source_check;
@@ -9,7 +9,7 @@
9
9
  // Progressive enhancement is the contract: a page that does not load palette.js
10
10
  // keeps a hidden slot and no key handler, and nothing in shell.js calls in here.
11
11
  //
12
- // The index is CLIENT-SIDE ONLY (no new server surface, task 2178 scope):
12
+ // The index has three CLIENT-SIDE layers (task 2178 scope):
13
13
  // 1. Pages — read live from the sidebar shell.js rendered, so rank + module
14
14
  // gating come for free and can never drift from the nav. A hidden nav item
15
15
  // is a hidden destination. (Cosmetic only; the server still gates each page.)
@@ -17,12 +17,33 @@
17
17
  // cannot be read from another page's DOM, so they are declared below and
18
18
  // held to the real ids by tests/hall_palette.mjs.
19
19
  // 3. Records — id syntax: `1234`, `g 12`, `b 7`, `i 42`.
20
- // Full-text search over tasks/docs is phase 2 — it needs a server surface.
20
+ // PLUS one server-backed layer (task 1003460): typing prose (3+ chars, debounced,
21
+ // per-open memoized) asks POST /api/bongos/search { source_kind: 'db' } — the
22
+ // rank/owner-scoped recall layer (ADR 0060) that already indexes every
23
+ // task/idea/blocker/doc, and had zero hall consumers before this. Content hits
24
+ // are merged in ON TOP of the synchronous local compute() — never replacing it —
25
+ // which is what keeps this progressive enhancement: a slow, failed, or killed
26
+ // endpoint just means the merge never happens, and nav-jump/id-resolution/
27
+ // page-jump (all pure, all local) are completely unaffected. Only source_refs
28
+ // this palette can actually ROUTE to are surfaced (task/idea/blocker today — see
29
+ // contentEntry) — a doc/session-log/commit hit is real prose the endpoint found,
30
+ // but the hall has no page to jump to for it, and a palette row that goes
31
+ // nowhere on Enter is worse than not showing it.
21
32
  (function () {
22
33
  'use strict';
23
34
 
24
35
  var PALETTE_ID = 'jump-palette';
25
36
  var MAX_RESULTS = 40;
37
+ var SEARCH_MIN_LEN = 3; // 1-2 chars is websearch_to_tsquery's least selective case (falls through
38
+ // to the unindexed trigram leg AND a zero-result corpusHealth() probe) —
39
+ // not worth a round trip on the palette's most common input length.
40
+ var SEARCH_DEBOUNCE_MS = 300; // paced against searchRateLimit (60/min/builder, modules/memory/routes/
41
+ // search.js) — a builder typing a phrase word-by-word still fires a
42
+ // handful of requests, not one per keystroke.
43
+ var SEARCH_LIMIT = 8;
44
+ var SEARCH_TITLE_MAX = 120; // an indexed title is write-once ledger data — cap it before it's markup.
45
+ var SEARCH_CACHE_MAX = 30; // per-open memo (cleared on open()) so retyping a prefix replays the
46
+ // last response instead of re-issuing an identical search.
26
47
 
27
48
  // Sub-destinations, grouped by the nav item that owns them. `gate` names the
28
49
  // element whose visibility decides whether the group is offered — the nav item
@@ -79,6 +100,10 @@
79
100
  var results = [];
80
101
  var cursor = 0;
81
102
  var lastFocus = null;
103
+ var searchSeq = 0; // bumped on every keystroke/close — invalidates in-flight requests
104
+ var searchTimer = null;
105
+ var searchAbort = null; // the in-flight request's AbortController, if the runtime has one
106
+ var searchCache = {}; // query (trimmed+lowercased) -> raw hits[], this OPEN only
82
107
 
83
108
  function esc(s) {
84
109
  return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
@@ -153,6 +178,116 @@
153
178
  return null;
154
179
  }
155
180
 
181
+ // V4 (docs/voice.md): a dash is banned in copy this project QUOTES, not copy it
182
+ // authors, enforced by normalising at render — never by editing the ledger. A
183
+ // task/idea/blocker title is exactly that quoted case. Same normalisation as
184
+ // modules/public-landing/public/index.html's cleanTitle() / goal-inbox.js's.
185
+ function cleanTitle(raw) {
186
+ return String(raw == null ? '' : raw)
187
+ .replace(/\s*[—–]\s*/g, ', ')
188
+ .replace(/,\s*,/g, ',')
189
+ .replace(/\s*\([^()]*\)\s*$/, '')
190
+ .replace(/[\s,]+$/, '')
191
+ .trim();
192
+ }
193
+
194
+ function truncate(s, max) {
195
+ s = String(s == null ? '' : s);
196
+ return s.length > max ? s.slice(0, max - 1).trim() + '…' : s;
197
+ }
198
+
199
+ // Map one POST /search hit to a palette entry, or null when this palette has
200
+ // nowhere to send the reader for that kind of hit (see the header note). Only
201
+ // source_kind='db' rows carry a source_ref this palette recognizes today —
202
+ // task/idea/blocker, the same three id-syntax already resolves in recordEntry.
203
+ // Scoping (rank + owner) is entirely the endpoint's (ADR 0016/0060) — this is
204
+ // presentation only, never a second filter.
205
+ function contentEntry(hit) {
206
+ if (!hit || hit.source_kind !== 'db') return null;
207
+ var ref = String(hit.source_ref || '');
208
+ var label = function (fallback) { return truncate(cleanTitle(hit.title) || fallback, SEARCH_TITLE_MAX); };
209
+ var m = ref.match(/^task:(\d+)$/);
210
+ if (m) return { label: label('Task ' + m[1]), group: 'Content', href: '/task/' + m[1] };
211
+ m = ref.match(/^idea:(\d+)$/);
212
+ if (m) return { label: label('Idea ' + m[1]), group: 'Content', href: '#/idea/' + m[1], overlay: true };
213
+ m = ref.match(/^blocker:(\d+)$/);
214
+ if (m) return { label: label('Blocker ' + m[1]), group: 'Content', href: '#/blocker/' + m[1], overlay: true };
215
+ return null;
216
+ }
217
+
218
+ // Merge raw /search hits into the rendered results, deduped by destination
219
+ // href against whatever's already showing (a local id-syntax match resolving
220
+ // the same task, or an earlier merge). MAX_RESULTS is the same ceiling the
221
+ // local pool is held to.
222
+ function mergeHits(hits) {
223
+ var seen = {};
224
+ results.forEach(function (e) { seen[e.href] = true; });
225
+ var added = [];
226
+ hits.forEach(function (hit) {
227
+ var e = contentEntry(hit);
228
+ if (e && !seen[e.href]) { seen[e.href] = true; added.push(e); }
229
+ });
230
+ if (!added.length) return;
231
+ results = results.concat(added).slice(0, MAX_RESULTS);
232
+ render();
233
+ }
234
+
235
+ // Debounced remote leg. Fires at most once per SEARCH_DEBOUNCE_MS of typing,
236
+ // and every keystroke invalidates whatever was scheduled/in-flight before it
237
+ // (searchSeq, plus actually aborting the request in flight so a superseded
238
+ // query doesn't keep FTS/trgm work running server-side) so a slow response
239
+ // for a stale query can never land after a newer one, or after the palette
240
+ // closed. A failed fetch (network error, a non-2xx, an abort, or `fetch` just
241
+ // not existing) resolves to nothing being appended — the synchronous local
242
+ // compute() already rendered, so the palette degrades to exactly today's
243
+ // behaviour, never an error state.
244
+ function scheduleSearch(query) {
245
+ if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
246
+ if (searchAbort) { searchAbort.abort(); searchAbort = null; }
247
+ searchSeq++;
248
+ var mySeq = searchSeq;
249
+ var q = String(query || '').trim();
250
+ var key = q.toLowerCase();
251
+ if (q.length < SEARCH_MIN_LEN) return;
252
+ if (Object.prototype.hasOwnProperty.call(searchCache, key)) { mergeHits(searchCache[key]); return; }
253
+ if (typeof fetch !== 'function') return;
254
+ searchTimer = setTimeout(function () { runSearch(q, key, mySeq); }, SEARCH_DEBOUNCE_MS);
255
+ }
256
+
257
+ function cacheHits(key, hits) {
258
+ // Unbounded within one open() would leak for a palette left open a long
259
+ // time; this is a per-open convenience, not a durable cache, so just drop
260
+ // the oldest entry rather than a real LRU.
261
+ var keys = Object.keys(searchCache);
262
+ if (keys.length >= SEARCH_CACHE_MAX) delete searchCache[keys[0]];
263
+ searchCache[key] = hits;
264
+ }
265
+
266
+ function runSearch(q, key, mySeq) {
267
+ var controller = typeof AbortController === 'function' ? new AbortController() : null;
268
+ searchAbort = controller;
269
+ fetch('/api/bongos/search', {
270
+ method: 'POST',
271
+ credentials: 'same-origin',
272
+ headers: { 'Content-Type': 'application/json' },
273
+ body: JSON.stringify({ q: q, limit: SEARCH_LIMIT, source_kind: 'db' }),
274
+ signal: controller ? controller.signal : undefined,
275
+ }).then(function (r) {
276
+ return r && r.ok ? r.json() : null;
277
+ }).then(function (data) {
278
+ if (searchAbort === controller) searchAbort = null;
279
+ // Stale (a newer keystroke/search ran) or the palette closed meanwhile.
280
+ if (mySeq !== searchSeq || !isOpen()) return;
281
+ if (!data || !Array.isArray(data.results)) return;
282
+ cacheHits(key, data.results);
283
+ mergeHits(data.results);
284
+ }).catch(function () {
285
+ if (searchAbort === controller) searchAbort = null;
286
+ // Fail-open: killing the endpoint (or aborting a superseded request)
287
+ // leaves the palette exactly as useful as today.
288
+ });
289
+ }
290
+
156
291
  function compute(q) {
157
292
  var query = String(q || '').trim().toLowerCase();
158
293
  var rec = recordEntry(query);
@@ -227,6 +362,7 @@
227
362
  results = compute(input.value);
228
363
  cursor = 0;
229
364
  render();
365
+ scheduleSearch(input.value);
230
366
  });
231
367
  listEl.addEventListener('click', function (ev) {
232
368
  var li = ev.target.closest ? ev.target.closest('[data-i]') : null;
@@ -249,6 +385,7 @@
249
385
  function open() {
250
386
  if (!root) build();
251
387
  if (isOpen()) return;
388
+ searchCache = {}; // a fresh open may see DB state the last one didn't
252
389
  lastFocus = document.activeElement;
253
390
  root.hidden = false;
254
391
  document.documentElement.classList.add('palette-open');
@@ -261,6 +398,9 @@
261
398
 
262
399
  function close() {
263
400
  if (!isOpen()) return;
401
+ if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
402
+ if (searchAbort) { searchAbort.abort(); searchAbort = null; }
403
+ searchSeq++; // invalidate any pending/in-flight search
264
404
  root.hidden = true;
265
405
  document.documentElement.classList.remove('palette-open');
266
406
  if (lastFocus && lastFocus.focus) lastFocus.focus();
@@ -297,5 +437,5 @@
297
437
  if (kbd) kbd.textContent = isMac ? '⌘K' : 'Ctrl K';
298
438
  }
299
439
 
300
- window.OTBPalette = { open: open, close: close, isOpen: isOpen, compute: compute };
440
+ window.OTBPalette = { open: open, close: close, isOpen: isOpen, compute: compute, contentEntry: contentEntry };
301
441
  })();
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1075",
3
+ "version": "1.19.1076",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1075",
9
+ "version": "1.19.1076",
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.1075",
3
+ "version": "1.19.1076",
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",
@@ -7827,5 +7827,27 @@
7827
7827
  "id": "1003897",
7828
7828
  "text": "Cleanup: the code, tests and settings no longer describe the dev box as if it still existed, and seven code-quality limits are tightened to today's better numbers. Nothing changes for builders."
7829
7829
  }
7830
+ ],
7831
+ "1.19.1076": [
7832
+ {
7833
+ "id": "1004363",
7834
+ "text": "Fixed the test that was still stopping new releases. It created a sample task without saying what kind of work it was, which the database no longer allows. With it fixed, the release carrying the database fix can go out, and t"
7835
+ },
7836
+ {
7837
+ "id": "1003460",
7838
+ "text": "Ctrl-K now searches the project's memory: typing prose merges in matching tasks/ideas/blockers from the existing recall search endpoint, debounced and gracefully degrading if the endpoint is ever unavailable — jumping by id, p"
7839
+ },
7840
+ {
7841
+ "id": "1003899",
7842
+ "text": "The dev box can't quietly come back: an automated check now fails the build if its code, tables, permissions or files reappear, and points whoever tripped it to the decision record that explains why."
7843
+ },
7844
+ {
7845
+ "id": "1004362",
7846
+ "text": "Fixed the database update that kept making the site upgrade roll back. It tried to delete old dev-box logins while one past task claim still pointed at one of them, and the database refused. The update now clears that pointer"
7847
+ },
7848
+ {
7849
+ "id": "1003898",
7850
+ "text": "The dev box's removal is written up in one decision record (ADR 0346), and the 20 older records that described it now point there. The live guides, the design mockups and the README no longer tell anyone to use a dev box."
7851
+ }
7830
7852
  ]
7831
7853
  }
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/dev-box-guard.js — the dev box stays retired (task 1003899, goal
3
+ // 1000120; ADR 0346).
4
+ //
5
+ // WHY. The dev box was removed across a dozen areas: a module, a third of it
6
+ // leaked into the core, four tables, a session source, three permissions, a CLI,
7
+ // a desktop app, hall pages. A removal spread that wide rots back in one careless
8
+ // import, fixture or copied migration. This is the ratchet that holds it: every
9
+ // tracked file outside the historical record is scanned for the retired names,
10
+ // and any hit fails CI with a pointer to ADR 0346, so the next builder learns WHY
11
+ // and not only THAT.
12
+ //
13
+ // WHAT COUNTS, narrowly — each rule names a thing that only ever meant the box:
14
+ // • module-key the key `dev-box` used AS A KEY: quoted ('dev-box', "dev-box",
15
+ // `dev-box`) or as a path segment (modules/dev-box/). Matched
16
+ // with fitness-lib's delimited-token matcher, so `dev-boxes`
17
+ // and an ADR filename like 0072-dev-box-code-… never hit. Prose
18
+ // ("the dev-box removal") is deliberately not a key.
19
+ // • identifier the dropped tables and columns: builder_boxes, box_intents,
20
+ // box_events, builder_ssh_keys, box_blocked*.
21
+ // • permission a quoted permission key of the retired `box.*.*` family.
22
+ // • session-source a session `source` compared or assigned to 'box'.
23
+ // • path a tracked file under scripts/gds/box*, infra/box-*,
24
+ // modules/dev-box/ or .devcontainer/.
25
+ // "box" alone is NOT a rule: it also means a server (the co-hosting box, box.env).
26
+
27
+ 'use strict';
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const { execFileSync } = require('node:child_process');
32
+ const { keyTokenRe } = require('./fitness-lib.js');
33
+
34
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
35
+ const ADR = 'docs/adr/0346-dev-boxes-are-retired.md';
36
+ const NAME = 'the dev box stays retired (ADR 0346)';
37
+
38
+ // THE EXEMPTION LIST — the one place a file may still name the box. Each entry is
39
+ // `path-or-prefix` → why. A prefix ends in '/'. Keep reasons about the file's
40
+ // PURPOSE, never about what the tree happens to contain today.
41
+ const DEV_BOX_EXEMPT = new Map([
42
+ // The historical record — superseded, never rewritten (ADR README "Superseding").
43
+ ['docs/adr/', 'historical record: ADRs are superseded by ADR 0346, never rewritten'],
44
+ ['docs/session-logs/', 'historical record: dated session logs'],
45
+ ['docs/audits/', 'historical record: dated audit reports'],
46
+ ['docs/governance/', 'historical record: dated governance reports'],
47
+ ['limitations/', 'historical record: shipped-version scope archives'],
48
+ ['docs/module-api-changelog.md', 'history ledger of doorway releases; entries are appended, never rewritten'],
49
+ // Applied migrations: they created the box objects and, last, dropped them. Named one
50
+ // by one so a NEW migration is still checked. The column drop of task 1004360 joins
51
+ // this list when it lands.
52
+ ['migrations/067_builder_boxes.sql', 'applied migration (created the box tables)'],
53
+ ['migrations/069_builder_ssh_keys.sql', 'applied migration'],
54
+ ['migrations/070_box_intents.sql', 'applied migration'],
55
+ ['migrations/071_box_first_provision_approval.sql', 'applied migration'],
56
+ ['migrations/073_box_terminal_access.sql', 'applied migration'],
57
+ ['migrations/078_box_claude_active.sql', 'applied migration'],
58
+ ['migrations/079_box_first_connected_at.sql', 'applied migration'],
59
+ ['migrations/090_box_scoped_sessions.sql', 'applied migration (added the box session source)'],
60
+ ['migrations/102_remove_box_first_provision_approval.sql', 'applied migration'],
61
+ ['migrations/104_builder_box_blocked.sql', 'applied migration (added the box_blocked columns)'],
62
+ ['migrations/105_box_host_keys.sql', 'applied migration'],
63
+ ['migrations/121_box_code_version.sql', 'applied migration'],
64
+ ['migrations/core_236_rank_floor_backfill.sql', 'applied migration (names the key in an example)'],
65
+ ['migrations/core_237_box_widen_paths.sql', 'applied migration'],
66
+ ['migrations/core_238_box_unattended_cap.sql', 'applied migration'],
67
+ ['migrations/core_248_module_enablement_overrides.sql', 'applied migration (names the key in an example)'],
68
+ ['migrations/core_258_drop_dev_box_tables.sql', 'the migration that drops the box tables and session source by name (task 1003895)'],
69
+ ['modules/government/migrations/governance_002_seed_and_backfill.sql', 'applied migration (seeded the box.* keys)'],
70
+ ['modules/government/migrations/governance_007_delegate_archon_ops_to_metic.sql', 'applied migration'],
71
+ ['modules/government/migrations/government_019_retire_box_permissions.sql', 'the migration that revokes the box.* keys by name (task 1003894)'],
72
+ ['modules/provisioning/migrations/provisioning_014_module_selection.sql', 'applied migration (names the key in an example)'],
73
+ ['modules/provisioning/migrations/provisioning_001_tables.sql', 'applied migration (its comments cite the box queue it was modelled on)'],
74
+ // Retirement guards: code and tests whose JOB is to name the retired thing.
75
+ ['src/modules.js', 'RETIRED_MODULE_KEYS: an instance config still saying "dev-box": true boots with a warning'],
76
+ ['tests/modules.mjs', 'proves the retired-key tolerance (and that a typo like "dev-boxes" still throws)'],
77
+ ['tests/government_session_scope.mjs', 'proves a leftover source=box session is refused'],
78
+ ['tests/auth_page_gate.mjs', 'proves a leftover source=box session is refused a page'],
79
+ ['tests/terms_acceptance.mjs', 'proves a leftover source=box session cannot mint an acceptance'],
80
+ ['tests/government_seed.mjs', 'proves government_019 revoked the box.* keys from every rank'],
81
+ ['tests/core_258_box_session_claims_db.mjs', 'proves core_258 applies over a claim created from a box session (task 1004362)'],
82
+ ['tests/government_protected_surfaces.mjs', 'RETIRED_SINCE_R101: retired protected paths may leave the registry only once gone'],
83
+ ['scripts/gds/dev-box-guard.js', 'this guard (it defines the rules)'],
84
+ // Captured data and seed history that name real records.
85
+ ['scripts/gds/seed-', 'one-shot task seeders: their subject is the historical tasks they filed'],
86
+ ['scripts/hall-preview/fixtures/', 'captured API responses: the ledger still holds these task and goal rows'],
87
+ ['.bongos-core.json', 'a 1.0.0 seed manifest from July (commit b6ee5d29) that this repo never regenerates'],
88
+ ]);
89
+
90
+ const TEXT_EXT = /\.(js|mjs|cjs|json|md|sql|sh|html|css|yml|yaml|txt|toml)$/;
91
+ const PATH_RULE = /^(scripts\/gds\/box|infra\/box-|modules\/dev-box\/|\.devcontainer\/)/;
92
+
93
+ function exemptionFor(rel) {
94
+ for (const [entry, why] of DEV_BOX_EXEMPT) {
95
+ if (entry.endsWith('/') || entry.endsWith('-') ? rel.startsWith(entry) : rel === entry) return why;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ // The key used as a key: fitness-lib's delimited match, with a quote, backtick or
101
+ // slash on both sides (a colon may close it, as in a JSON key).
102
+ // Compiled once for the whole scan; a global regex is stateful, so lastIndex is reset per line.
103
+ const KEY_RE = new RegExp(keyTokenRe('dev-box').source, 'g');
104
+ function namesModuleKey(line) {
105
+ const re = KEY_RE;
106
+ re.lastIndex = 0;
107
+ let m;
108
+ while ((m = re.exec(line))) {
109
+ if (/['"`/]/.test(m[1] || '') && /['"`/:]/.test(m[2] || '')) { re.lastIndex = 0; return true; }
110
+ re.lastIndex = m.index + 1;
111
+ }
112
+ return false;
113
+ }
114
+
115
+ const LINE_RULES = [
116
+ ['module-key', namesModuleKey],
117
+ ['identifier', (l) => /\b(builder_boxes|box_intents|box_events|builder_ssh_keys|box_blocked\w*)\b/.test(l)],
118
+ ['permission', (l) => /['"`]box\.[a-z_]+\.[a-z_]+['"`]/.test(l)],
119
+ ['session-source', (l) => /\bsource\b\s*(?:===?|!==?|:|=|IN\s*\()[^\n]{0,40}['"]box['"]/i.test(l)],
120
+ ];
121
+
122
+ // Pure: scan one file's text. Returns [{ rule, line }].
123
+ function scanText(text) {
124
+ const hits = [];
125
+ String(text == null ? '' : text).split('\n').forEach((l, i) => {
126
+ for (const [rule, test] of LINE_RULES) if (test(l)) hits.push({ rule, line: i + 1 });
127
+ });
128
+ return hits;
129
+ }
130
+
131
+ function trackedFiles(root = REPO_ROOT) {
132
+ return execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 30000 })
133
+ .split('\n').filter(Boolean);
134
+ }
135
+
136
+ // The fitness check. `files` and `read` are injectable so the tests can plant a
137
+ // violation without touching the tree.
138
+ function checkNoDevBox({ files, read } = {}) {
139
+ const list = files || trackedFiles();
140
+ const readFile = read || ((rel) => fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'));
141
+ const violations = [];
142
+ for (const rel of list) {
143
+ if (exemptionFor(rel)) continue;
144
+ if (PATH_RULE.test(rel)) violations.push(`${rel} — path: a retired dev-box location (ADR 0346)`);
145
+ if (!TEXT_EXT.test(rel)) continue;
146
+ let text;
147
+ try { text = readFile(rel); } catch { continue; }
148
+ for (const { rule, line } of scanText(text)) violations.push(`${rel}:${line} — ${rule}: a retired dev-box name (ADR 0346)`);
149
+ }
150
+ const note = `The dev box was removed on purpose (${ADR}). ${list.length} tracked file(s) scanned for its module key, tables/columns, box.* permissions, box session source and paths. `
151
+ + `A hit means use what replaced it (a local checkout; server-mediated publish for a checkout with no push credential). Only a retirement guard or a historical record may name the box: add it to DEV_BOX_EXEMPT in scripts/gds/dev-box-guard.js, with its reason.`;
152
+ if (list.length === 0) {
153
+ return { name: NAME, ok: false, hardFail: true, violations: ['scan defect — enumerated 0 tracked files; a broken enumeration, not a clean result.'], warnings: [], note };
154
+ }
155
+ return { name: NAME, ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note };
156
+ }
157
+
158
+ module.exports = { checkNoDevBox, scanText, namesModuleKey, exemptionFor, DEV_BOX_EXEMPT, PATH_RULE };
159
+
160
+ if (require.main === module) {
161
+ const r = checkNoDevBox();
162
+ console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.name}\n${r.note}`);
163
+ for (const v of r.violations) console.log(` ✗ ${v}`);
164
+ process.exitCode = r.ok ? 0 : 1;
165
+ }
@@ -70,8 +70,18 @@ function walkJsAllRoots(subpath, roots = MODULE_ROOTS) {
70
70
  return out;
71
71
  }
72
72
 
73
+ // Match a module key as a DELIMITED token so `art-pipeline` hits in `'art-pipeline'` /
74
+ // `mount('art-pipeline')` but never inside `art-pipelines` or `xart-pipeline`. Hyphens are
75
+ // non-word chars, so the boundary guards against word-chars AND hyphens on both
76
+ // sides (no lookbehind — portable on every node the unit gate runs). Moved here from
77
+ // fitness.js (task 1003899) so dev-box-guard.js matches the retired key the same way.
78
+ function keyTokenRe(key) {
79
+ const esc = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
+ return new RegExp(`(^|[^\\w-])${esc}($|[^\\w-])`);
81
+ }
73
82
 
74
83
  module.exports = {
84
+ keyTokenRe,
75
85
  CORE_ROOT,
76
86
  INSTANCE_ROOT,
77
87
  MODULE_ROOTS,
@@ -33,7 +33,7 @@ const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-con
33
33
  // live beside it — the shared roots/walkers and three check clusters. They are
34
34
  // IMPORTED (not registered by path) so the CHECKS array and module.exports below
35
35
  // stay byte-identical to the single-file version every test was written against.
36
- const { CORE_ROOT, INSTANCE_ROOT, MODULE_ROOTS, REPO_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
36
+ const { CORE_ROOT, INSTANCE_ROOT, MODULE_ROOTS, REPO_ROOT, keyTokenRe, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
37
37
  const { IDENTITY_SCAN_ALLOWED, IDENTITY_SCAN_EXCLUDE, IDENTITY_SCAN_EXTRA_FILES, IDENTITY_SCAN_EXTS, IDENTITY_SCAN_ROOTS, IDENTITY_SCAN_SKIP_DIRS, PROMPT_IDENTITY_LITERALS, checkDesignContract, checkKernelPromptsNoHardcodedIdentity, checkNeutralConfigsNoHostIdentity, collectIdentityFiles, scanPromptIdentity, stripCommentsForIdentity } = require('./fitness-checks-identity.js');
38
38
  const { WRITE_VALIDATION_BASELINE, WRITE_VALIDATION_EXEMPT, checkWriteRoutesValidated, scanUnvalidatedWriteRoutes } = require('./fitness-checks-write-validation.js');
39
39
  const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, PRELAUNCH_SURFACE, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkPrelaunchVocabulary, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
@@ -683,15 +683,6 @@ function moduleDir(key) {
683
683
  return path.join(CORE_ROOT, 'modules', key);
684
684
  }
685
685
 
686
- // Match a module key as a DELIMITED token so `art-pipeline` hits in `'art-pipeline'` /
687
- // `mount('art-pipeline')` but never inside `art-pipelines` or `xart-pipeline`. Hyphens are
688
- // non-word chars, so the boundary guards against word-chars AND hyphens on both
689
- // sides (no lookbehind — portable on every node the unit gate runs).
690
- function keyTokenRe(key) {
691
- const esc = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
692
- return new RegExp(`(^|[^\\w-])${esc}($|[^\\w-])`);
693
- }
694
-
695
686
  // Pure: scan kernel source text for any hardcoded module key, skipping comment
696
687
  // lines. Returns violation strings (one per hit). Exported for the negative test
697
688
  // (mirrors scanScopeMapSource / scanDeterministicBody).
@@ -1350,6 +1341,7 @@ const CHECKS = [
1350
1341
  // Task 1003206 carved this file from 2,004 lines to ~1,350, so that toll is paid
1351
1342
  // and the check gets its own line back.
1352
1343
  require('./version-literal-guard.js').checkNoVersionLiterals,
1344
+ require('./dev-box-guard.js').checkNoDevBox, // task 1003899 / ADR 0346: the retired dev box stays out (its exemption list lives in that file)
1353
1345
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1354
1346
  require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
1355
1347
  require('./baseline-staleness.js').checkBaselineFloors, // Check 26 — task 1003210: a shrink-only baseline that never shrinks is unmanaged debt; a floor exempting one must say why
@@ -118,6 +118,10 @@ const INTEGRATION = new Set([
118
118
  // (Prose here avoids single-quoted tokens: the pairing guard below parses this
119
119
  // set out of the source, so a quoted word in a comment reads as an entry.)
120
120
  'copy_desk_flags_db',
121
+ // task 1004362: core_258 against a real Postgres holding a claim created from a
122
+ // dev box session. The claims foreign key that failed the live upgrade is a
123
+ // schema fact only a transaction engine enforces. Always rolled back.
124
+ 'core_258_box_session_claims_db',
121
125
  'discord_autojoin', 'discord_oauth',
122
126
  // task 1003000 (IC6): the three ideator credit streams against a real Postgres.
123
127
  // The three DB-free sibling suites stub the client and SIMULATE the
@@ -431,7 +431,7 @@ const MODULE_GLOBS = {
431
431
  'scripts/gds/scope-roster-guard.js',
432
432
  'scripts/gds/route-shadow-guard.js', 'scripts/gds/routine-schedule.js',
433
433
  'scripts/gds/seam-wiring-guard.js', 'scripts/gds/skill-preflight.js',
434
- 'scripts/gds/test-path-guard.js', 'scripts/gds/version-literal-guard.js',
434
+ 'scripts/gds/test-path-guard.js', 'scripts/gds/version-literal-guard.js', 'scripts/gds/dev-box-guard.js',
435
435
 
436
436
  // ADOPTED (task 1004111): the enforcement surfaces this module already IS.
437
437
  // install-git-hooks.js + scripts/gds/hooks/ are the pre-push hook beside the
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.19.1075'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.19.1076'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');