@bongos/core 1.19.943 → 1.19.945

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.943",
6
- "core_contract": "1.19.943",
7
- "source_commit": "d2864572d1b1f1112a574071d4fe288920a71216",
5
+ "core_version": "1.19.945",
6
+ "core_contract": "1.19.945",
7
+ "source_commit": "069e65e6a01a57f09cf32de375376fb396a30dc4",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-21T22:14:04.050Z",
9
+ "built_at": "2026-09-21T23:41:12.424Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 511,
12
+ "docs_redacted": 512,
13
13
  "agent_docs_stubbed": 26,
14
14
  "functional_verbatim": 2340,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2877,
20
- "tree_sha256": "475596e7ac9620819058973e021b0c26d5122429253e9206320f31130a67c709",
19
+ "file_count": 2878,
20
+ "tree_sha256": "658ce325bf77e617c3b8b44277b6b53c3a76935b9de049db21f05062abb53779",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1027,7 +1027,7 @@
1027
1027
  {
1028
1028
  "path": "docs/adr/0111-instance-hosting-provisioning-module.md",
1029
1029
  "mode": "0000644",
1030
- "sha256": "a0da85222fe36f16d16b1f52a7d1e50b5aa794942cee6dfa5a69ab53300e6f6e"
1030
+ "sha256": "d216d4cfb0f269afa00af7963b3fe16d97596a6407b26859c7fd3126c7fc518c"
1031
1031
  },
