@bongos/core 1.19.70 → 1.19.71

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.70",
6
- "core_contract": "1.19.70",
7
- "source_commit": "b92ae4a4d4458ff81409d2aff12bfc88bd0ef50d",
5
+ "core_version": "1.19.71",
6
+ "core_contract": "1.19.71",
7
+ "source_commit": "4eba29162c98469e50170c8942bb995192ac0378",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-12T15:26:39.115Z",
9
+ "built_at": "2026-08-12T15:37:06.175Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 293,
12
+ "docs_redacted": 294,
13
13
  "agent_docs_stubbed": 21,
14
14
  "functional_verbatim": 1223,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1537,
20
- "tree_sha256": "56c1de962c6965257fb63c72e0201b2a0652833acf9ade499ed57c96e9650adb",
19
+ "file_count": 1538,
20
+ "tree_sha256": "7d0cdc9a7a536092bcbcf6f173a3c55c18773c1878e1613394b768dd54d6228f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1309,10 +1309,15 @@
1309
1309
  "mode": "0000644",
1310
1310
  "sha256": "bb187a29953ba5c316977f09fe86e26ca200693b23cf5c41ece93e9dd24f337e"
1311
1311
  },
1312
+ {
1313
+ "path": "docs/adr/0168-scaffold-packs-installed-core.md",
1314
+ "mode": "0000644",
1315
+ "sha256": "8bdad4b4333cbb7b38f66e4dddab08675dfc0a431d21c0be6a31ca4afd8c498f"
1316
+ },
1312
1317
  {
1313
1318
  "path": "docs/adr/README.md",
1314
1319
  "mode": "0000644",
1315
- "sha256": "b0387cdc43a67c4b9834691155d1fa9454930cb70b0660bb55a0f7e819c7ec90"
1320
+ "sha256": "a96302a9849a2d7225b4a47ce0d7a7ac5f66d6849e80781f9a8e5aa32eba51d4"
1316
1321
  },