1032
1032
  {
1033
1033
  "path": "docs/adr/0112-private-goals-invitations-join-requests.md",
@@ -1192,7 +1192,7 @@
1192
1192
  {
1193
1193
  "path": "docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md",
1194
1194
  "mode": "0000644",
1195
- "sha256": "02a9ba781c3db1c78b7aff23a3d7feb9fcca80bd49c291891af635d88288126f"
1195
+ "sha256": "74f14a90b88d585a4b50247a7dd54a01accc229e7f64a875c9491612e3d0dd3c"
1196
1196
  },
1197
1197
  {
1198
1198
  "path": "docs/adr/0146-configurable-reward-mode-cost-plus-only.md",
@@ -1917,7 +1917,7 @@
1917
1917
  {
1918
1918
  "path": "docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md",
1919
1919
  "mode": "0000644",
1920
- "sha256": "7ccc21098aaaf80df6d07d7fa31199b9283a26e522d55ee7a9e472987721d097"
1920
+ "sha256": "4e1631b94e83af33a8ea9ebf51657e0a5fcb0b9da30a41d443e7caa8f6b3c35b"
1921
1921
  },
1922
1922
  {
1923
1923
  "path": "docs/adr/0286-a-gated-project-s-manifest-tells-a-stranger-how-to-sign-in-and-nothing-else.md",
@@ -2104,10 +2104,15 @@
2104
2104
  "mode": "0000644",
2105
2105
  "sha256": "6a4ad58720701e760068d5ce1761733790beef0a142aa31a08782d029cc7a390"
2106
2106
  },
2107
+ {
2108
+ "path": "docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md",
2109
+ "mode": "0000644",
2110
+ "sha256": "8fa6ef943e3fc0855d15f83baefaabf810994b80da2ecdbbcc28d752ed0498f3"
2111
+ },
2107
2112
  {
2108
2113
  "path": "docs/adr/README.md",
2109
2114
  "mode": "0000644",
2110
- "sha256": "7c66ecbd1940f3cdd830f0fb4acf65c41178807a431bb8fab382e4b8a0dbc8ae"
2115
+ "sha256": "eef598f074ff3e22a4748b9092eb83d04235205c34e66db6bbe76547f050843d"
2111
2116
  },
2112
2117
  {
2113
2118
  "path": "docs/api-reference.md",
@@ -2677,7 +2682,7 @@
2677
2682
  {
2678
2683
  "path": "docs/module-api-changelog.md",
2679
2684
  "mode": "0000644",
2680
- "sha256": "441e7d11f2b340623b122c51c788d0ff1d0610ff1c4296db3344c8702c8fb541"
2685
+ "sha256": "b19c89c223392cbb28efd08e1b296125a846e7d9f18260b71793378992d42178"
2681
2686
  },
2682
2687
  {
2683
2688
  "path": "docs/modules-contract.md",
@@ -8002,12 +8007,12 @@
8002
8007
  {
8003
8008
  "path": "package-lock.json",
8004
8009
  "mode": "0000644",
8005
- "sha256": "2ff2f30a70ad8facea46291fa01e89889194edb8fa02e904bda7b8343754bbfd"
8010
+ "sha256": "bd902078abd41ebb698133bdce5862123fa89d7740794a50721c04f9621cc9fc"
8006
8011
  },
8007
8012
  {
8008
8013
  "path": "package.json",
8009
8014
  "mode": "0000644",
8010
- "sha256": "35b084d56b257e0df6c6a738d94c305a6aa8419471c7b9fe32f41afef640f5fd"
8015
+ "sha256": "9c1945ef497e4f0068ababa55bcd863ef336446951f4cab9eecbb36788dddf05"
8011
8016
  },
8012
8017
  {
8013
8018
  "path": "public-docs/index.html",
@@ -8097,7 +8102,7 @@
8097
8102
  {
8098
8103
  "path": "scripts/gds/agents-sync.js",
8099
8104
  "mode": "0000644",
8100
- "sha256": "7a639c73f8d2f4f523899ab737641174ca7b5ed5409427238d7fc397cdba65ed"
8105
+ "sha256": "29f3d1521c04165a891a8055f005cb51ca019c51c556063e55167675051a43af"
8101
8106
  },
8102
8107
  {
8103
8108
  "path": "scripts/gds/api-alias-caller-scan.js",
@@ -9617,7 +9622,7 @@
9617
9622
  {
9618
9623
  "path": "scripts/migrate.sh",
9619
9624
  "mode": "0000644",
9620
- "sha256": "3a35826ec753908fb9aec9c5a4b5c37e71e38ba0d00c1d1f30387087be509d97"
9625
+ "sha256": "1ba289b008ef4118cfd464ab5eb24ad4b694981942680220f115913691645e08"
9621
9626
  },
9622
9627
  {
9623
9628
  "path": "scripts/migrate_cost_ledger.sh",
@@ -10012,7 +10017,7 @@
10012
10017
  {
10013
10018
  "path": "src/module-api.js",
10014
10019
  "mode": "0000644",
10015
- "sha256": "cd5bcc86c8d6ecb21b11c6dd9df73de2b63e0759edf781eeee2e6d36f6452aad"
10020
+ "sha256": "704e7c44feb33354f257ee42164701791d33c548c2daf07ed35bdd1d77bc7f9f"
10016
10021
  },
10017
10022
  {
10018
10023
  "path": "src/module-loader/catalog.js",
@@ -10187,7 +10192,7 @@
10187
10192
  {
10188
10193
  "path": "tests/agents_sync.mjs",
10189
10194
  "mode": "0000644",
10190
- "sha256": "1184d82ba51d3dd3f2bf952292b71defd4b6ea51fba5e3800429cfed1450fe31"
10195
+ "sha256": "daa2d58b9a248222a54305c3fa41d30e73c8ea8cd65db8a14e31d30dfe0ca555"
10191
10196
  },
10192
10197
  {
10193
10198
  "path": "tests/agents_validate.mjs",
@@ -1,6 +1,7 @@
1
1
  # ADR 0111 — Instance hosting + domains as a Cloud Bongos module: the `provisioning` module boundary + the self-host / managed split
2
2
 
3
- - **Status:** Accepted (boundary + split decided with the owner; build is task-pending — see §Consequences for the seeded tasks)
3
+ - **Status:** Accepted, **superseded in part by [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)** (2026-09-21) — the module boundary and the enqueue-only trust boundary STAND; the DigitalOcean droplet and `co-tenant` shapes it provisions do not. Customer projects move to a managed platform; DigitalOcean hosts hubs only.
4
+ - **Status (original):** Accepted (boundary + split decided with the owner; build is task-pending — see §Consequences for the seeded tasks)
4
5
  - **Date:** 2026-07-03
5
6
  - **Track:** internal (a portable Cloud Bongos platform capability)
6
7
  - **Task:** [#1485](https://example.com/builders#/task/1485) (BONGOS-V1, goal 26 — *Cloud Bongos productization and self-host*) — "spec the module boundary + the self-host vs managed split **before** tasking."
@@ -1,5 +1,6 @@
1
1
  # 0145 — A free "own-repo" hosted project tier needs domain separation + per-project isolation (not a bare cloudbongos.com subdomain)
2
2
 
3
+ - **Status:** **Superseded in part by [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)** (2026-09-21). Three of this ADR's decisions are replaced: the free tier becomes a **capped trial**, not all-free; customer projects leave the shared box for a managed platform, so **container isolation on our own box is no longer the plan**; and **Finding 1 is closed by removing its precondition** — with no customer code on a `cloudbongos.com` subdomain there is no tenant to receive the hub session cookie, so the sign-in hardening in the build sequence below is no longer a blocking prerequisite (it remains worth doing on its own merits). The two security FINDINGS below stand as analysis and are the reason the new direction is safe.
3
4
  - **Status:** Accepted — owner decided 2026-07-15: **keep `cloudbongos.com`** (no separate domain), **container** isolation, **all-free** for now. Choosing the literal same domain makes hardening core sign-in a required prerequisite (see "Decisions" + "Required build sequence"). Build unblocked ([task 1002251](https://cloudbongos.com/builders#/task/1002251)); blocker 1000080 resolved.
4
5
  - **Date:** 2026-07-15
5
6
  - **Deciders:** Lars (owner/Archon) chose the direction after a security review; Claude ran the review + designed.
@@ -1,6 +1,6 @@
1
1
  # ADR 0285 — A shared box holds about twelve projects per GB, and memory is the wall
2
2
 
3
- - **Status:** accepted
3
+ - **Status:** accepted; **no longer load-bearing as of [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)** (2026-09-21). The measurements below are correct and still describe the box — it simply stops holding customer projects, so the ceiling no longer gates project creation and the queued resize ([task 1003923](https://cloudbongos.com/builders#/task/1003923)) is held rather than spent.
4
4
  - **Date:** 2026-09-15
5
5
  - **Task:** [1003877](https://cloudbongos.com/builders#/task/1003877) (goal 1000106), a spike. Informs [1003923](https://cloudbongos.com/builders#/task/1003923) (the resize) and [1001949](https://cloudbongos.com/builders#/task/1001949) (managed billing).
6
6
 
@@ -0,0 +1,96 @@
1
+ # ADR 0323 — Hosting is three shapes, and we are not the landlord
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-21
5
+ - **Task:** [task 1004140](https://cloudbongos.com/builders#/task/1004140) (goal 1000106 — working area 1, Project creation)
6
+ - **Deciders:** Lars (Archon) — every numbered decision below is the owner's; Claude ran the audit and the platform comparison.
7
+ - **Gates:** [task 1004141](https://cloudbongos.com/builders#/task/1004141) (the rename) · [task 1004142](https://cloudbongos.com/builders#/task/1004142) (the migration) · [task 1004062](https://cloudbongos.com/builders#/task/1004062) (the env manifest)
8
+ - **Supersedes in part:** [ADR 0111](0111-instance-hosting-provisioning-module.md) · [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) · [ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md)
9
+ - **Related:** [ADR 0002](0002-digitalocean-over-hetzner.md) (why DigitalOcean) · [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) (the pull this ADR makes load-bearing for `byo-host`) · [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) (the seam whose second implementation this makes unnecessary)
10
+
11
+ ## Context
12
+
13
+ Five active projects run as `hosting_shape='standalone'` on one `s-1vcpu-2gb` DigitalOcean droplet, beside the control plane itself. Each is a Node process and a Postgres database, about 80 MB warm; the box holds fifteen ([ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md)) at roughly $0.80 per project per month. It is the cheapest arrangement available, and standing it up cost us `scripts/gds/provision*.js` — about 4,600 lines that write systemd units, create unix accounts, render Caddy snippets, run `createdb`, install per-instance backup timers, and count memory against a ceiling.
14
+
15
+ That is the cost nobody priced: we are a landlord. Every one of those legs is infrastructure we maintain so that someone else's code can run on our machine.
16
+
17
+ Three further facts made the arrangement worse than its price suggested:
18
+
19
+ - **Customer code sits on our hub's registrable domain.** [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) Finding 1 established that the hub's session cookie, scoped `.cloudbongos.com`, is sent by the browser to every `*.cloudbongos.com` host — including a tenant's. It required the sign-in hardening to land *before* any tenant went on a subdomain. Two tenants are on subdomains today, one of them not the platform owner's, and the hardening has not shipped.
20
+ - **`dedicated` was written and never used.** A per-project droplet is 15–20× the shared cost, and no project has ever run on one.
21
+ - **The self-host container path kept finding new floors.** [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) made supervision a seam and named three remaining blockers; two shipped, and each existed only because we were building a container host of our own.
22
+
23
+ The owner's direction, settled over 2026-09-20/21, reframed the question. The platform should not host other people's applications at all: it should build on what others maintain, and let the owner keep the deployment platform they already use.
24
+
25
+ ## Decision
26
+
27
+ ### 1. Three owner-facing shapes
28
+
29
+ | Shape | Meaning |
30
+ |---|---|
31
+ | `byo-host` | The owner runs the instance: their machine, their VPS, their own platform account. **New.** |
32
+ | `cloud-host` | We stand it up for them on a managed platform. Rename of `standalone`. |
33
+ | `dedicated-cloud-host` | The same path on a larger plan. Rename of `dedicated`. |
34
+
35
+ `control-plane` is unchanged and stays unrequestable — it is the hub, not a product option.
36
+
37
+ **`co-tenant` is retired, not renamed.** It means *a process on our box, from our checkout* — the most platform-owned shape there is, and the exact opposite of bring-your-own-host. Reusing the name would make every existing comment and ADR reference lie. One row carries it and it is `torn_down`.
38
+
39
+ `dedicated-cloud-host` is a **plan size, not a second code path**. The DigitalOcean droplet and cloud-init implementation that sat behind `dedicated` becomes dead code.
40
+
41
+ ### 2. DigitalOcean hosts hubs only
42
+
43
+ No customer project is deployed to it. The control plane stays where it is; the projects leave.
44
+
45
+ This closes [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) Finding 1 by removing its precondition rather than by hardening the cookie: with no customer code on a `cloudbongos.com` subdomain, there is no tenant to receive the hub session. The cookie hardening remains worth doing on its own merits and is no longer load-bearing.
46
+
47
+ ### 3. Render is the default platform for a `cloud-host`
48
+
49
+ Render's Postgres carries point-in-time recovery and automated backups on every paid tier. Roughly $14 per project per month (web service plus database).
50
+
51
+ Rejected: **Fly.io**, whose managed Postgres starts around $38/mo — the compute is cheaper and the database is not. **Railway**, whose Postgres templates are explicitly unmanaged: you tune and back them up, which is precisely the responsibility this decision exists to shed.
52
+
53
+ ### 4. Cloudflare is the front door, not the compute
54
+
55
+ DNS, TLS, and — when `byo-host` needs to reach a machine behind a router — Tunnels, for which a working client already exists at `scripts/gds/cf-tunnel.js`.
56
+
57
+ **Not the hall's runtime.** Cloudflare Workers is a V8-isolate runtime, not Node; Express and `pg` would have to be rewritten. Cloudflare Containers is built for bursty workloads that sleep between requests, and the hall runs background sweeps and a live event stream that do not survive sleeping. Its Postgres would be external in either case.
58
+
59
+ ### 5. Bongos does not integrate with the customer's deployment platform
60
+
61
+ Their application already deploys itself: Vercel and Netlify watch the repository. Bongos writes code and pushes it. There is no adapter layer, and an adapter layer would solve a problem that does not exist.
62
+
63
+ This is what makes "bring your preferred platform" nearly free to support. The platform we do not talk to is the platform we do not have to add.
64
+
65
+ ### 6. The deployment platform is the system of record for secrets; Bongos stores names, never values
66
+
67
+ A project declares its environment variables to Bongos as a **manifest** — name, purpose, environment, required or optional — so a builder writing code knows what the surface is. The values live where the running application reads them, in a store that already encrypts, audits and rotates them.
68
+
69
+ Bongos holds no application secret, no platform API token, and runs no "is it set?" probe. Adding any of those makes the platform the custodian of other companies' credentials, which is the responsibility this ADR is spending its whole budget to avoid. See [task 1004062](https://cloudbongos.com/builders#/task/1004062).
70
+
71
+ For `byo-host` there is no platform to ask, and the manifest is simply marked unverifiable. That keeps *Bongos holds no values* true everywhere, with no exception to reason about later.
72
+
73
+ ### 7. The free tier is a capped trial
74
+
75
+ At ~$14 per project, an indefinite subsidised free tier is not viable. This replaces [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md)'s "all-free for now". Implementing the cap is its own task; it cannot be metered before the migration.
76
+
77
+ ## Consequences
78
+
79
+ - **This wave is mostly a deletion.** Systemd units, per-instance unix accounts, Caddy snippets, `createdb`, backup timers and the capacity ceiling are things Render provides. Not all of `provision*.js` goes — the repository scaffold, the GitHub App manifest handshake and the core-upgrade path serve the hub and the `byo-host` scaffold too. The migration task removes what is genuinely orphaned, after the projects have moved, and not before.
80
+
81
+ - **Per-project cost rises about 17×, from $0.80 to ~$14.** That is the price of not being a landlord, and it is why decision 7 exists. It also makes metering ([task 1001949](https://cloudbongos.com/builders#/task/1001949)) and the spend rollup ([task 1001621](https://cloudbongos.com/builders#/task/1001621)) real work rather than deferred work.
82
+
83
+ - **[ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md) stays true and stops being load-bearing.** Its measurements were correct and its ceiling still describes the box; the box just stops holding projects. The queued resize to 2 vCPU / 4 GB is therefore **not** to be spent ([task 1003923](https://cloudbongos.com/builders#/task/1003923), held).
84
+
85
+ - **[ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md)'s seam is not extended.** It correctly said it was "two implementations wide, not N". Render supervises its own processes, so the seam neither grows a third backend nor needs one.
86
+
87
+ - **The hub becomes the single point of failure it always was, now visibly.** With projects elsewhere, everything that can be lost with the droplet is the control plane's own database — which makes offsite backups ([task 1002214](https://cloudbongos.com/builders#/task/1002214)) the highest-consequence open item in the goal.
88
+
89
+ - **`byo-host` starts cheap and stays honest.** It reuses the pull that already exists ([ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md)): the owner supplies an origin and the hub reads the instance's own public manifest. Nothing is pushed to it, so it is excluded from the settings-apply and restart paths for the same reason `dedicated` was.
90
+
91
+ ## Alternatives considered
92
+
93
+ - **Move to AWS or GCP.** Rejected. Their pricing is earned by consuming their managed services; a bare Linux instance from either is 2–3× DigitalOcean for the same memory, plus a console, an IAM model and a billing surface to learn. It adds maintenance where the goal is to remove it.
94
+ - **Keep the shared box and fix the cookie instead.** Cheaper, and it closes the security finding. Rejected because it leaves the 4,600-line landlord runner in place and answers none of the other three questions.
95
+ - **Build a deployment-platform adapter layer (Vercel/Netlify/Render).** Rejected as speculative pluggability, the thing [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) explicitly warned against. Their applications already deploy themselves.
96
+ - **Bongos as the secrets store, pushing values to each platform via its API.** Rejected: it makes us the custodian of other companies' credentials for convenience that the manifest already delivers.
@@ -414,3 +414,4 @@ This keeps the decision history honest and traceable.
414
414
  | 0318 | [**Listing a file for task affinity is not claiming to own it** ([task 1004110](https://cloudbongos.com/builders#/task/1004110), goal 1000095 — BONGOS-V2). The case [ADR 0314](0314-module-protection-is-per-path-not-per-module.md) named and left open, closed by neither of the two routes it predicted. `builder-settings` derived `protected` — Archon-only to scope a goal to — on exactly one glob, `src/bongos/routes/me.js`, caught by the blanket `src/bongos/routes/` prefix. **That glob was never an ownership claim:** `MODULE_GLOBS` listed it for task AFFINITY and said in the same comment that the `GET /me` aggregator stays a thin core facade, and `isProtected()` cannot tell the two apart. **Both predicted routes were measured and rejected.** Narrowing the blanket prefix: all eighteen files under `src/bongos/routes/` are protected by that prefix AND NOTHING ELSE, so narrowing unwalls `auth.js`, `builders.js`, `backup.js` and `core-update.js` together — now pinned as a tripwire test rather than left as a caution. Moving `me.js` into `modules/builder-settings/`: the aggregator resolves TEN module ports (`lifecycle`, `reward`, `onboarding`, `government`, `specialities`, `grade`, `box`, `art` ×2, `discord`) and `builder-settings` is one of them, so hosting it inside one module means resolving nine foreign ports from the inside — the `core -> module` edge [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 forbids. Decision: **ownership follows what the file is** — `me.js` moves to `kernel`, which already owns `src/bongos/routes/security.js`. This is 0314 one step on: owning the command is not owning the authority, and **being listed next to a module is not owning it either**. **Rejected: a general affinity tier** in `MODULE_GLOBS` mirroring `isCommandPath` — the audit does not support it. Three other modules (`kernel`, `devsecops`, `methodology`) are also protected only by paths outside their own directory, but in each the foreign path genuinely IS the domain; `builder-settings` was the only module protected by a path its own source called core, so a new tier would be a new place for a hole to hide, built for a population of one. `protectedModules()` goes 9 -> 8. **The wall that matters does not move:** a sub-Metic builder touching `/me` is still refused by `checkPermissionPaths`, asserted directly rather than assumed. The sixteen still-unowned route files remain [task 1004116](https://cloudbongos.com/builders#/task/1004116), whose option (a) this de-risks.](0318-listing-a-file-for-task-affinity-is-not-owning-it.md) | permissions / module scope |
415
415
  | 0321 | [**Brightness is a payout-weighted share, and a removal's targets are rows** ([task 1004099](https://cloudbongos.com/builders#/task/1004099), goal 1000095 — BONGOS-V2). Size is an idea's total ideator payout and never falls; brightness is how much of that work is still alive, and the pair is what lets a thought whose version was torn out draw LARGE AND DARK rather than merging with one that never mattered. Two things had to be settled. **The removal facts were stored in the wrong direction**: [ADR 0319](0319-a-removal-verdict-is-a-row-beside-the-question.md) gave a confirmed removal a row carrying the verdict and `ship_task_id` — the task whose merge DELETED the files — and deliberately left out the tasks whose work was removed, saying brightness would design its own storage when it needed one. Those are different rows under different ideas, so reading one as the other dims the wrong star while looking like a working query. `<redacted>` stores `(proposal_task_id, removed_task_id, paths)` at PROPOSAL time, from the SAME derivation objects the proposal body renders as prose — one derivation, two renderings, so they cannot disagree and `removal-proposals.js` keeps its refusal to parse a body back. Proposal time and not verdict time because the attribution comes from capped, rate-limited GitHub commit-history reads that only exist while the cascade runs. NOT a runner fan-out ([ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md)): the rule still files one task; this records a side fact about it. `ON CONFLICT DO NOTHING`, never `DO UPDATE`, since a degraded second walk finds FEWER paths and must not erase a good first read — and the writer runs on the cascade's SKIPPED row too, so a re-ship repairs what a derivation outage missed. **The share is weighted by PAYOUT, not by task count**, or deleting a one-line fix dims a star as hard as deleting the feature; weighting by what each hop earned also makes 'large and dead' exact, and makes the two channels a share of ONE number rather than two unrelated measurements. Basis is paid + estimated to match size, which required `priceDescentForIdea`/`ideaSizes` to return a per-task `byTask` summing EXACTLY to `credits` — including ledger payments for tasks the descent no longer reaches, which `idea-size.js` keeps in `credits` so size cannot fall and which would otherwise make the parts sum to less than the whole. Three edges, each silent: an idea that earned nothing is `share: 1, hasWork: false`, because zero over zero is not zero and a protostar is not a remnant (the BV2.R19 distinction); an unreadable removal port gives `share: null, attributable: false`, because 'could not look' is not 'nothing was removed'; and a CONFIRMED removal naming no target dims nothing at all — brightness cannot know which idea — so `countUnattributedConfirmed()` exists to say the gap out loud instead of quietly brightening the sky. Brightness lives in `economy` beside size and reaches lifecycle through the new `lifecycle.removals` port, resolved OPTIONALLY and NOT declared in `consumes` (declaring it would turn 'lifecycle off' into 'instance will not start' — the `ideas.descent` precedent). **No credit is clawed back and nothing here writes**: ADR 0313 stands, `shipped` is terminal, only the picture changes. Rejected: parsing the proposal body (ADR 0319 already refused it — a human-facing line shape fails silently when reworded); re-deriving at verdict time; treating `ship_task_id` as the removed task; counting tasks rather than payouts; clamping the share at the top, which would hide the anomaly it claims to prevent.](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md) | ideas / economy |
416
416
  | 0322 | [**The five states are derived once, and a remnant needs evidence** ([task 1004103](https://cloudbongos.com/builders#/task/1004103), goal 1000095 — BONGOS-V2). The ideator surface draws a thought as one of five objects — star, protostar, satellite, remnant, cold body — and three surfaces are about to draw them (the room, the descent, the sky). Every fact already existed: `idea_inbox.status` (`core_195`), [ADR 0321](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md)’s `hasWork` / `share` / `attributable`, and `tasks.goal_id` + `tasks.status`. What was missing was ONE place that reads them: a state inferred in three renderers is three states, and the sky can say a thought went out while the room beside it still calls it alive, with nothing erroring. **Decision:** one pure resolver (`modules/ideas/idea-state.js`), brightness exposed on the `reward` port (ideas may not reach `credit_log` any other way — [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 — and already declares `reward` in `consumes`), and the ORDER of the tests IS the decision. **MERGED wins over everything:** a folded thought keeps its own money (`trail-size.js` is emphatic) but is no longer its own point in the sky, so letting work win draws one idea twice, beside the thought that absorbed it. **WORK beats a DISCARD:** a cold body “never ignited, so it has no size at all” is a stated PROPERTY, and testing work first makes it true by construction — triage really can discard a duplicate whose twin already routed and paid, and `updateIdea` never touches the ledger. **A REMNANT needs EVIDENCE, never the absence of an answer:** `share: 0` is a confirmed removal, `share: null` is an unreadable one, and ADR 0321 pays for that distinction upstream — so an idea with work and no readable facts is a STAR carrying `attributable: false`, because reading a falsy share as death turns one unmounted module into a sky of hollow rings. **A PROTOSTAR is the absence of WORK, not of a status:** ignition is being PAID for something, so a promoted-but-unshipped idea is still unignited, which is exactly what the nod queue exists for. **Position is a second axis** (`well` / `open-space` / `awaitingNod`) because the design says the object alone cannot carry it, and the nod is asked of EVERY task, not only the one that named the well. **`suggested_goal_id` is deliberately NOT a well** — `projection.js` records it as strictly advisory, the routing hint on a filing that did not convert, so reading it would put a thought in a nod queue with no task to promote; the binding `tasks.goal_id` now rides on both of the trail’s task joins at no extra round trip. The page reads the ledger ONCE: the state attach hands economy back the `byTask` breakdown size just computed, all of them or none (a partial map reads a missing entry as “no work” and draws a paid star as a protostar). An absent `reward` port does not empty the sky — states still resolve, flagged unattributable. Nothing writes; ADR 0313 stands. Rejected: resolving in each renderer; deriving from `share` alone (it merges a protostar with a remnant, the exact BV2.R19 distinction); treating an unreadable share as zero; status-first ordering; reading the hint as a home; a sixth object for “unknown brightness” (the vocabulary is the owner’s and is five — the uncertainty is a flag on the answer, not a new thing in the sky).](0322-a-remnant-needs-evidence-and-merged-wins.md) | ideas / economy |
417
+ | 0323 | [**Hosting is three shapes, and we are not the landlord** ([task 1004140](https://cloudbongos.com/builders#/task/1004140), goal 1000106 — BONGOS-V2). Five projects ran as one Node process each on the control-plane droplet, about $0.80/month apiece — the cheapest arrangement available, bought with ~4,600 lines of `provision*.js` that write systemd units, create unix accounts, render Caddy snippets, `createdb`, install backup timers and count memory against a ceiling. That is the cost nobody priced: **we are a landlord.** The owner reframed the question — build on what others maintain, and let an owner keep the deployment platform they already use. **Seven decisions.** The shapes become `byo-host` (the owner runs it), `cloud-host` (we stand it up for them) and `dedicated-cloud-host` (the same path, larger plan); `co-tenant` is **retired, not renamed**, because it means *a process on our box from our checkout* — the opposite of bring-your-own-host, so reusing the name would make every existing comment lie. **DigitalOcean hosts hubs only**, which closes [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) Finding 1 by removing its precondition rather than hardening the cookie: with no customer code on a `cloudbongos.com` subdomain there is no tenant to receive the hub session. **Render is the default** — its Postgres carries PITR and automated backups on every paid tier; Fly's managed Postgres starts ~$38/mo and Railway's is explicitly unmanaged, which is the exact responsibility being shed. **Cloudflare is the front door, not the compute**: Workers is a V8-isolate runtime that would require rewriting Express and `pg`, and Containers is built for bursty workloads that sleep, which a hall with background sweeps and a live event stream does not survive. **No deployment-platform adapter is built at all** — their application already deploys itself from the repository, so the platform we do not talk to is the platform we do not have to add. **Secrets: the platform is the system of record and Bongos stores names, never values** — a manifest of name, purpose and environment, no application secret, no platform API token, no "is it set?" probe, because each of those makes us custodian of other companies' credentials for convenience the manifest already delivers; `byo-host` marks the manifest unverifiable rather than opening an exception. **The free tier becomes a capped trial**, since ~$14/project cannot be subsidised indefinitely. Per-project cost rises ~17×, which is the price of not being a landlord and the reason metering stops being deferred work. Mostly a DELETION: [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md)'s supervision seam is not extended (Render supervises its own processes), [ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md) stays true and stops being load-bearing with its queued resize held rather than spent, and the hub's offsite backup becomes the highest-consequence open item. Rejected: AWS/GCP (2–3× the cost for a bare instance, plus an IAM and billing surface to learn — maintenance added where the goal is to remove it); keeping the shared box and fixing only the cookie (cheaper, closes the finding, leaves the landlord runner and answers nothing else); a Vercel/Netlify/Render adapter layer (speculative pluggability); Bongos as the secrets vault pushing values through platform APIs.](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) | hosting / provisioning / cost |
@@ -2360,5 +2360,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2360
2360
  landed since 1.19.941 with no explicit bump. run 35659043986. (task 1002620)
2361
2361
  1.19.943 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2362
2362
  landed since 1.19.942 with no explicit bump. run 35661533404. (task 1002620)
2363
+ 1.19.944 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2364
+ landed since 1.19.943 with no explicit bump. run 35665131778. (task 1002620)
2365
+ 1.19.945 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2366
+ landed since 1.19.944 with no explicit bump. run 35668642462. (task 1002620)
2363
2367
  ---------------------------------------------------------------------------
2364
2368
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.943",
3
+ "version": "1.19.945",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.943",
9
+ "version": "1.19.945",
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.943",
3
+ "version": "1.19.945",
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",
@@ -317,6 +317,14 @@ function planSync(files, { existingByName = new Map(), authorRankFor = () => nul
317
317
 
318
318
  // ---- disk + git ------------------------------------------------------------
319
319
 
320
+ // The repo a definitions dir belongs to, by the `<root>/.claude/agents` layout.
321
+ // Not REPO_ROOT: a split-root instance runs this script from the core package
322
+ // while its definitions live in the instance repo (task 1004139), so paths and
323
+ // git history must be read against the definitions' repo, not the core's.
324
+ function definitionsRoot(dir) {
325
+ return path.resolve(dir, '..', '..');
326
+ }
327
+
320
328
  // An ABSENT directory is a legitimate state, not an error: `.claude/agents/`
321
329
  // does not exist in the core tree at all today, and an instance that ships no
322
330
  // agents must deploy cleanly. Zero files, exit 0, nothing written.
@@ -328,10 +336,11 @@ function readAgentFiles(dir = DEFAULT_DIR) {
328
336
  if (err.code === 'ENOENT') return [];
329
337
  throw err;
330
338
  }
339
+ const root = definitionsRoot(dir);
331
340
  return names.sort().map((f) => {
332
341
  const full = path.join(dir, f);
333
342
  return {
334
- file: path.relative(REPO_ROOT, full).split(path.sep).join('/'),
343
+ file: path.relative(root, full).split(path.sep).join('/'),
335
344
  candidate: parseAgentFile(fs.readFileSync(full, 'utf8'), { name: f.replace(/\.md$/, '') }),
336
345
  };
337
346
  });
@@ -350,9 +359,9 @@ function readAgentFiles(dir = DEFAULT_DIR) {
350
359
  //
351
360
  // It is kept because provenance is genuinely useful where nothing is riding on
352
361
  // it. The wall is that it is never consulted where it could grant anything.
353
- function lastCommitterLogin(file, { runGit = defaultRunGit } = {}) {
362
+ function lastCommitterLogin(file, { runGit = defaultRunGit, cwd = REPO_ROOT } = {}) {
354
363
  let out;
355
- try { out = runGit(['log', '-1', '--format=%ae%n%an', '--', file]); }
364
+ try { out = runGit(['log', '-1', '--format=%ae%n%an', '--', file], cwd); }
356
365
  catch { return null; }
357
366
  const [email = '', authorName = ''] = String(out || '').split(/\r?\n/);
358
367
  const noreply = /^(?:\d+\+)?([A-Za-z0-9-]+)@users\.noreply\.github\.com$/.exec(email.trim());
@@ -360,8 +369,8 @@ function lastCommitterLogin(file, { runGit = defaultRunGit } = {}) {
360
369
  return authorName.trim() || null;
361
370
  }
362
371
 
363
- function defaultRunGit(args) {
364
- return execFileSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });
372
+ function defaultRunGit(args, cwd = REPO_ROOT) {
373
+ return execFileSync('git', args, { cwd, encoding: 'utf8' });
365
374
  }
366
375
 
367
376
  module.exports = {
@@ -386,11 +395,12 @@ async function main() {
386
395
  const asJson = argv.includes('--json');
387
396
  const dirIdx = argv.indexOf('--dir');
388
397
  const dir = dirIdx >= 0 ? argv[dirIdx + 1] : DEFAULT_DIR;
398
+ const root = definitionsRoot(dir);
389
399
 
390
400
  const files = readAgentFiles(dir);
391
401
  if (files.length === 0) {
392
402
  if (asJson) console.log(JSON.stringify({ ok: true, scanned: 0, plan: [] }));
393
- else console.log(`agents-sync: no definitions under ${path.relative(REPO_ROOT, dir) || dir} — nothing to reconcile.`);
403
+ else console.log(`agents-sync: no definitions under ${path.relative(root, dir) || dir} — nothing to reconcile.`);
394
404
  return 0;
395
405
  }
396
406
 
@@ -413,7 +423,7 @@ async function main() {
413
423
  // ONE git subprocess per file, memoised — `authorRankFor` is called again from
414
424
  // planSync, and re-shelling out there doubled the blocking spawns for an answer
415
425
  // that cannot have changed mid-run.
416
- const loginByFile = new Map(files.map((f) => [f.file, lastCommitterLogin(f.file)]));
426
+ const loginByFile = new Map(files.map((f) => [f.file, lastCommitterLogin(f.file, { cwd: root })]));
417
427
  // One query for every distinct committer, not one per file.
418
428
  const logins = [...new Set([...loginByFile.values()].filter(Boolean))];
419
429
  const rankByLogin = new Map();
@@ -411,14 +411,16 @@ fi
411
411
  # Gated on the module set the module loop just migrated (DB overrides included,
412
412
  # task 1004138). A fresh isModuleEnabled() read never loaded the overrides, so an
413
413
  # `agents` switched on in the hall got its tables but never its registry. Pinned to
414
- # $DB like that read, so the reconcile writes into the schema this run applied. The
415
- # file check keeps an instance without the script as silent as the old cwd-relative
416
- # require left it: a split-root instance (core in node_modules) still does not
417
- # reconcile, because which .claude/agents it should read is an open design question.
418
- if [[ " $NODE_ENABLED_MODULES " == *" agents "* ]] && [ -f scripts/gds/agents-sync.js ]; then
414
+ # $DB like that read, so the reconcile writes into the schema this run applied.
415
+ #
416
+ # The script is the core's, the definitions are the instance's own (task 1004139, an
417
+ # owner decision): a split-root instance (core in node_modules) reconciles ITS
418
+ # .claude/agents, never the core package's shipped set and no merge of the two.
419
+ # Single-root, CORE_ROOT == INSTANCE_ROOT and this is the same call as before.
420
+ if [[ " $NODE_ENABLED_MODULES " == *" agents "* ]]; then
419
421
  echo
420
422
  echo "post-deploy: reconciling .claude/agents/ into the agent registry"
421
- if ! DATABASE_URL='' PGDATABASE="$DB" node scripts/gds/agents-sync.js; then
423
+ if ! DATABASE_URL='' PGDATABASE="$DB" node "$CORE_ROOT/scripts/gds/agents-sync.js" --dir "$INSTANCE_ROOT/.claude/agents"; then
422
424
  echo "post-deploy: agents-sync failed — the registry is stale by one deploy, not broken" >&2
423
425
  fi
424
426
  fi
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.943'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.945'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -24,6 +24,7 @@
24
24
  // Run: node tests/agents_sync.mjs
25
25
 
26
26
  import { strict as assert } from 'node:assert';
27
+ import { execFileSync } from 'node:child_process';
27
28
  import { createRequire } from 'node:module';
28
29
  import fs from 'node:fs';
29
30
  import os from 'node:os';
@@ -279,6 +280,32 @@ t('a real directory is read, .md only, sorted', () => {
279
280
  assert.deepEqual(files.map((f) => f.candidate.name), ['alpha', 'historian'], 'sorted, so a run is reproducible');
280
281
  });
281
282
 
283
+ console.log('\na split-root instance reconciles its OWN definitions (task 1004139):');
284
+
285
+ t('definitions outside the core read against their own repo, not the core', () => {
286
+ // The core runs from node_modules while .claude/agents lives in the instance repo.
287
+ // Read against the core, source_path came out as ../../../.claude/agents/x.md and the
288
+ // git author lookup ran in the wrong repo.
289
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-sync-instance-'));
290
+ const dir = path.join(root, '.claude', 'agents');
291
+ fs.mkdirSync(dir, { recursive: true });
292
+ fs.writeFileSync(path.join(dir, 'scout.md'), FILE.replace('name: historian', 'name: scout'));
293
+ const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'ignore' });
294
+ try {
295
+ git('init', '-q');
296
+ git('add', '.');
297
+ git('-c', 'user.email=12345+octocat@users.noreply.github.com', '-c', 'user.name=Someone',
298
+ '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'add scout');
299
+
300
+ const [f] = sync.readAgentFiles(dir);
301
+ assert.equal(f.file, '.claude/agents/scout.md', 'source_path is relative to the instance root');
302
+ assert.equal(sync.lastCommitterLogin(f.file, { cwd: root }), 'octocat', 'history is read from the instance repo');
303
+ assert.equal(sync.lastCommitterLogin(f.file), null, 'the core repo has no history for an instance file');
304
+ } finally {
305
+ fs.rmSync(root, { recursive: true, force: true });
306
+ }
307
+ });
308
+
282
309
  console.log('\nplanSync — the whole reconcile, still pure:');
283
310
 
284
311
  t('one plan entry per file, each carrying its own author rank', () => {
@@ -407,6 +434,11 @@ t('migrate.sh invokes agents-sync, module-gated and fail-open', () => {
407
434
  // a fresh isModuleEnabled() read never saw a hall toggle.
408
435
  assert.match(sh, /\$NODE_ENABLED_MODULES " == \*" agents "\*/, "gated on the module — it is default:false");
409
436
  assert.doesNotMatch(sh, /isModuleEnabled\('agents'\)/, 'no override-blind re-read of the module flag');
437
+ // Split-root (task 1004139): the core's script, the instance's own definitions, pinned to
438
+ // $DB — and no instance-relative file check, which silenced every split-root instance.
439
+ assert.match(sh, /DATABASE_URL='' PGDATABASE="\$DB" node "\$CORE_ROOT\/scripts\/gds\/agents-sync\.js" --dir "\$INSTANCE_ROOT\/\.claude\/agents"/,
440
+ 'reconcile the instance .claude/agents with the core script, against $DB');
441
+ assert.doesNotMatch(sh, /-f scripts\/gds\/agents-sync\.js/, 'no instance-relative existence check');
410
442
  // Fail-OPEN: a stale registry is a nuisance, an aborted deploy is an outage,
411
443
  // and the script is already fail-closed about what it imports.
412
444
  assert.match(sh, /agents-sync failed — the registry is stale by one deploy/,