1317
1322
  {
1318
1323
  "path": "docs/api-reference.md",
@@ -1562,7 +1567,7 @@
1562
1567
  {
1563
1568
  "path": "docs/recipes/ops-gotchas.md",
1564
1569
  "mode": "0000644",
1565
- "sha256": "0256bd5d59f4a34886efbcabf3f12c520a0c2092e2035b5c969e4b1769c99604"
1570
+ "sha256": "ddbb6d4e0569a6773b648a6fe97abb2e2e04c6e36a5f3100af0cd995688a4810"
1566
1571
  },
1567
1572
  {
1568
1573
  "path": "docs/recipes/overnight-builder.md",
@@ -4047,12 +4052,12 @@
4047
4052
  {
4048
4053
  "path": "package-lock.json",
4049
4054
  "mode": "0000644",
4050
- "sha256": "68b3bc7974e4f90d5291ba89dc3451d1cde4db4cff174ba3ed1347e4599c77da"
4055
+ "sha256": "9fed4a4c8c227a418e6cc695f1a9a7ddea366f1e0ddd95f788d7b64dacd03ff2"
4051
4056
  },
4052
4057
  {
4053
4058
  "path": "package.json",
4054
4059
  "mode": "0000644",
4055
- "sha256": "505474194374951dfc13ffca64f10c469da2782e1756214c3efb3a60b8151b68"
4060
+ "sha256": "af4d4ea3a0dd190dda1d0f04e2d9450b395cc4026405833e970f2ac81772fe5d"
4056
4061
  },
4057
4062
  {
4058
4063
  "path": "public-docs/index.html",
@@ -5027,7 +5032,7 @@
5027
5032
  {
5028
5033
  "path": "scripts/gds/provision.js",
5029
5034
  "mode": "0000644",
5030
- "sha256": "e4108117dbe8dd6493ba41ed053fe8648424ca127b1c26505aa6f6702b2dd94c"
5035
+ "sha256": "7ad384495370312a57f7c1f0dab4770888c83afd08b89a8dc27aebae408abfd1"
5031
5036
  },
5032
5037
  {
5033
5038
  "path": "scripts/gds/publish-manifest.js",
@@ -5677,7 +5682,7 @@
5677
5682
  {
5678
5683
  "path": "src/module-api.js",
5679
5684
  "mode": "0000644",
5680
- "sha256": "993ad9f8c4be17c840767dff8c4e092a2ea55c5cbe73a09a53371e31cccfa720"
5685
+ "sha256": "110f17337515aa6751a7c881bd20c0694f38686b5b62c29da43ac2677be2b5af"
5681
5686
  },
5682
5687
  {
5683
5688
  "path": "src/module-loader/loader.js",
@@ -7162,7 +7167,7 @@
7162
7167
  {
7163
7168
  "path": "tests/provision.mjs",
7164
7169
  "mode": "0000644",
7165
- "sha256": "bd8e111c2d5614f79086c5f8298cec6ffdcb7ab57b94565457520d85df0c24a8"
7170
+ "sha256": "edb6c5da9ec32536d50f7cb648b81b026b7926f73f891feb0906fc7343b5b11d"
7166
7171
  },
7167
7172
  {
7168
7173
  "path": "tests/provisioning_domain_attach.mjs",
@@ -0,0 +1,34 @@
1
+ # 0168 — The scaffold vendors the hub's running core (npm pack of the installed package); registry-default deferred to the go-public step
2
+
3
+ - **Status:** Accepted — built in task 1002706.
4
+ - **Date:** 2026-08-12
5
+ - **Deciders:** jaxri (Archon) — autonomous session; the go-public trigger stays the owner's (idea 1000700, ADR 0134).
6
+ - **Task:** task 1002706 · **Goal:** 1000059 · gap 13 of [`docs/audits/project-creation-gap-list.md`](../audits/project-creation-gap-list.md) · ties: task 1002621, task 1002275, idea 1000671.
7
+
8
+ ## Context
9
+
10
+ Two facts collided at the scaffold's core-install step:
11
+
12
+ 1. **New projects were born frozen-stale.** A fresh scaffold pinned `file:vendor/bongos-core-1.19.2.tgz` — ~60 patches behind — because `installedCoreTarball()` (task 1002275) reuses whatever tarball the control plane's own `file:` pin names, and the mothership's pin had sat at 1.19.2. ADR 0136 channels can't move a `file:` pin, so children had no upgrade path.
13
+ 2. **The registry cutover quietly broke the fallback.** Task 1002621 moved cloudbongos.com's own pin to the registry (`"@bongos/core": "1.19.56"` — verified on the box 2026-08-12). With no `file:` pin, `installedCoreTarball()` returns null and the scaffold falls into the `package-core.js` source rebuild — which **aborts on a consumer instance** (REPO_ROOT inside gitignored `node_modules` → empty core set → the child repo is left empty, the exact task-1002275 failure resurrected). The next greenfield scaffold on the box would have failed outright.
14
+
15
+ The task brief proposed "make registry the scaffold default now that `@cloudbongos/core` is public (verify unauthenticated `npm view` works)". **That verification fails**: both `@cloudbongos/core` and `@bongos/core` 404 anonymously on `registry.npmjs.org` (2026-08-12) — they are private scoped packages (ADR 0134), public only at the launch go-public step. A registry-default scaffold today would hand every new owner a caret pin + env-fed `.npmrc` they cannot resolve without a paid-org token — a project **born uninstallable**, strictly worse than born-stale.
16
+
17
+ ## Decision
18
+
19
+ **Three-rung core-resolution ladder** in the standalone scaffold (freshest-correct first), replacing the two-rung one:
20
+
21
+ 1. `file:`-pinned control plane → reuse its pinned tarball (task 1002275, unchanged).
22
+ 2. **NEW — registry-pinned consumer control plane → `npm pack` the core it has installed** (`installedCorePackDir()` finds `node_modules/@bongos/core`, the scaffold packs it to a temp dir and `--vendor-core`s the result). The child pins the **same, current core the hub runs** — 1.19.56 today, not 1.19.2 — and stays `file:`-pinned, so its owner needs no npm token.
23
+ 3. Genuine source checkout → `package-core.js` rebuild (unchanged).
24
+
25
+ The opt-in registry path (`PROVISION_CORE_FROM_REGISTRY=1` + `NPM_TOKEN`, fail-soft) is untouched. **Registry as the scaffold default is deferred to the ADR 0134 go-public step** — the same owner decision idea 1000700 tracks (public repo / public npm / wait for the ADR 0099 mirror). When the package goes public, flipping the default becomes a small follow-up: drop the token gate, keep fail-soft.
26
+
27
+ **Ops leg (gap 13's "flip the box env"):** nothing to flip now — setting `PROVISION_CORE_FROM_REGISTRY=1` on the box today would birth token-locked children (the private-pin trap above). Child freshness rides two things instead: this ladder (children inherit the hub's core at birth) and the hub's own pin staying current (task 1002708 — the upgrade lane must commit + push the pin).
28
+
29
+ ## Consequences
30
+
31
+ - Born-stale collapses from "frozen at 1.19.2 forever" to "as fresh as the hub" — and the hub's freshness is an explicitly tracked lane (task 1002708).
32
+ - The post-cutover empty-repo regression is closed before it fired in production.
33
+ - `npm pack` naming (`<scope>-<name>-<version>.tgz`) is derived from the installed package's own `name`, so both scope aliases work.
34
+ - Rejected: registry-default now (verified-dead premise; uninstallable children), dropping `NPM_TOKEN` (the package is private — tokenless `npm ci` fails closed by design, ADR 0134), vendoring from the npm cacache (undocumented layout; `npm pack` is the supported path).
@@ -239,6 +239,7 @@ This keeps the decision history honest and traceable.
239
239
  | 0164 | [**The implausible-pass guard reads substance + evidence; the Metic+ honor-system residual is explicit** ([task 1002658](https://cloudbongos.com/builders#/task/1002658); grader-hardening goal 1000056). The guard fired only on `issues.length===0` — one fabricated `{}` defeated it, its magnitude inputs are client-supplied, and every in-sample firing (eval 2-for-2, tasks 1002643 ×2, 1002663) was a false positive on an honest unanimous zero-issue panel PASS. Redesign: count only SUBSTANTIVE issues (known severity + non-empty summary — padding trips the guard regardless), and waive the zero-issue pass when `signals.workers` carries the full &ge;3-worker panel shape, stamped `implausible_pass_waived` for /grade-audit. The residual is named, not hidden: every grade-time input is client-authored, so plausibility at grade time is honest-mistake hygiene; the binding authorities on the real diff are land-time (CI unit, gate-review, main audit) — Metic+ grade-time honesty is accepted honor-system per this ADR. The ADR-0042 CI grade-gate (land it vs record permanent deferral) goes to the owner as a blocker, not decided here. No approval step added or removed (ADR 0162).](<redacted>.md) | GDS / grading |
240
240
  | 0165 | [**Instances are hookless by default: no scaffolded `.husky/`; local hooks stay an instance-owned opt-in** ([task 1002641](https://cloudbongos.com/builders#/task/1002641); idea 1000655). Task 1002608 delivered the ADR 0043 Layer 1 pre-push gate for the CORE repo, but nothing materializes `.husky/` on a scaffolded standalone instance (`publish-manifest.js` excludes it by design). Decision: that is the accepted posture, not a gap. Sub-Metic instance builders hold no push credential at all (server-mediated publish, ADR 0053/0055) — a client pre-push hook guards a push that cannot happen; a client hook is not a security boundary (ADR 0016); the core cannot maintain hooks it deliberately does not ship (scaffolded hooks would be born orphans — the audit-authorship rot lesson, task 1002640); and ADR 0162 puts gates in CI, rank-consistent and hands-free. Secrets hooks follow the same rule (the PR-time CI secret scan enforces). The opt-in stays real and instance-owned: an instance commits its own `.husky/` and the core's `install-git-hooks.js` (already run by `/builder-setup`) activates it. Rejected: scaffolding wrappers at init, an `--with-hooks` flag (speculative), treating hooklessness as a regression. Revisit if an instance model has builders pushing directly with their own credentials.](<redacted>.md) | provisioning / trust boundary |
241
241
  | 0166 | [**Module maintenance & deprecation policy: upkeep is a declared `maintenance` block, and `bongos upgrade` surfaces rot in DISABLED modules too** ([task 1001773](https://cloudbongos.com/builders#/task/1001773)). Implements [ADR 0107](<redacted>.md) §7, which named the catalog-rot problem and deferred the mechanics. `module.json` gains an optional-in-schema, required-to-upstream `maintenance` block — `status` one of `core-maintained` / `maintained` / `deprecated` / `orphaned`, plus `since` / `removeAfter` / `successor` / `note`. Each status owes something the validator enforces: `maintained` requires a named top-level `maintainer` (an unnamed owner IS the orphaning failure), a sunset status requires a deprecation path, and `removeAfter` is rejected on a non-sunset status. `orphaned` is deliberately first-class — a policy that only permits happy statuses gets satisfied by lying. The load-bearing fix: accepted modules ship `default:false` (§6), so every rotted catalog entry is DISABLED by construction — and `preflightModules()` returned only `blocking`, discarding `warnings`, so a real `bongos upgrade --to X` reported `"all enabled modules satisfy the target"` while saying nothing about them. It now returns + prints `warnings`/`deprecations`/`expired` with the maintainer NAMED, before the blocking check returns (so a refused bump reports them too). Enabled mismatches still refuse; disabled ones still never block — report, don't block, because blocking an instance's bump on a catalog entry it never opted into makes the shared catalog a liability. `bongos module check`/`submit` refuse an undeclared posture or an already-sunset module; `GET /modules` + the hall Modules tab show an `upkeep:` chip (incl. `undeclared`); all 17 bundled modules stamped `core-maintained` with a unit test holding the line. Rejected: blocking on disabled rot, a required manifest field (breaks every existing manifest), DB-tracked upkeep (must travel with the module, ADR 0107 §2), auto-deprecating on incompatibility (a fact is not a decision), a separate MAINTAINERS file.](<redacted>.md) | modules / upstreaming |
242
+ | 0168 | [**The scaffold vendors the hub's RUNNING core (`npm pack` of the installed package); registry-default deferred to the go-public step** ([task 1002706](https://cloudbongos.com/builders#/task/1002706); gap 13 of the project-creation audit). Two collided facts: children were born pinned to a frozen `file:vendor/bongos-core-1.19.2.tgz` (installedCoreTarball reuses the hub's own file: pin — task 1002275 — and the hub's pin sat at 1.19.2), and the task-1002621 registry cutover (hub pin now `"@bongos/core": "1.19.56"`, verified on the box) made installedCoreTarball return null — dropping the scaffold into the package-core.js source rebuild that ABORTS on a consumer instance (empty core set → empty child repo, the 1002275 failure resurrected). Fix: a three-rung ladder — file:-pin reuse → NEW `installedCorePackDir()` + `npm pack` of the hub's installed core (child pins the SAME, CURRENT core, stays file:-pinned, owner needs no token) → source rebuild. The brief's "make registry the default now that the package is public" was verified FALSE (both npm scopes 404 anonymously; ADR 0134 keeps them private until go-public) — a registry-default child would be born UNINSTALLABLE for a tokenless owner. Registry-default is deferred to the go-public step (idea 1000700); the gap-13 "flip the box env" ops leg is explicitly a no-op until then; child freshness rides this ladder + the hub-pin lane (task 1002708). Rejected: registry-default now, dropping NPM_TOKEN, vendoring from the npm cacache.](<redacted>.md) | provisioning / distribution |
242
243
  | 0167 | [**The /create gate trust link defaults to the AGPL-3.0 license text, not the npm package page** ([task 1002705](https://cloudbongos.com/builders#/task/1002705); gap 1 of the project-creation audit). The gate's "open source" link pointed at a nonexistent repo (404); the task brief's suggested default — the npm package page — is ALSO dead for the gate's actual audience (signed-out visitors): `@cloudbongos/core` and `@bongos/core` both 404 anonymously (private scopes, ADR 0134), and the ADR 0099 public mirror stays dormant until ~2026-11. The license text is the one live, instance-neutral artifact behind the claim today. The browsable-code upgrade is the owner's filed choice (idea 1000700): public repo now (forfeits the ADR 0065 §4 redaction window) / public npm package / wait for the mirror. Rejected: the npm link (verified dead), flipping the repo public (not a builder's call), plain unlinked text (less information, same cost).](<redacted>.md) | public landing / governance |
243
244
 
244
245
  > ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
@@ -225,6 +225,16 @@ Organized by topic, not chronology. New entries go in the matching section; crea
225
225
  - **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.
226
226
  - **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.
227
227
 
228
+ ## New-project scaffolds & the core pin (gap 13 · task 1002706 · ADR 0168)
229
+
230
+ ### Do NOT set `PROVISION_CORE_FROM_REGISTRY=1` on the box until the core package goes public
231
+
232
+ - **Symptom (why you'd be tempted):** the project-creation audit (gap 13) found new projects born on a frozen `file:vendor/bongos-core-1.19.2.tgz`, and the registry-install mode looks like the fix.
233
+ - **Root cause:** `@bongos/core` (and the old `@cloudbongos/core` scope) is a **private** npm package until the ADR 0134 go-public step — both 404 anonymously (verified 2026-08-12). A registry-scaffolded child gets a caret pin + env-fed `.npmrc` that **its owner cannot resolve without a paid-org `NPM_TOKEN`** — a project born uninstallable, strictly worse than born-stale.
234
+ - **Fix (shipped, task 1002706):** nothing to flip. The scaffold now inherits the hub's **running** core automatically: a `file:`-pinned control plane reuses its pinned tarball (task 1002275), a **registry-pinned** control plane (the post-1002621 mothership) `npm pack`s its installed `node_modules/@bongos/core`, and only a true source checkout rebuilds via `package-core.js`. Children stay `file:`-pinned and current-at-birth.
235
+ - **When the package goes public** (idea 1000700 / ADR 0134): flipping registry-default becomes a small follow-up — set `PROVISION_CORE_FROM_REGISTRY=1` in the runner env (token no longer required for public installs; the fail-soft vendor path stays).
236
+ - **Housekeeping:** the mothership's `vendor/` still holds a pile of stale `bongos-core-1.18.x` tarballs no pin references — safe to prune on the next box pass. Child freshness depends on the hub's own pin staying current — that lane is task 1002708 (the upgrade must commit + push the pin, never npm-install in place).
237
+
228
238
  ## When to add a new entry here
229
239
 
230
240
  Anytime you spend more than ~10 minutes diagnosing something that turned out to have a non-obvious cause, add a section. Include symptom, root cause, fix, and why-it-bites. The cost of a 5-minute write-up is much less than the cost of the next session re-discovering the same thing.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.70",
3
+ "version": "1.19.71",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.70",
9
+ "version": "1.19.71",
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.70",
3
+ "version": "1.19.71",
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",
@@ -399,6 +399,39 @@ function installedCoreTarball({ instanceRoot, fsImpl = fs } = {}) {
399
399
  return null;
400
400
  }
401
401
 
402
+ // The REGISTRY-pinned sibling of installedCoreTarball (task 1002706). After a control
403
+ // plane moves its own core pin off the vendored tarball onto the npm registry (the
404
+ // cloudbongos.com cutover, task 1002621), its package.json holds a semver pin — no
405
+ // `file:` — so installedCoreTarball returns null and the scaffold would fall into the
406
+ // package-core.js source rebuild, which ABORTS on a consumer instance (the empty-set
407
+ // abort of task 1002275, resurrected by the cutover). The installed package itself is
408
+ // still the exact core this hub runs: return its node_modules dir + version + the
409
+ // `npm pack` tarball name so the scaffold can pack it into a vendorable .tgz. The child
410
+ // stays file:-pinned — its owner needs no npm token (the registry package is PRIVATE
411
+ // until the ADR 0134 go-public step). Returns { dir, version, tarballName } or null
412
+ // (a file: pin, an uninstalled dep, or a source checkout). fs reads only → dry-run safe.
413
+ function installedCorePackDir({ instanceRoot, fsImpl = fs } = {}) {
414
+ const root = instanceRoot || process.env.BONGOS_INSTANCE_ROOT || process.cwd();
415
+ let pkg;
416
+ try { pkg = JSON.parse(fsImpl.readFileSync(path.join(root, 'package.json'), 'utf8')); }
417
+ catch { return null; } // no/unreadable package.json → a source checkout
418
+ const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
419
+ for (const name of ['@bongos/core', '@cloudbongos/core']) {
420
+ const spec = String(deps[name] || '').trim();
421
+ if (!spec || spec.startsWith('file:')) continue; // file: pins belong to installedCoreTarball
422
+ const dir = path.join(root, 'node_modules', ...name.split('/'));
423
+ try {
424
+ const installed = JSON.parse(fsImpl.readFileSync(path.join(dir, 'package.json'), 'utf8'));
425
+ const version = installed && installed.version ? String(installed.version) : null;
426
+ if (!version) continue;
427
+ // npm pack names a scoped tarball <scope>-<name>-<version>.tgz (no @, / → -).
428
+ const base = String(installed.name || name).replace(/^@/, '').replace(/\//g, '-');
429
+ return { dir, version, tarballName: `${base}-${version}.tgz` };
430
+ } catch { /* pinned but not installed → fall through */ }
431
+ }
432
+ return null;
433
+ }
434
+
402
435
  // The onboard mode a provisioning row carries (migration provisioning_004, task 1002562).
403
436
  // 'adopt' means target_ref is a repo the owner ALREADY has, so the scaffold must LAYER
404
437
  // onto its history. Anything else — including a row written before the column existed, or
@@ -606,20 +639,38 @@ async function scaffoldStandaloneRepo(inst, deps) {
606
639
  log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
607
640
  exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --registry-core`);
608
641
  } else {
609
- // Vendored tarball. PREFER a pre-built pinned tarball this control plane already has
610
- // (a consumer instance's own `file:` core pin) — rebuilding via package-core.js aborts
611
- // with an EMPTY core set when this runner IS a consumer instance (REPO_ROOT is inside
612
- // gitignored node_modules), which left new repos empty (task 1002275). Only rebuild
613
- // from source when there is no such pin (a genuine core source checkout).
642
+ // Vendored tarball — a three-rung resolution ladder, freshest-correct first:
643
+ // 1. a `file:`-pinned control plane reuses its own pinned tarball (task 1002275 —
644
+ // rebuilding via package-core.js aborts with an EMPTY core set when this runner
645
+ // IS a consumer instance, leaving new repos empty);
646
+ // 2. a REGISTRY-pinned consumer (the post-1002621 mothership shape) packs the core
647
+ // it has INSTALLED — the child pins the SAME, CURRENT core the hub runs, ending
648
+ // the frozen-old-tarball births (task 1002706, gap 13) without requiring the
649
+ // child's owner to hold a token for the still-private registry package;
650
+ // 3. a genuine core source checkout rebuilds fresh via package-core.js.
614
651
  let tgz = installedCoreTarball({ instanceRoot: deps.instanceRoot });
652
+ let packDest = null; // set on the pack rung — cleaned up once init has vendored the tgz
615
653
  if (tgz) {
616
654
  log(` [scaffold] core install: reusing the pinned core tarball this control plane runs (${path.basename(tgz)}) — no source rebuild (task 1002275)`);
617
655
  } else {
618
- log(` [scaffold] core install: building a fresh vendored tarball from source (package-core.js)`);
619
- exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
620
- tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
656
+ const packSrc = installedCorePackDir({ instanceRoot: deps.instanceRoot });
657
+ if (packSrc) {
658
+ // Keyed by VERSION, not slug: no request-derived value enters this command, and
659
+ // two concurrent scaffolds of the same core pack identical bytes — collision-safe.
660
+ packDest = path.join(os.tmpdir(), `bongos-core-pack-${packSrc.version}`);
661
+ log(` [scaffold] core install: packing the registry-installed core this control plane runs (v${packSrc.version}) — no source rebuild (task 1002706)`);
662
+ exec(`mkdir -p ${packDest}`);
663
+ exec(`npm pack ${packSrc.dir} --pack-destination ${packDest}`);
664
+ tgz = path.join(packDest, packSrc.tarballName);
665
+ } else {
666
+ log(` [scaffold] core install: building a fresh vendored tarball from source (package-core.js)`);
667
+ exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
668
+ tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
669
+ }
621
670
  }
622
671
  exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
672
+ // init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
673
+ if (packDest) exec(`rm -rf ${packDest}`);
623
674
  }
624
675
 
625
676
  // 4. commit + push to the instance's own main (keep node_modules out).
@@ -1988,7 +2039,7 @@ module.exports = {
1988
2039
  backupUnitName, backupServicePath, backupTimerPath, backupScriptPath, backupService, backupTimer,
1989
2040
  standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
1990
2041
  // standalone scaffold (ADR 0108 / 0125, task 2055; consumer-core fix task 1002275)
1991
- parseTargetRef, instanceRepoRemote, instanceInitSpec, installedCoreTarball, scaffoldStandaloneRepo, coreVersionSafe,
2042
+ parseTargetRef, instanceRepoRemote, instanceInitSpec, installedCoreTarball, installedCorePackDir, scaffoldStandaloneRepo, coreVersionSafe,
1992
2043
  // adopt vs greenfield onboarding (ADR 0121, task 1002562)
1993
2044
  onboardMode, alreadyScaffolded,
1994
2045
  zoneForDomain, dnsUpsertEnv, classifyZoneScope, checkDnsTokenScope,
package/src/module-api.js CHANGED
@@ -567,8 +567,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
567
567
  // landed since 1.19.68 with no explicit bump. run 31608503813. (task 1002620)
568
568
  // 1.19.70 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
569
569
  // landed since 1.19.69 with no explicit bump. run 31612248859. (task 1002620)
570
+ // 1.19.71 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
571
+ // landed since 1.19.70 with no explicit bump. run 31613220077. (task 1002620)
570
572
  // ---------------------------------------------------------------------------
571
- const CORE_VERSION = '1.19.70'; // CI auto-patch carrier (ADR 0161); see changelog above
573
+ const CORE_VERSION = '1.19.71'; // CI auto-patch carrier (ADR 0161); see changelog above
572
574
 
573
575
  // A namespaced logger so a module's log lines are attributable + consistent.
574
576
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -489,7 +489,8 @@ t('installedCoreTarball: reuses a consumer instance file: core pin; null on a so
489
489
  rmSync(join(inst, 'vendor', 'bongos-core-1.17.4.tgz'));
490
490
  assert.equal(P.installedCoreTarball({ instanceRoot: inst }), null, 'a dangling file: pin resolves to null');
491
491
 
492
- // source checkout: a caret range (no file: pin) → null → the caller rebuilds via package-core.js
492
+ // a caret range (no file: pin) → null HERE — the registry-consumer case is
493
+ // installedCorePackDir's (task 1002706); only a true source checkout rebuilds
493
494
  const src = mkdtempSync(join(tmpdir(), 'cptgz-source-'));
494
495
  writeFileSync(join(src, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '^1.17.4' } }));
495
496
  assert.equal(P.installedCoreTarball({ instanceRoot: src }), null, 'a source checkout has no file: pin');
@@ -535,6 +536,80 @@ await ta('scaffoldStandaloneRepo: on a consumer control plane it REUSES the pinn
535
536
  }
536
537
  });
537
538
 
539
+ // ── registry-pinned consumer control plane (task 1002706) ─────────────────────────
540
+ // After the mothership's registry cutover (task 1002621) its package.json pins
541
+ // "@bongos/core": "1.19.x" — NO file: pin — so installedCoreTarball returns null and,
542
+ // before this fix, the scaffold fell into the package-core.js source rebuild, which
543
+ // ABORTS on a consumer instance (empty core set → empty child repo — the 1002275
544
+ // failure resurrected). Pack the INSTALLED core instead: the child pins the SAME,
545
+ // CURRENT core the hub runs (also kills the frozen-1.19.2 born-stale bug, gap 13).
546
+ t('installedCorePackDir: a registry-pinned consumer with an installed core → its node_modules dir; file:/uninstalled/bare → null', () => {
547
+ const reg = mkdtempSync(join(tmpdir(), 'cpdir-reg-'));
548
+ mkdirSync(join(reg, 'node_modules', '@bongos', 'core'), { recursive: true });
549
+ writeFileSync(join(reg, 'node_modules', '@bongos', 'core', 'package.json'), JSON.stringify({ name: '@bongos/core', version: '1.19.56' }));
550
+ writeFileSync(join(reg, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '1.19.56' } }));
551
+ const hit = P.installedCorePackDir({ instanceRoot: reg });
552
+ assert.ok(hit, 'a registry-pinned consumer resolves');
553
+ assert.equal(hit.dir, join(reg, 'node_modules', '@bongos', 'core'));
554
+ assert.equal(hit.version, '1.19.56');
555
+ assert.equal(hit.tarballName, 'bongos-core-1.19.56.tgz', 'npm pack naming for the scoped package');
556
+
557
+ // a file: pin belongs to installedCoreTarball, never this path
558
+ const filePinned = mkdtempSync(join(tmpdir(), 'cpdir-file-'));
559
+ writeFileSync(join(filePinned, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': 'file:vendor/bongos-core-1.17.4.tgz' } }));
560
+ assert.equal(P.installedCorePackDir({ instanceRoot: filePinned }), null, 'file: pin → null');
561
+
562
+ // registry pin but the dep is not installed → null → the source rebuild stays the fallback
563
+ const uninstalled = mkdtempSync(join(tmpdir(), 'cpdir-unin-'));
564
+ writeFileSync(join(uninstalled, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '^1.19.0' } }));
565
+ assert.equal(P.installedCorePackDir({ instanceRoot: uninstalled }), null, 'uninstalled → null');
566
+
567
+ // no package.json at all → null
568
+ const bare2 = mkdtempSync(join(tmpdir(), 'cpdir-bare-'));
569
+ assert.equal(P.installedCorePackDir({ instanceRoot: bare2 }), null, 'no package.json → null');
570
+
571
+ // the @cloudbongos/core alias is honored (matches installedCoreTarball + core-node.sh),
572
+ // and the tarball name follows the INSTALLED package's own name
573
+ const alt2 = mkdtempSync(join(tmpdir(), 'cpdir-alt-'));
574
+ mkdirSync(join(alt2, 'node_modules', '@cloudbongos', 'core'), { recursive: true });
575
+ writeFileSync(join(alt2, 'node_modules', '@cloudbongos', 'core', 'package.json'), JSON.stringify({ name: '@cloudbongos/core', version: '2.0.0' }));
576
+ writeFileSync(join(alt2, 'package.json'), JSON.stringify({ dependencies: { '@cloudbongos/core': '^2.0.0' } }));
577
+ assert.equal(P.installedCorePackDir({ instanceRoot: alt2 }).tarballName, 'cloudbongos-core-2.0.0.tgz');
578
+
579
+ [reg, filePinned, uninstalled, bare2, alt2].forEach((d) => rmSync(d, { recursive: true, force: true }));
580
+ });
581
+
582
+ await ta('scaffoldStandaloneRepo: a REGISTRY-pinned consumer control plane PACKS its installed core — current version, no source rebuild (task 1002706)', async () => {
583
+ const cp = mkdtempSync(join(tmpdir(), 'cpdir-cp-'));
584
+ mkdirSync(join(cp, 'node_modules', '@bongos', 'core'), { recursive: true });
585
+ writeFileSync(join(cp, 'node_modules', '@bongos', 'core', 'package.json'), JSON.stringify({ name: '@bongos/core', version: '1.19.56' }));
586
+ writeFileSync(join(cp, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '1.19.56' } }));
587
+ const base = mkdtempSync(join(tmpdir(), 'cpdir-standalone-')); // FRESH base → the scaffold proceeds
588
+ const origBase = P.CONFIG.standaloneBase;
589
+ P.CONFIG.standaloneBase = base;
590
+ try {
591
+ const { deps, cmds } = recorder({ apply: true });
592
+ await P.scaffoldStandaloneRepo(
593
+ { slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
594
+ { ...deps, instanceRoot: cp },
595
+ );
596
+ const joined = cmds.join('\n');
597
+ assert.ok(!/package-core\.js/.test(joined), 'does NOT rebuild from source (the empty-set abort on a consumer)');
598
+ assert.match(joined, /npm pack /, 'packs the installed core');
599
+ assert.match(joined, /--vendor-core \S*bongos-core-1\.19\.56\.tgz/, 'vendors the CURRENT core the hub runs into the child');
600
+ assert.ok(!/--registry-core/.test(joined), 'the child stays file:-pinned — its owner needs no npm token');
601
+ // the temp pack dir is version-keyed (no request-derived slug in the command) and
602
+ // cleaned up once init has vendored the tarball into the child
603
+ assert.match(joined, /mkdir -p \S*bongos-core-pack-1\.19\.56/, 'pack dir is keyed by core version');
604
+ const initAt = joined.indexOf('--vendor-core');
605
+ const rmAt = joined.indexOf('rm -rf');
606
+ assert.ok(rmAt > initAt && /rm -rf \S*bongos-core-pack-1\.19\.56/.test(joined), 'temp pack dir removed AFTER init consumed the tarball');
607
+ } finally {
608
+ P.CONFIG.standaloneBase = origBase;
609
+ [cp, base].forEach((d) => rmSync(d, { recursive: true, force: true }));
610
+ }
611
+ });
612
+
538
613
  // ── scaffold push via the owner OAuth token (task 1002317) ────────────────────────
539
614
  // Deploy keys are per-repo; a shared key can't write to a NEW project repo. Push with the
540
615
  // owner's token (which created the repo) instead — WITHOUT leaking it into a logged command.