@bongos/core 1.19.383 → 1.19.384
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 +24 -14
- package/docs/adr/0195-adr-numbers-are-checked-like-migration-numbers.md +46 -0
- package/docs/adr/README.md +34 -23
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/adr-namespace.js +273 -0
- package/scripts/gds/fitness.js +7 -0
- package/src/module-api.js +3 -1
- package/tests/fitness.mjs +118 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.384",
|
|
6
|
+
"core_contract": "1.19.384",
|
|
7
|
+
"source_commit": "c17ffedef449a2cb392b9bedc87fa31f1746625a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-27T21:02:17.519Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 338,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1603,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1963,
|
|
20
|
+
"tree_sha256": "d6620f625c1c0969033c8326d7e5d9490f054072e6a6d0eecef66552c2025290",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1469,10 +1469,15 @@
|
|
|
1469
1469
|
"mode": "0000644",
|
|
1470
1470
|
"sha256": "65bd3a60b8d313af7ea69acf7fe9a522a15a144c4395b3788bd3f82cc20e8351"
|
|
1471
1471
|
},
|
|
1472
|
+
{
|
|
1473
|
+
"path": "docs/adr/0195-adr-numbers-are-checked-like-migration-numbers.md",
|
|
1474
|
+
"mode": "0000644",
|
|
1475
|
+
"sha256": "bef44bdf75d61a0e7f076b78d384c4523cb706e5a1023768daa232feee6898aa"
|
|
1476
|
+
},
|
|
1472
1477
|
{
|
|
1473
1478
|
"path": "docs/adr/README.md",
|
|
1474
1479
|
"mode": "0000644",
|
|
1475
|
-
"sha256": "
|
|
1480
|
+
"sha256": "ad42cce140bb167b6702b8181d1e31ca39f537dca3f294440fcc53ece97f47d3"
|
|
1476
1481
|
},
|
|
1477
1482
|
{
|
|
1478
1483
|
"path": "docs/api-reference.md",
|
|
@@ -5112,12 +5117,12 @@
|
|
|
5112
5117
|
{
|
|
5113
5118
|
"path": "package-lock.json",
|
|
5114
5119
|
"mode": "0000644",
|
|
5115
|
-
"sha256": "
|
|
5120
|
+
"sha256": "d1bb9eb4376b39eb26a2fb46d91a70556c332521be0f193633d11807248f48ca"
|
|
5116
5121
|
},
|
|
5117
5122
|
{
|
|
5118
5123
|
"path": "package.json",
|
|
5119
5124
|
"mode": "0000644",
|
|
5120
|
-
"sha256": "
|
|
5125
|
+
"sha256": "cbb22bb9c93706c85329e51d1a67becc24d477f6c63baadadb2c6b10aa1cc59a"
|
|
5121
5126
|
},
|
|
5122
5127
|
{
|
|
5123
5128
|
"path": "public-docs/index.html",
|
|
@@ -5474,6 +5479,11 @@
|
|
|
5474
5479
|
"mode": "0000644",
|
|
5475
5480
|
"sha256": "07ff338beac12979eba50e4a6fe9c2f6dbfd842fa01abe1c412b19888d10c4a0"
|
|
5476
5481
|
},
|
|
5482
|
+
{
|
|
5483
|
+
"path": "scripts/gds/adr-namespace.js",
|
|
5484
|
+
"mode": "0000644",
|
|
5485
|
+
"sha256": "9c7ef61b64e41ee5dd6d913883e16dfed6d44bd6eefa4960b8dde566a63b5621"
|
|
5486
|
+
},
|
|
5477
5487
|
{
|
|
5478
5488
|
"path": "scripts/gds/api.js",
|
|
5479
5489
|
"mode": "0000644",
|
|
@@ -5827,7 +5837,7 @@
|
|
|
5827
5837
|
{
|
|
5828
5838
|
"path": "scripts/gds/fitness.js",
|
|
5829
5839
|
"mode": "0000644",
|
|
5830
|
-
"sha256": "
|
|
5840
|
+
"sha256": "2fcc7dfa75a6fcf1ebc0fb75d0e4343dc54b9d75b88fb798f8b16674b919c1b8"
|
|
5831
5841
|
},
|
|
5832
5842
|
{
|
|
5833
5843
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -7022,7 +7032,7 @@
|
|
|
7022
7032
|
{
|
|
7023
7033
|
"path": "src/module-api.js",
|
|
7024
7034
|
"mode": "0000644",
|
|
7025
|
-
"sha256": "
|
|
7035
|
+
"sha256": "62c5f68383474bfeb692ea38e926c689615320d644d8d83fe7c35b3dd51e23bb"
|
|
7026
7036
|
},
|
|
7027
7037
|
{
|
|
7028
7038
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7912,7 +7922,7 @@
|
|
|
7912
7922
|
{
|
|
7913
7923
|
"path": "tests/fitness.mjs",
|
|
7914
7924
|
"mode": "0000644",
|
|
7915
|
-
"sha256": "
|
|
7925
|
+
"sha256": "e247bd7ab5ea26493186d9c308a7774bd6ef65fe057613da887ec94fe8b2b5b5"
|
|
7916
7926
|
},
|
|
7917
7927
|
{
|
|
7918
7928
|
"path": "tests/fitness_ratchets.mjs",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 0195 — ADR numbers are checked like migration numbers, and the twenty existing collisions are grandfathered by name
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-27
|
|
5
|
+
- **Deciders:** the builder on [task 1003164](https://cloudbongos.com/builders#/task/1003164)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Two ADRs could carry the same number and nothing flagged it. It happened for real on 2026-08-22: [task 1002483](https://cloudbongos.com/builders#/task/1002483)'s branch authored `<redacted>.md` while, three days into that branch sitting stranded at `completed`, [task 1003042](https://cloudbongos.com/builders#/task/1003042) landed `0181-teardown-state-gate-and-address-retention.md` on `main`. Merging `main` into the branch produced both files side by side.
|
|
10
|
+
|
|
11
|
+
**Git cannot see this.** The filenames differ, so there is no path collision and no merge conflict — git adds both cleanly. `docs/adr/README.md` auto-merges too, ending up with two `| 0181 |` rows, the incoming one placed out of the ascending order the index reads in. The `unit` gate, fitness, docs-entropy and the grader all passed. A human reading `ls docs/adr/` found it.
|
|
12
|
+
|
|
13
|
+
**The asymmetry that made this a bug rather than a gap.** Migrations already have exactly this protection. `migration-namespace.js` exports `stemCollisions()`, and `fitness.js` runs `checkMigrationNamespacing` as a hard failure — because a re-used migration number silently *skips* a migration, which the team correctly treated as unacceptable. ADRs got `next-number.js`, which computes the next free number for a claim card from a possibly-stale tree, best-effort, at claim time — and then nothing enforced it afterwards. Same shared, monotonic, human-allocated number line; one guarded, one not.
|
|
14
|
+
|
|
15
|
+
**Why it matters past tidiness.** An ADR number is a *citation target*. [Task 1000900](https://cloudbongos.com/builders#/task/1000900)'s close-rationale cited "ADR 0180" and, because of this collision, `0180` on `main` is *Dependency intake & drift policy* — an unrelated document. A reader following that citation lands nowhere near the ship gate it meant. ADRs cite each other by number throughout, and `docs/adr/README.md` is the index every agent is told to consult, so a duplicated number makes a citation ambiguous **forever**.
|
|
16
|
+
|
|
17
|
+
**What building the check revealed.** The bug report described one collision. The live tree held **twenty**, plus fourteen index defects the same merge class had produced: two ADRs listed twice (`0116`, `0117` — a merge duplicated the rows verbatim) and twelve ADRs with no index row at all. Ten of those twelve were the *second file of a colliding pair* — which is the mechanism by which a collision hides, since the file that never gets indexed is the one nobody sees. The hand-written "known collision" paragraph under the index narrated eight collisions and had fallen twelve behind reality: a record nobody checked, the shape [ADR 0184](<redacted>.md) was written about.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
**We will check ADR numbers the way we check migration numbers — a hard CI failure — and grandfather the twenty existing collisions by exact filename rather than renumbering them.**
|
|
22
|
+
|
|
23
|
+
1. **`scripts/gds/adr-namespace.js`** is the ADR sibling of `migration-namespace.js`: pure functions over a file list and the index text, no disk, unit-testable. It exports `checkAdrNumbering()`, registered in `fitness.js` (Check 31) and therefore inside the required `unit` CI check — no new workflow.
|
|
24
|
+
|
|
25
|
+
2. **A number claimed by two files is a hard failure naming both paths**, unless that number's file set is *exactly* the frozen pair in `LEGACY_DUPLICATE_ADRS`.
|
|
26
|
+
|
|
27
|
+
3. **The index is checked by link target, not by number**: every ADR file has exactly one row, and every row points at a file that exists. This is one invariant covering all three real defects — the dangling row, the duplicated row, and the unindexed file.
|
|
28
|
+
|
|
29
|
+
4. **Row ordering warns, never blocks.** Ten legacy rows sit out of ascending order. Blocking would fail every PR for a mess nobody's diff created — the split [ADR 0189](<redacted>.md) draws between declaration rot (hard) and inherited drift (warn).
|
|
30
|
+
|
|
31
|
+
## Alternatives considered
|
|
32
|
+
|
|
33
|
+
- **Renumber the nineteen newer halves so the tree is genuinely unique.** Rejected: renumbering breaks every existing citation to those numbers, which is precisely the harm this check exists to prevent, and `README.md` states the rule outright — a number, once assigned, is never reused. It would also rewrite nineteen files' git history for a cosmetic property no reader depends on, since each pair's two files are unambiguous by *name*.
|
|
34
|
+
- **Grandfather by number instead of by exact filename** (`0181` is simply exempt). Rejected: that permanently un-guards twenty numbers. A third ADR joining `0181`, or a rename of either half, would pass silently. Set-equality on filenames keeps the exemption exactly as wide as the history that earned it, and the list can only shrink.
|
|
35
|
+
- **A numeric cutoff, mirroring `LEGACY_BARE_CORE_MAX`.** Rejected: migrations could use a cutoff because the new namespace makes collision *structurally* impossible above it. Here a new ADR takes a number above the current max, so a cutoff would exempt every historical number while catching nothing that hasn't already happened — and a new file deliberately numbered `0050` would sail through.
|
|
36
|
+
- **Enforce "exactly one index row per ADR *number*"**, as the bug report proposed. Rejected: it cannot express a grandfathered pair, which legitimately has two rows. Keying on the link target says what we actually mean and catches strictly more.
|
|
37
|
+
- **Fix it in `next-number.js` instead** — make the allocator authoritative. Rejected: the allocator already exists and already tries. It runs at *claim* time against a tree that may be days stale, which is exactly how the `0181` pair happened; only a check at merge time sees both files.
|
|
38
|
+
- **Leave the check advisory** (warn, like the doc-comment gate). Rejected: this is the failure the migration check treats as unacceptable, one layer up, and a warning is what the situation already effectively had.
|
|
39
|
+
|
|
40
|
+
## Consequences
|
|
41
|
+
|
|
42
|
+
- A new ADR number collision now fails the `unit` gate, naming both paths and the remedy. The failure arrives on the PR that creates it rather than months later via a wrong citation.
|
|
43
|
+
- The index and the tree can no longer drift apart unnoticed: 214 files, 214 rows, checked on every PR. Adding an ADR without indexing it is now a build failure — a small new obligation on every ADR author, and the point.
|
|
44
|
+
- The twenty legacy pairs keep their filenames and their citations. They are listed once, in code, in a list that may only shrink; the prose paragraph that used to narrate them (and lagged twelve behind) now points at that list instead of restating it.
|
|
45
|
+
- Ten index rows sit out of order and stay that way, as warnings. Anyone reordering them is welcome to; nothing forces it.
|
|
46
|
+
- `docs/adr/README.md`'s `#` column now uses `NNNN-a` / `NNNN-b` for all twenty pairs, extending a convention that previously covered five of them.
|
package/docs/adr/README.md
CHANGED
|
@@ -123,7 +123,8 @@ This keeps the decision history honest and traceable.
|
|
|
123
123
|
| 0054 | [Flat cost-plus session token reward (example as equity): reward = round(true_cost_usd × 1.20 × 100), server-priced from per-model tokens, idempotent per session_id](<redacted>.md) | GDS / economy |
|
|
124
124
|
| 0055 | [Server-mediated branch publish (the dev box holds no GitHub credential): box uploads a thin bundle, the GDS pushes + opens the PR + auto-merges with a server-side PAT](<redacted>.md) | GDS / dev boxes + ship pipeline |
|
|
125
125
|
| 0056 | [Version-control the prod deploy script as a reviewable mirror (`scripts/deploy/deploy-prod.sh`), and retry the post-restart `/healthz` so a slow service bind no longer false-fails a successful deploy](<redacted>.md) | GDS / deploy pipeline |
|
|
126
|
-
| 0057 | [Approve new members and first dev boxes from Discord: two Archon-only channels where a 👍/👎 fires the existing admit (`access_requests`) / first-box (`box_intents`) approve action, re-checked against the reactor's live rank](<redacted>.md) _(box half superseded by ADR 0059)_ | GDS / Discord + onboarding |
|
|
126
|
+
| 0057-a | [Approve new members and first dev boxes from Discord: two Archon-only channels where a 👍/👎 fires the existing admit (`access_requests`) / first-box (`box_intents`) approve action, re-checked against the reactor's live rank](<redacted>.md) _(box half superseded by ADR 0059)_ | GDS / Discord + onboarding |
|
|
127
|
+
| 0057-b | [Per-builder container cost ledger (org-funded → self-fund tracking)](<redacted>.md) | GDS / cost + dev boxes |
|
|
127
128
|
| 0058 | [Automatic gate-surface gatekeeper (rank-based auto-approval + deterministic hard floor)](<redacted>.md) | GDS / shipping pipeline |
|
|
128
129
|
| 0059 | [Single approval: remove the dev-box (first-box) Archon approval gate — collapse to one human gate (member-join admission); an admitted builder's first box provisions hands-off](<redacted>.md) | GDS / builder onboarding |
|
|
129
130
|
| 0060 | [GDS retrieval layer: Postgres-resident, lexical-first (FTS + pg_trgm fused with RRF), rank-scoped, fail-open — the "one-call recall" architecture (no new infra; embeddings deferred behind a measured gate)](0060-gds-retrieval-layer.md) | GDS / recall + search |
|
|
@@ -138,8 +139,10 @@ This keeps the decision history honest and traceable.
|
|
|
138
139
|
| 0069 | [Builders-hall live updates move from a Colyseus WebSocket room to an SSE endpoint (`GET /api/gds/live`) so the hall refreshes live on a game-less Cloud Bongos boot (R55, C8). Supersedes ADR 0030's §2 transport only — the `NOTIFY` + `live-channel.js` signal layer is unchanged; same opaque zero-leak `nudge`, `requireBuilder` gate, per-IP cap kept inline (no `src/rooms/` import). Deletes the room + join-guard; the hall stops loading `/vendor/colyseus.js`. EventSource auto-reconnect replaces the manual rejoin backoff.](<redacted>.md) | GDS / builders hall |
|
|
139
140
|
| 0070 | [Deterministic delivery of in-session cards (hook-delivered, read-only vs mutating)](<redacted>.md) | GDS / context |
|
|
140
141
|
| 0071 | [Dev-box teardown confirms the DigitalOcean droplet is actually deleted BEFORE marking the GDS row 'destroyed' (idempotent + retried; a failed delete parks at 'error' with droplet_id preserved, never a silent zombie); adds a bidirectional `reconcile-drift` sweep (force-delete zombie droplets no live row claims, mark ghost rows whose droplet vanished) on an hourly timer, with an in-flight young-droplet guard. Fixes the running-but-'destroyed' zombie that froze a box on old code.](<redacted>.md) | GDS / dev boxes |
|
|
141
|
-
| 0072 | [Bongos app ships Mac-signed + notarized first; Windows code signing deferred (BV1.R01, C1). Reuses the Dev Box app's signing identity verbatim — the same Apple Developer ID + the inert-until-armed, `main`-ref-gated (SR-13/SR-14) Mac block — rather than minting a new cert; declines a Windows cert for now, riding the existing Windows-only blocker [#20](https://example.com/builders#/blocker/20) (Azure Trusted Signing vs EV). One cert decision serves both apps once they merge into the umbrella. $0 new recurring cost; Windows users keep the "Run anyway" first-run friction until blocker [#20](https://example.com/builders#/blocker/20) resolves.](<redacted>.md) | Bongos app / packaging |
|
|
142
|
-
|
|
|
142
|
+
| 0072-a | [Bongos app ships Mac-signed + notarized first; Windows code signing deferred (BV1.R01, C1). Reuses the Dev Box app's signing identity verbatim — the same Apple Developer ID + the inert-until-armed, `main`-ref-gated (SR-13/SR-14) Mac block — rather than minting a new cert; declines a Windows cert for now, riding the existing Windows-only blocker [#20](https://example.com/builders#/blocker/20) (Azure Trusted Signing vs EV). One cert decision serves both apps once they merge into the umbrella. $0 new recurring cost; Windows users keep the "Run anyway" first-run friction until blocker [#20](https://example.com/builders#/blocker/20) resolves.](<redacted>.md) | Bongos app / packaging |
|
|
143
|
+
| 0072-b | [Dev-box code staleness: make it visible, never auto-reset](<redacted>.md) | GDS / dev boxes |
|
|
144
|
+
| 0073-a | [The secrets scan excludes TruffleHog's generic URI detector. It flagged a `scheme://userinfo@host` SSRF-guard rejection-test fixture (and, fatally, a URL quoted in a fix commit's message — TruffleHog scans commit messages, which `--exclude-paths` cannot reach), reddening every PR off main once a dev-box tunnel host stopped resolving (ADR 0071). Excluding the URI detector on both scan surfaces loses no real coverage — connection-string secrets (postgres://, mysql://, …) keep their dedicated detectors. Lesson: exclude noisy detectors at the detector level, and keep commit messages free of credential-shaped URLs.](<redacted>.md) | GDS / CI gates |
|
|
145
|
+
| 0073-b | [A consistent "the system needs an input from you" signal, and pragmatic BYOK for the per-builder Gemini key](<redacted>.md) | GDS / builder signals + keys |
|
|
143
146
|
| 0074 | [The memory map stays light (budget guard + auto-tidy + drawer)](<redacted>.md) | GDS / memory |
|
|
144
147
|
| 0075 | [Per-system cost attribution + the savings counterfactual ledger (System 1 of the LLM-reduction redesign). Corrects LLM pricing to flat Opus 4.8 rates (no 1M premium); splits the single `session` `cost_log` row into call-site-tagged rows (`session:<redacted>, `cron:<routine>:*` via `OTB_COST_CRON_ROUTINE`) each carrying a per-tier USD split (migration 123); adds a `cost_baseline` table (migration 124) where every later system records a `priceUsage()`-computed counterfactual (never client-supplied) so savings are `SUM(baseline−actual)` by system; surfaces `by_source`/`by_tier` (cache_read share) / `savings` as additive blocks on the unchanged `/public/cost-summary`, with the audit folding them in plus a `savings_without_baseline` tripwire. Turns the 82%-cache_read finding from an inference into a queryable column.](<redacted>.md) | GDS / cost ledger |
|
|
145
148
|
| 0076 | [In-session deterministic compaction (System 2 — the only lever on the 82% marathon-cache_read pool, est 20-40%). A pure classifier (`.claude/hooks/eviction-policy.js`) finds provably-stale `tool_result` blocks (superseded reads, duplicate reads, old uncited bulk dumps) and replaces each with a one-line recovery stub; never touches user/assistant/reasoning turns, the context-pack, worktree guidance, the last N turns, or the newest read of a file; a backward-reference liveness scan keeps anything a later turn cites. Batched ONLY at the PreCompact boundary (per-turn would invalidate the prompt cache); recovery is retrieval-first via the stub → `/recall` (search.js) / re-Read / tool-rerun loop, with a one-time directive that prevents re-fetch loops. Wired behind `OTB_INSESSION_COMPACT`, OFF by default (wire, don't arm). Spike 1347 measured ~24% realized headroom on real transcripts.](<redacted>.md) | GDS / context + cost |
|
|
@@ -148,12 +151,14 @@ This keeps the decision history honest and traceable.
|
|
|
148
151
|
| 0079 | [Value-gated autonomous engagement (System 3 — the autonomous-spend lever, est 8-14%; **SHIPPED INERT**). Merges three spawn decisions behind one off-by-default flag (`OTB_AUTONOMY_ENABLED`): a $0 deterministic precheck (`autonomy-gate.js`) that NO-GOs a nightly routine on no-signal nights or a spend-brake (audited in `autonomy_runs`, migration 126); a cascade router (`cascade-dispatch.js`) that drafts a spawn on the cheapest sufficient model and escalates only on an objective signal, never above the builder's ceiling; and a safe_autofix lane that collapses review→triage→rebuild into one fix-and-ship run — permitted ONLY when high-confidence + single-file + not a protected path + the grader is LIVE (never while GRADER_BYPASS is on). BUILT DISABLED per owner directive: decide() returns GO and autofixAllowed() returns false by default, so wiring changes nothing until armed alongside the grader rework. Does NOT touch marathon spend (that's System 2 / ADR 0076).](<redacted>.md) | GDS / autonomy + cost |
|
|
149
152
|
| 0080 | [LLM-cache API transport + trust model (System 5 — the transport that finally lets the content-addressed cache of ADR 0077 hit). Its two consumers (`architect-audit.js`, `onboarding-analyze.js`) run as CLI/cron on builder machines where the `pg` pool is unreachable (peer-socket only), so every cache op silently degraded to miss/no-store and the cache never hit organically (eval task 1372: 0 rows, 0 hits despite correct logic). Fix: expose the cache over the GDS API — `POST /api/gds/llm-cache/lookup` + `/store` in `src/bongos/routes/llm-cache.js`, each mapping 1:1 onto the existing `llm-cache.js` DB helper (the route never reimplements SQL; `/store` mounts its own 512 KB body parser since a full subagent stdout exceeds the global 64 KB cap). `llm-cache.js` self-routes: pool first (memoized `poolReachable()` — a `false` is never memoized so a boot blip can't permanently disable the server cache), else an injected `apiCall` fallback (`setApiFallback`), so the always-on server core never statically requires the CLI layer (preserving the core↔host boundary the fitness gate guards). Best-effort end-to-end: any pool/network/auth error degrades to miss/no-store and never throws. Trust model (load-bearing): a store writes a `result_stdout` a later run replays at $0 and trusts, so an open store endpoint is a content-injection vector into the planning pipeline and a metrics-integrity vector into the public savings number — hence rank-gated, reusing the ADR 0016 server-enforced trust boundary.](<redacted>.md) | GDS / LLM cache + cost |
|
|
150
153
|
| 0081 | [Tool-agnostic design layer — the repo (design tokens + component code + exported assets) is the single source of truth; Claude Design, Figma, and any future design tool are SWAPPABLE ADAPTERS against that neutral contract, not hardwired integrations (the ADR 0062 core↔host pattern applied to design — vendor-lock-in insurance the owner asked for). Splits a `ui` discipline out of `artist`/graphics (UI = design-as-code, graded by diff + screenshot; graphics = pixel art, graded by vision). Code-canonical round-trip: structured UI round-trips as code, freeform canvas art is a committed asset; a talented human can still edit directly in Figma and the change round-trips into the repo via a normal GDS task (the escape hatch). One ship pipeline + ONE multimodal grader with a single regen loop, parameterized by type (rubric + evidence chosen by task type) — retires the standalone Gemini vision path and the two-grader-set maintenance trap. Claude Design adapter = code-native /design-sync + Claude Code handoff; Figma adapter = human-edit→code + code→Figma sync. Grader/ship changes ride the gate-review hard floor (ADR 0043). Implementation cluster tasks 1376–1380.](<redacted>.md) | GDS / design pipeline |
|
|
151
|
-
| 0082 | [Server-side auto-resolution of generated-file & append-index merge conflicts: the publish-reconciler runs a temp-clone `git merge origin/main` with the `otb-regen` driver + a `docs/adr/README.md merge=union` attribute active, then pushes the resolution so a PR that conflicts ONLY on generated indexes or the ADR append-index self-heals server-side (GitHub's own merge can't run the custom driver) — the PR #300/[#167](https://example.com/builders#/task/167) stall. Pushes a commit (all required checks re-run), aborts to a human on any genuine hand-conflict, best-effort + kill-switchable; zero trust-boundary change (rejects the alternative of lowering deploy access to `thetes`).](<redacted>.md) | GDS / CI + merge |
|
|
154
|
+
| 0082-a | [Server-side auto-resolution of generated-file & append-index merge conflicts: the publish-reconciler runs a temp-clone `git merge origin/main` with the `otb-regen` driver + a `docs/adr/README.md merge=union` attribute active, then pushes the resolution so a PR that conflicts ONLY on generated indexes or the ADR append-index self-heals server-side (GitHub's own merge can't run the custom driver) — the PR #300/[#167](https://example.com/builders#/task/167) stall. Pushes a commit (all required checks re-run), aborts to a human on any genuine hand-conflict, best-effort + kill-switchable; zero trust-boundary change (rejects the alternative of lowering deploy access to `thetes`).](<redacted>.md) | GDS / CI + merge |
|
|
155
|
+
| 0082-b | [Productized key-storage provisioning (server-generated master key, zero SSH)](<redacted>.md) | security / key storage |
|
|
152
156
|
| 0083 | [Practical build plan for the modular architecture (strangler migration) — the decision record that turns the scoped design research (`docs/design/modular-architecture/`, 5 docs) into a concrete, CI-guarded, one-module-at-a-time build. Builds the whole product into `core + modules`: a ~3.5k-LOC kernel + ~16 modules, each a vertical slice reached through ONE published, semver'd doorway (`src/module-api.js` — the only core file a module may import); a loader that discovers/validates/mounts `modules/*/module.json` behind kernel-composed auth and replaces the closed `KNOWN_MODULES` registry in `src/modules.js`; modules talk only through kernel-mediated seams (ports + events), never by importing each other. Boundaries are held by fitness functions (a violation is a red build), not discipline — five new `fitness.js` checks (core-never-imports-a-module, <redacted>, module-route-rank-gate via parameterizing `route-rank-check.js`, core-carries-no-module-code, module-migrations-additive+namespaced). Module-owned migrations generalize the working `migrations/game/` enabled-gated precedent to `modules/<key>/migrations/`. Reorganize, NOT rewrite (the foundation is sound, the filing is wrong); stay single-package/single-process/no-bundler (modularity is a separate axis). Moves ordered by MEASURED isolation: dev-box (the template) → game → discord → art-pipeline; the recurring seam is `routes/me.js` + `settings.js`, cut by a module→core contribution hook. task→module affinity (retire `touches[]`) is the final-tier follow-on. Reconciles the numbering collision: the design docs + epic [#1388](https://example.com/builders#/task/1388) + task [#1389](https://example.com/builders#/task/1389) call this "ADR 0080", but 0080 is the LLM-cache — the module-system record is THIS file. Seeds BONGOS-V1 criterion C5 + the BV1.R## children.](<redacted>.md) | GDS / architecture |
|
|
153
157
|
| 0084 | [Auto-derived per-task rank floor — every task is rank-scoped and the claim gate finally BITES. `tasks.requires_rank` (V3.R82) + the claim-time gate (`db.claimTask` → `INSUFFICIENT_RANK`) + the `/builder-start` hide were all sound but INERT: no create/edit path ever set `requires_rank`, so every task took the `NOT NULL DEFAULT 'xenos'` and anyone at thetes+ could claim anything. Fix: `deriveRequiredRank(touches, security_sensitive)` derives a floor by reusing the SAME protected-path matcher as the trust boundary (`permission-path-check.matchProtected`, ADR 0043) — the rank/authz/ship-grade/deploy/migration core (the paths a sub-Metic builder already can't push) floors at `metic`, game/art/docs stay `xenos`; never auto-assigns `archon`. `highestRank(floor, requested)` composes it with an Archon override that can only RAISE (a request below the floor is clamped up; `POST`/`PATCH /tasks` accept `requires_rank`, Archon-only; flipping `security_sensitive` ratchets up). Migration 145 backfills every non-terminal task (raise-only, idempotent, SQL generated faithfully from `PROTECTED_GLOBS`). Hall shows a rank pill. One sensitivity source of truth shared with the git-push floor, so claim-floor and push-floor can't drift.](<redacted>.md) | GDS / authz |
|
|
154
158
|
| 0085 | [Builders are uncapped by default — removes the default $50/mo per-builder spend cap. `classifyCostStatus` no longer falls back to a project default: a `NULL`/`<=0` `monthly_budget_usd` now means UNCAPPED (`level:'ok'`, `budget_mtd_usd:null`, nothing blocks), and the `DEFAULT_MONTHLY_BUDGET_USD=50` constant is deleted. Reverses the default-cap half of GDS-V3 C3 (task 302): the default blocked the very work it was meant to enable — a new builder's own `session:<redacted> API cost crossed $50 within a day and froze /builder-ship + the art pipeline (builder 95 hit $69.32 MTD on day two). KEEPS the Archon override (`PATCH /builders/:id/budget`) as a manual emergency brake — a positive value still enforces warn(≥80%)/over(≥100%). Cost recording + the /public/cost-summary dashboard are unchanged (removes blocking, not observability). No schema change; project-wide guards (autonomy-gate, art HARD_STOP) unaffected.](<redacted>.md) | GDS / cost ledger |
|
|
155
159
|
| 0086 | [Goal-scoped work hierarchy — assign goals, not tasks. Introduces a `goal` tier and a six-level hierarchy (Project → Version → Goal → Criteria → Task → Sub-task) whose structure is a per-instance config preset (`config/hierarchy.json`, owner-set at onboarding — the ADR 0062 pattern) and whose enforcement is core. A goal is a SHARED, joinable, module-scoped workspace: joining grants bounded authority to author tasks within the goal's `scope_modules` (the wall, set by a trusted rank at planning, never self-granted; widened only by an approved request — Archon for protected modules). Reuses almost everything: criteria re-parent under goals (`done_when_criteria.goal_id`; `/status` rolls up Version→Goals→Criteria→Tasks), sub-tasks stay `parent_task_id`, the per-task rank floor is the auto-derived one (ADR 0084), dependencies become a polymorphic blocked-until-done edge resolved at the claim gate (ADR 0015 — cross-goal allowed, reference-not-grant). Achievement auto-flags when a criterion's tasks ship, then a planner (Metic+) confirms via a new `/goal-review` skill (idea-triage-style), auto-flipping the goal achieved. Deterministic scope + protected-path gate at create; the ship-time grader/gate-review/main-audit stay authoritative on the real diff. Deferred to follow-ons: the reward model, sub-Metic membership in protected-scope goals (amends ADR 0043), and an AI relevance judge. Fills ADR 0083 Rule 5 ("a task is scoped to a module").](<redacted>.md) | GDS / work hierarchy |
|
|
156
|
-
| 0087 | [Bongos app architecture + the model-agnostic feedback handoff contract (BONGOS-V1 / C4). Records the umbrella desktop app that absorbs the Dev Box app as a module, the capture→synthesize→handoff pipeline (R04–R14), and the Rung-3 *visual* capture floor (pixels + voice + freeze-frame markup over any app) with element-level reads deferred to V2 (Accessibility requested lazily, never up front). The contract: a bundle is a self-contained `~/CloudBongos/feedback/<id>/` dir where every screenshot is referenced by ABSOLUTE FILE PATH, never base64 — so the text prompt stays ~5–11k tokens and any agent opens the frames with its own file read. One shared `latest` pointer (atomic `latest.json` + best-effort symlink, self-healing) feeds three readers that can't drift: Claude Desktop (clipboard + open folder, R12), Claude Code (`/feedback` slash command cats prompt.md + Reads the frame paths, R13), and an optional zero-dependency stdio MCP server (`get_latest_feedback` returns text + file paths, never base64, R14). Hand-rolled MCP (newline-delimited JSON-RPC, no SDK dep) keeps the install surface flat. Bongos succeeds the GDS as the primary builder surface after GDS-V4; a fourth agent is an adapter, not a redesign. $0 new recurring cost.](<redacted>.md) | Bongos app / feedback loop |
|
|
160
|
+
| 0087-a | [Bongos app architecture + the model-agnostic feedback handoff contract (BONGOS-V1 / C4). Records the umbrella desktop app that absorbs the Dev Box app as a module, the capture→synthesize→handoff pipeline (R04–R14), and the Rung-3 *visual* capture floor (pixels + voice + freeze-frame markup over any app) with element-level reads deferred to V2 (Accessibility requested lazily, never up front). The contract: a bundle is a self-contained `~/CloudBongos/feedback/<id>/` dir where every screenshot is referenced by ABSOLUTE FILE PATH, never base64 — so the text prompt stays ~5–11k tokens and any agent opens the frames with its own file read. One shared `latest` pointer (atomic `latest.json` + best-effort symlink, self-healing) feeds three readers that can't drift: Claude Desktop (clipboard + open folder, R12), Claude Code (`/feedback` slash command cats prompt.md + Reads the frame paths, R13), and an optional zero-dependency stdio MCP server (`get_latest_feedback` returns text + file paths, never base64, R14). Hand-rolled MCP (newline-delimited JSON-RPC, no SDK dep) keeps the install surface flat. Bongos succeeds the GDS as the primary builder surface after GDS-V4; a fourth agent is an adapter, not a redesign. $0 new recurring cost.](<redacted>.md) | Bongos app / feedback loop |
|
|
161
|
+
| 0087-b | [Compete with Cursor on governance, not tooling](<redacted>.md) | strategy / positioning |
|
|
157
162
|
| 0088 | [Consistent-character generative animation — a generate→grade→audit→regenerate loop built by EXTENDING the existing family pipeline (ADR 0008: `run_family` + cross-frame coherence + the `character_identity` Gemini review + `t5_anchor_refine`), not a new grader/loop (the owner's "don't reinvent the loop"). Decision (with the owner, 2026-06-26): option A — Gemini + grade/audit with a PLUGGABLE generator backend (the ADR 0062/0081 adapter pattern) — over a per-character LoRA, which is deferred as "level-2" (needs GPU training + an open model). The family pipeline is already a consistent-character multi-frame loop (walk/peck cycles ship through it) but tuned for 32×32 pixel-art; this adds a painterly `character_anim` family kind (drops palette-snap/32×32), the two genuinely-new criteria `pose_match` + order-aware transition-coherence (existing checks are order-agnostic — they'd pass a shuffled animation), a recursive Gemini inbetween layer for high frame counts (≈12–24 "on twos"/"on ones"), a painterly cutout/register post-process, and packaging as a Cloud Bongos module (ADR 0083). First consumer = the Bongo Buddha's 8 actions → widget (task 1523). Implementation epic: tasks 1549–1559.](<redacted>.md) | GDS / art pipeline |
|
|
158
163
|
| 0089 | [Modular, multi-model adversarial grader (open-weight ensemble) — refactor the ship-gate into a central, work-type-parameterized engine (gather evidence → deterministic pre-passes → judge ensemble → normalize+aggregate → gate → persist) where each MODULE contributes only its rubric + evidence-gatherer + specialist critics through the ADR 0083 seam (`grader.workType.<key>`), answering the owner's "per-module or general?" as central-engine + module-plugged. Adds open-weight LLM judges from a DIFFERENT model family than the author (Claude stays as the quality floor, never the sole judge) to kill same-family self-preference inflation (10–25%); judges run via a `ModelProvider` adapter (`openai-compatible` → DeepInfra/OpenRouter/vLLM/Ollama, the ADR 0062 neutral-contract pattern) on a hosted pay-per-call API (~$1–5/mo, owner spend-gated). Small (2–3) audited panel — z-score/rank-normalize then median + hard-veto aggregation; an error-correlation audit routine (Cohen's κ vs owner decisions) keeps the jury failing DIFFERENTLY (the "Nine Judges, Two Effective Votes" finding) rather than assuming vendor-diversity. Deterministic route-rank + permission-path pre-passes stay ZERO-LLM and authoritative (no LLM verdict, Claude or open, can upgrade a fail over a protected path; ADR 0016/0043/0080); VLM judges advisory on art, deterministic palette/grid checks authoritative. Owner decisions 2026-06-26: central-engine + module-plugged; ensemble vote; hosted API. Reuses the worker-contract + task_grades schema + LLM cache (ADR 0077); rolls out advisory-first behind a flag. Build cluster task-pending (task 1561).](<redacted>.md) | GDS / grader + quality gate |
|
|
159
164
|
| 0090 | [Metic gains task authoring — re-ranks `POST /tasks`, `PATCH /tasks/:id`, `POST /tasks/:id/promote`, and the `tasks/:id/{dependencies,criteria}` CRUD from Archon-only to `requireRank('metic','archon')` (owner decision 2026-06-26: a rank trusted to triage ideas is trusted to seed + shape the tasks they become). Moves the two `EXPECTED_RANKS` pins in lockstep and updates the `// rank:` annotations so the route-rank audit stays green. Keeps Archon-only: rank-setting, version-close, abandon, grader-bypass confirm, and the ADR 0086 cross-tier `POST /dependencies` + goal-scope edges. Amends ADR 0018's table row 7; sits beside ADR 0086, which remains the module-fenced authoring path for sub-Metic ranks. "Tighten later" debt explicitly accepted — no per-version/module/goal fence on a Metic's authoring.](<redacted>.md) | GDS / permissions |
|
|
@@ -164,13 +169,15 @@ This keeps the decision history honest and traceable.
|
|
|
164
169
|
| 0095-a | [Borrowed memory, retrieval & collision concepts from claude-mem (Apache-2.0) and graphify (MIT) — after evaluating both Claude-Code tools and rejecting either wholesale (Python stack / local-daemon+Chroma architecture / no rank-scoping / open-core+memecoin governance), adopt eight stack-agnostic CONCEPTS, reimplemented in-stack with no new dependency or service ("borrow concepts, not tools"): from graphify — symbol-level collision detection (path-prefix→symbol/dependency overlap in `path-match.js`, the forecast-accuracy lever ADR 0049 left open), an AST/tree-sitter repo-map upgrade via the ADR 0063 `extractFile()` seam (polyglot + symbol resolution + a dependency graph that also feeds the ADR 0094 atlas truer edges), and forward-facing "why" extraction (`# WHY:`/ADR rationale into recall + auto-injected on file open); from claude-mem (1–5) — progressive-disclosure recall with token-cost visibility (~10× fewer tokens/query, mechanism-agnostic), a hook graceful-degradation policy (infra-fail→exit 0, code-bug→exit 2), a memory-usefulness feedback loop, a `<private>` content-exclusion marker, and windowed content-hash dedup. Deliberately does NOT decide grep vs. a semantic/embedding layer — a companion owner-authored ADR owns that, and every item here composes with whichever it picks. Declines lifting either codebase, the Chroma/uv dependency, and automatic-capture-as-default (folded into the BFG as suggestion-only).](<redacted>.md) | GDS / memory + retrieval |
|
|
165
170
|
| 0095-b | [Cross-agent context management — adopt docstrings now (meaning written into version-controlled source is the zero-cost, zero-drift core of BOTH grep and semantic findability — no separate index to pay for or keep fresh); record as DEFERRED future-decision content the `AGENTS.md` cross-tool adapter layer (one canonical instructions file + thin per-tool pointers), the greppability / Grep-Test naming conventions, the `AIDEV-NOTE` anchor-comment convention (guarded by a fitness check so it can't rot), a semantic / embeddings layer (local model + embedded store + incremental git-hook reindex, run alongside grep — never instead), and a per-project "retrieval & agent profile" chosen at init that provisions the adapter set. Preserves the cross-agent research (agents navigate by grep not embeddings; Anthropic + Sourcegraph dropped embedding indexes, Cursor only augments grep with them). The portability frame is the ADR 0062 / 0081 core↔host swappable-adapter pattern applied to the agent surface; corroborates ADR 0060's deferred-embeddings gate. One accepted decision; everything else is framing for a future GDS-V4 / Cloud Bongos planning session. Follow-on task 1654 tracks the docstring rollout.](<redacted>.md) | Cloud Bongos / context + retrieval |
|
|
166
171
|
| 0096 | [Require a `credits_reward` (example) value before a task is workable — closes the hole where `credits_reward NOT NULL DEFAULT 0` (migration 003) let a task be promoted→claimed→shipped while paying 0 example. Enforces a present, positive reward at THREE points: the promote route (`POST /tasks/:id/promote` refuses an unrewarded flip to `ready` with an actionable `REWARD_REQUIRED` 409 that SUGGESTS a value from kind/est), the auto-promote predicate (`task_is_fully_unblocked`, migration 177 — adds `(reward_gate_exempt OR credits_reward>0)` so a deps-shipped task with no reward stays `backlog`), and a claim backstop (`db.claimTask` mirrors the `DEPS_NOT_SHIPPED` second-line-of-defence). GRANDFATHERS the ~57 already-`ready` zero-reward tasks via a `tasks.reward_gate_exempt` flag set true for every task at/past `ready` at migration time — no currently-claimable task becomes unclaimable, no `ready` task is retro-demoted (the predicate change is forward-only: triggers never re-evaluate a `ready` row). Keeps `credits_reward` AUTHOR-SET (ADR 0023 anti-gaming): suggests, never auto-applies; setting it stays `PATCH /tasks/:id`. Coordinates with the backfill task (<redacted>, task 1672) to value the grandfathered rows.](<redacted>.md) | GDS / economy + lifecycle |
|
|
167
|
-
| 0097 | [One active claim per session + a claim binds to a dedicated worktree (task 1642) — closes the commingled-ship hole: a single Claude Code session works one git worktree which owns one branch, so claiming a 2nd task from the same worktree lands both tasks' commits on one branch and `/builder-ship` (which publishes the whole branch) drags the unfinished task's work out under the first's ship. The only prior guard (`gateNewcomerClaim`) was Xenos-only, builder-scoped, and fail-OPEN. Adds a SESSION-scoped (keyed on `creator_session_id`, not builder — preserves the many-sessions-per-builder norm), ALL-rank, FAIL-CLOSED block at `POST /claims` (`409 session_already_has_active_claim` naming the in-flight task). NULL-session web claims (`bind_session=false`) are skipped so claim-in-browser/ship-from-any-terminal still works. The decision is a pure exported fn (`shouldRefuseSessionClaim`, DB-free unit-tested); the route middleware is thin. DB defence-in-depth: partial unique index `uniq_active_claim_per_session ON claims(creator_session_id) WHERE released_at IS NULL AND creator_session_id IS NOT NULL` (migration 180 — the real `released_at` convention, NOT the spec's nonexistent `status` column; heals any current violation before CREATE so deploy can't break). Part B: the builder-claim skill default flow now claims into a dedicated worktree (`EnterWorktree task-N`). AMENDS ADR 0049 — adds the within-session invariant 0049 left implicit; the cross-claim split contract is untouched.](<redacted>.md) | GDS / lifecycle |
|
|
172
|
+
| 0097-a | [One active claim per session + a claim binds to a dedicated worktree (task 1642) — closes the commingled-ship hole: a single Claude Code session works one git worktree which owns one branch, so claiming a 2nd task from the same worktree lands both tasks' commits on one branch and `/builder-ship` (which publishes the whole branch) drags the unfinished task's work out under the first's ship. The only prior guard (`gateNewcomerClaim`) was Xenos-only, builder-scoped, and fail-OPEN. Adds a SESSION-scoped (keyed on `creator_session_id`, not builder — preserves the many-sessions-per-builder norm), ALL-rank, FAIL-CLOSED block at `POST /claims` (`409 session_already_has_active_claim` naming the in-flight task). NULL-session web claims (`bind_session=false`) are skipped so claim-in-browser/ship-from-any-terminal still works. The decision is a pure exported fn (`shouldRefuseSessionClaim`, DB-free unit-tested); the route middleware is thin. DB defence-in-depth: partial unique index `uniq_active_claim_per_session ON claims(creator_session_id) WHERE released_at IS NULL AND creator_session_id IS NOT NULL` (migration 180 — the real `released_at` convention, NOT the spec's nonexistent `status` column; heals any current violation before CREATE so deploy can't break). Part B: the builder-claim skill default flow now claims into a dedicated worktree (`EnterWorktree task-N`). AMENDS ADR 0049 — adds the within-session invariant 0049 left implicit; the cross-claim split contract is untouched.](<redacted>.md) | GDS / lifecycle |
|
|
173
|
+
| 0097-b | [Retroactive reward backfill for shipped 0-reward tasks](<redacted>.md) | economy / rewards |
|
|
168
174
|
| 0098 | [The Cloud Bongos public-mirror publish boundary as an executable manifest (`scripts/gds/publish-manifest.js`, `isPublishable()`) — makes ADR 0065 §4's prose boundary something R72 (the delayed redacted export) consumes and R73 (the no-leak proof) asserts against. Default-deny: an allowlist (portable core + platform + neutral branding + sanitized methodology docs) gated by a hard denylist that IMPORTS and partitions the three trust inventories — `PROTECTED_GLOBS` is NOT a denylist, only its topology subset (infra/deploy/CI/local-gate-hooks) is excluded; the rank/permission/ship machinery publishes AS the open-source methodology. Excludes the game + all art (the art subtree carries the ripped Pokémon FireRed reference corpus — an independent copyright reason) + instance identity/config/history + secrets. A unit test cross-checks the partition against the live `PROTECTED_GLOBS` so the carve-out can't silently drift (the gate-review "no allow-by-omission" doctrine applied to publishing). Defers the customer-0 brand pack + desktop-client source to R72/owner (excluded by default until then). No migration — a file-glob boundary has no DB state.](<redacted>.md) | Cloud Bongos / open-source governance |
|
|
169
175
|
| 0099 | [The time-delayed, redacted public-mirror export pipeline (R72, task 1211) — the file-level work ADR 0098 deferred: `scripts/public-mirror-export.js` picks the newest commit ≥180 days old (a lagged SNAPSHOT, not a history replay), partitions its tree through `isPublishable()` (ADR 0098 — the ONLY allow-list), redacts every published file (`scripts/gds/mirror-redact.js`: domain/subdomains → example.com, repo/db/world/company token → example, owner login+aliases → example-owner, public IPv4 **and** IPv6 → REDACTED_IP with localhost/RFC-1918/RFC-5737/link-local/ULA kept) THEN the ADR-0022 secret scrubber (extracted to `modules/security/secret-scrub.js` so it's CI-safe to require without ship.js's heavy deps; ship.js re-exports it). FAIL-CLOSED + atomic: all redaction in memory, a no-leak gate over every blob, and only a fully-clean set touches the mirror FS. Two invariants carry it: (1) SELF-LEAK — the redactor is itself published so it holds NO instance literal, deriving them at runtime from the excluded `config/branding.json`; (2) the no-leak DETECTOR is STRICTER THAN + DECOUPLED FROM redaction — `findLeaks` scans raw literal SUBSTRINGS + IPv4/v6 floors, not the bounded redaction regexes, because a shared word-bounded rule silently missed `example-owner` inside a regex-literal string AND blinded the gate to its own miss. Droplet IP + owner-name aliases are sourced from env in the EXCLUDED workflow yaml. Transport reuses the ADR-0029/[#741](https://example.com/builders#/task/741) scoped-deploy-key push; scheduled weekly; DORMANT no-op until history is 180d old (~2026-11), keeping the mirror empty until R73 lands + the owner flips it public. `.mirror-provenance.json` is the lineage anchor R73 ([#1212](https://example.com/builders#/task/1212)) reuses.](<redacted>.md) | Cloud Bongos / open-source governance |
|
|
170
176
|
| 0100 | [Two-repo instance model — a project consumes Cloud Bongos as a versioned platform dependency (resolves the R77 decision gate, task [#1607](https://example.com/builders#/task/1607)). Chooses the PHYSICAL repo topology [ADR 0062](<redacted>.md) §10 left open: an instance may be its OWN repo installing a pinned core (the `isPublishable()` subtree, [ADR 0098](<redacted>.md)) + layering its own content/branding/modules — not only a second process on one checkout (cloudbongos.com stays a supported, cheaper reskin shape). SEQUENCED + Mercury-driven: build the smallest slice to stand Mercury up as customer 2 on a PINNED core, then let its first core bump define the update channel (`bongos upgrade` is a stub today, [#1203](https://example.com/builders#/task/1203)) — don't design the channel up front; Example is NOT split (stays the single-checkout reference instance, free core updates) until a separate later call. GDS topology = PER-REPO (each project its own DB/tasks/builders/versions; a shared GDS is rejected — it re-couples what the split exists to separate). The generated mirror ([ADR 0065](<redacted>.md) §4 / [ADR 0099](<redacted>.md)) is RETAINED now, retired CONDITIONALLY once the core is physically extracted (then the core repo becomes the canonical public artifact); AGPL + governance + 6-month delay unaffected by topology. Trades free instant updates for a permanent per-project upgrade step + a bigger op footprint, weighed against the $5k budget. Seeds GDS-V4 R84→R85→R86.](<redacted>.md) | Cloud Bongos / architecture |
|
|
171
177
|
| 0101 | [Goal hierarchy Phase 5 — the deferred ADR 0086 non-decisions, recorded with a security review (BV1.R65, task 1519): (1) an ADVISORY-FIRST relevance / anti-hack judge at goal-task create/claim that reuses the ADR 0024 grader subagent harness, flags for a Metic to review, and NEVER hard-blocks a newcomer (the deterministic scope + protected-path + auto-rank-floor gates stay the only hard walls; a fallible LLM as a hard gate would re-introduce the bottleneck ADR 0086 removed + open a gaming/DoS surface); (2) sub-Metic membership inside PROTECTED-scope goals — a genuine widening of the ADR 0043 hard floor, made purely-additive + fail-closed: an Archon grants a tightly-scoped, audit-logged membership and the ship-time gates (pre-push / grader pre-pass / gate-review / main-audit) become GOAL-AWARE on the REAL diff (allow a sub-Metic protected-path hit only if a live Archon-granted goal membership enumerates exactly that path, else the 0043 floor bites unchanged); ships LAST behind Part 1 + an owner Gate sign-off (ADR 0058) + a red-team pass; (3) the goal-reward model — leaning KARMA (not example: real money, tight budget) + CONTRIBUTION-WEIGHTED (not membership-flat: anti-squatting; reuse the shipped-task attribution + BFG signals, ADR 0027) on goal ACHIEVEMENT, with any monetary component + the exact curve owner-gated at build time. Design + security review only; NO enforcement code. Child cluster P5.1 (judge) / P5.2 (goal-aware gates + 0043 amendment) / P5.3 (reward) filed backlog.](<redacted>.md) | GDS / permissions + economy |
|
|
172
178
|
| 0102 | [Finish bounding the kernel — flip fitness Check 12 ("kernel imports no domain") from a WARN-mode ratchet to a HARD gate by clearing all 7 tracked kernel→domain leaks (task [#1707](https://example.com/builders#/task/1707)). (1) `llm-cache.js` + `llm-pricing.js` reclassified as kernel-adjacent infra (pure/injected cost primitives, same category as the pg pool + `path-match.js` — added to `KERNEL_IMPORT_ALLOWLIST`, NOT a new module core would depend on anyway); (2) the identity/auth/admission cluster (`upsertBuilder`/`seatFirstAdminIfUnclaimed`/`getBuilderByGithubId`/`hasInvitedAccessRequest`/`createSession`/`lookupSession`/`SESSION_TTL_MS`/`RANKS`) carved from the `db.js` residue into `db-kernel.js` — `auth.js` (a KERNEL_FILE) resolves them there, `db.js` re-exports for its other callers, the `RANKS` twin now pairs with lifecycle; (3) the doorway's remaining domain re-exports pushed behind ports — Gemini-key storage → art-pipeline, `builderRollingAvgGrade` → the `grade` port; (4) `<redacted>` flipped to `violations` + `hardFail` at zero. Lands incrementally, each verified vs `fitness.js` + the unit suite + a live vanilla `docker compose up` (auth still 401s clean). Supersedes the ADR 0091 §1/§3 + 0093 WARN-mode deferral of the llm-infra carve.](<redacted>.md) | GDS / architecture |
|
|
173
|
-
| 0103 | [Consolidate the two parallel internal versions — GDS-V4 (frozen) folds into BONGOS-V1, now the single go-forward Cloud Bongos PLATFORM version (renamed "Cloud Bongos — platform (V1)", status `building`); its still-relevant open work carries forward. The Bongos DESKTOP APP becomes its own GOAL under BONGOS-V1 (mascot, branded download + auto-update, the cloudbongos.com landing page, feedback capture, the absorbed Dev Box switch — [ADR 0087](<redacted>.md)); the code carve `bongos-app/` → `modules/bongos-app/` is tracked under it. Prunes 19 superseded tasks (the C1 semantic-search/embeddings arm — retired by [ADR 0095](<redacted>.md)'s grep-over-embeddings decision; old V3-era grader tasks superseded by [ADR 0089](<redacted>.md); the T-NNN task-ref migration), moves 2 misfiled game-art tasks to product V2, and severs the 758→753 dependency edge so 758 stays claimable. Executed as reversible data migration `182` (only status/version/goal/edge mutations; in-flight `confirmed`/`active` tasks stay in GDS-V4 to land). `/status` now defaults to BONGOS-V1. Surfaced + fixed a 12-day silent DB-backup outage first (task [#1711](https://example.com/builders#/task/1711)).](<redacted>.md) | Cloud Bongos / methodology |
|
|
179
|
+
| 0103-a | [Consolidate the two parallel internal versions — GDS-V4 (frozen) folds into BONGOS-V1, now the single go-forward Cloud Bongos PLATFORM version (renamed "Cloud Bongos — platform (V1)", status `building`); its still-relevant open work carries forward. The Bongos DESKTOP APP becomes its own GOAL under BONGOS-V1 (mascot, branded download + auto-update, the cloudbongos.com landing page, feedback capture, the absorbed Dev Box switch — [ADR 0087](<redacted>.md)); the code carve `bongos-app/` → `modules/bongos-app/` is tracked under it. Prunes 19 superseded tasks (the C1 semantic-search/embeddings arm — retired by [ADR 0095](<redacted>.md)'s grep-over-embeddings decision; old V3-era grader tasks superseded by [ADR 0089](<redacted>.md); the T-NNN task-ref migration), moves 2 misfiled game-art tasks to product V2, and severs the 758→753 dependency edge so 758 stays claimable. Executed as reversible data migration `182` (only status/version/goal/edge mutations; in-flight `confirmed`/`active` tasks stay in GDS-V4 to land). `/status` now defaults to BONGOS-V1. Surfaced + fixed a 12-day silent DB-backup outage first (task [#1711](https://example.com/builders#/task/1711)).](<redacted>.md) | Cloud Bongos / methodology |
|
|
180
|
+
| 0103-b | [Core-first extraction: Cloud Bongos is the trunk, the instance splits off](<redacted>.md) | core / extraction |
|
|
174
181
|
| 0104 | [Trust the GDS API channel in Claude Code AUTO mode — pre-allow `Bash(node scripts/gds/api.js:*)` in the project `.claude/settings.json`. Completes task [#1051](https://example.com/builders#/task/1051)'s narrow named-script allow-list (from the task-[#1027](https://example.com/builders#/task/1027) nightmare): every autonomous GDS op without a dedicated script — idea-triage promote, create-task, blocker-resolve, criterion ratify, goal create, cost, security reports — rides the GENERIC `api.js` caller, which was never allow-listed, so auto mode's classifier hard-denied the whole autonomous GDS write surface as "[External System Writes] … beyond the user's request" (it only worked under the box's task-[#1251](https://example.com/builders#/task/1251) `bypassPermissions`). Safe because the GDS SERVER enforces `builders.rank` per-request ([ADR 0016](<redacted>.md)) — the classifier was never the gate; the rule grants nothing beyond the caller's rank, and `api.js` can only reach `/api/gds/*`. Scope-discipline stays in the per-builder `wandering` knob. Locked by `tests/box_ship_permissions.mjs`.](<redacted>.md) | harness permissions / dev box |
|
|
175
182
|
| 0105 | [Instance/product seed data out of the core migration path — OTB scope-truth (`versions`/`goals`/`done_when_criteria`) embedded in ~16 core migrations crash-looped a vanilla self-host on a fresh DB (FK violations) or polluted it with OTB's versions/goals; task [#1264](https://example.com/builders#/task/1264) patched only the two crashers (172/173 `DO`-block guards). Structural fix (task [#1704](https://example.com/builders#/task/1704)): the 10 PURE seed files move to a declared `migrations/instance/` dir; `scripts/migrate.sh` applies instance seeds only for the OWNING instance and DEFAULTS OFF (opt-in `GDS_APPLY_INSTANCE_SEEDS=1`; hard-off under the now-revived `MEDUSA_GDS_ONLY`), so a vanilla OR branded self-host never inherits them. Both dirs merge-sort by basename so the global numeric order is preserved (018 before 019) and an OTB from-scratch replay is byte-identical (5 versions / 11 goals / 43 criteria). The unavoidable seed blocks in the mixed files 003/006 guard in place on the `app.seed_instance_data` GUC (achievements + the generic `bfg`/`discord_bot` system principals kept). A `selfhost-boot.yml` CI gate (authored; landing gated on blocker 52 — the App can't merge `.github/workflows/*`) boots a fresh `postgres:16` and asserts a clean vanilla boot with zero OTB rows, closing the C9-style un-gated-proof gap. Verified against a throwaway DB both ways.](<redacted>.md) | Cloud Bongos / architecture |
|
|
176
183
|
| 0106 | [Goal owner/manager authority — refines ADR 0086's membership roles into a fixed hierarchy (task [#1735](https://example.com/builders#/task/1735)): the goal OWNER is `goals.created_by` (a column, not a role row — set at create, moved only by the new owner-only `POST /goals/:id/transfer`; the new owner must be Metic+); MANAGER is the `goal_members.role='lead'` value (rendered "manager" in the hall), owner-equivalent EXCEPT demotion (owner-only) and transfer; the owner's row is never demotable (`owner_role_fixed`), enforced in the pure ordered gate (`authorizeMemberRoleChange`, the authorizeGoalTaskCreate precedent) AND atomically inside the UPDATE (`setGoalMemberRole`). Creators backfill as leads via module migration `<redacted>` — deliberately NO earliest-member fallback (fail-closed; ownerless migration-160 system goals stay Archon-run). No goal delete — archive unchanged. Replaces the task's original leads-as-equals "never below one lead" invariant with the simpler fixed-owner model.](<redacted>.md) | GDS / goal hierarchy |
|
|
@@ -188,8 +195,6 @@ This keeps the decision history honest and traceable.
|
|
|
188
195
|
| 0118 | [API contract source-of-truth: code-first, schema-as-data — the spike (R07) for goal 36 (task [#1994](https://example.com/builders#/task/1994), BONGOS-V1). Decision: STAY code-first; the per-route `validate()` schema object (already structured data: type/required/enum/length caps in `_helpers.js`) is the SINGLE source — the OpenAPI spec, runtime validation, and the typed client are all DERIVED from it, none hand-authored. `gen-api-docs.js` already generates the spec from route source (ADR 0109) but shallowly (212 endpoints; 76 write ops with a requestBody, 24 without; `components.schemas` empty); enrich `extractBody()` to emit full typed schemas from the validate() rules. One schema, three consumers (spec + validation + client) = single-source WITHIN code-first, no drift. Refines R08 (enrich generator → generate+publish client), R09 (migrate callers — also repairs the R04 flat-error reads), R10 (external on-ramp), R11 (reject unknown fields + query params, same schema), R12 (build check: write route ⇒ spec-bound schema). Rejected: schema-first (rewrite the whole 212-endpoint route layer), hand-maintained spec (drift, per ADR 0109), framework swap to Fastify/tRPC (too invasive).](<redacted>.md) | API contract / goal 36 |
|
|
189
196
|
| 0119 | [API pagination contract — the contract (R13) for goal 36's `api-pagination` criterion (task [#2000](https://example.com/builders#/task/2000), BONGOS-V1). Three styles coexisted (limit-only, limit+offset, none) and some public lists (leaderboard, blockers, versions) returned everything (unbounded = cost/DoS surface). Decision: ONE convention — `?limit=&offset=` through a shared `parsePagination(req,{defaultLimit,maxLimit})` helper in `_helpers.js`: limit default 50 / hard-cap 200 (so no list is unbounded), offset default 0, bad/oversized values CLAMPED not rejected (list reads stay forgiving — the strict counterpart is write-validation R11). Response convention `{ items, page:{ limit, offset, total? } }` via `pageMeta()`. Chose limit/offset over cursor (already the in-use style, no opaque-token/stable-sort infra, trivial to curl; cursor left as a per-endpoint escape hatch for churning lists). Opt-in/non-breaking today; R14 ([#2001](https://example.com/builders#/task/2001)) adopts it across every list + caps the unbounded public lists. Rejected: cursor-as-default (disproportionate), page-number (awkward), leaving lists unbounded (the problem), 400-on-bad-limit (a read should be forgiving).](0119-api-pagination-contract.md) | API contract / goal 36 |
|
|
190
197
|
| 0120 | [Pay example on land (not on confirm) + a builder-owned rebase gate (task [#2035](https://example.com/builders#/task/2035), BONGOS-V1). Credits were awarded at the `confirmed` transition — before the code is on `main` — so stranded/never-pushed work paid out with no consequence, and the builder (the only one who can rebase it) had no reason to. Decision: (1) move BOTH the per-task credit AND the cost-plus session reward to the `shipped` (merged-to-`main`) transition; (2) the publish-reconciler flags a confirmed task `needs_rebase` (attributed to the claim holder) once auto-land is exhausted — merge conflict, failing required check vs current `main`, or no pushed branch; (3) a `needs_rebase` task blocks that builder's NEW claims (`REBASE_REQUIRED`) but not their existing/in-flight ones; (4) a one-time migration claws back the `task.confirmed` credits on the current confirmed backlog and re-gates it. Happy path unaffected (a clean PR auto-lands in minutes and pays then); the teeth bite only on stranding. Rejected: gate-only (stranded work still paid), soft-warn (ignorable), grandfather the backlog (leaves today's stranded-and-paid state), per-task-credit-only (owner chose fullest alignment).](<redacted>.md) | economy + lifecycle |
|
|
191
|
-
| 0117 | [API versioning + deprecation policy — the policy (R05) for goal 36's `api-versioning` criterion (task [#1992](https://example.com/builders#/task/1992), BONGOS-V1). The API (212 endpoints at `/api/bongos` + permanent `/api/gds` alias) had no version marker, no breaking-change definition, no deprecation window — any change could silently break a self-hoster/shipped Dev Box binary/status dashboard. Decision: (1) an explicit path segment `/api/bongos/v1/…` is the **contract** version, distinct from `info.version` (the package semver); (2) today's unversioned paths become **permanent aliases pinned to v1** — the load-bearing rule is *an unversioned path never silently jumps major versions*, so when v2 mounts at `/v2/` the alias stays on v1 and breaking changes reach only opted-in callers; (3) breaking = remove/rename/retype a field/endpoint/error-code, new required field, tightened validation, narrowed auth, changed default — non-breaking = additive fields/endpoints/optional-params + new error codes *within an existing ADR-0116 category*; (4) a superseded version lives **≥180 days** with RFC 8594 `Deprecation`/`Sunset` headers, the unversioned alias exempt (permanent); (5) the version is discoverable via OpenAPI `servers` + `x-api-version` + the machine-discovery index + `/docs`. **Policy only** — R06 ([#1993](https://example.com/builders#/task/1993)) mounts `/v1` + aliases the unversioned paths (day-one no-op). Rejected: header-based versioning (invisible in URL, un-curl-able), semver-in-path (too fine), reusing the package semver (conflates build with contract), sunsetting the unversioned alias (breaks un-updatable binaries).](<redacted>.md) | API contract / goal 36 |
|
|
192
|
-
| 0116 | [API error envelope + code taxonomy — the contract (R01) for goal 36's `api-error-envelope` criterion (task [#1988](https://example.com/builders#/task/1988), BONGOS-V1). An audit found **352 distinct `error:'…'` string literals** returned across 400/401/403/404/409/413/422/429/500/502/503 in ~4 different response shapes — a caller cannot write one error-handling path. Decision: **one envelope everywhere** — `{error:{code,message,details?}}` — where the specific `code` slug is **preserved** (a caller keying on `task_not_found` keeps working) and the 352 codes **collapse by category, not by discard**. A kernel registry `src/bongos/api-errors.js` defines ~12 canonical categories (each with a default HTTP status + `retriable` flag) and a pure/total `classify(code)` — an `OVERRIDES` table for the ~90 exceptions (auth codes, preconditions-as-conflicts, `*_too_large`) plus a suffix/prefix heuristic for the long tail (`*_failed`→internal, `*_not_found`→404, `bad_*`/`*_required`→400, `already_*`/`*_taken`→409, `*_unconfigured`→503); an unknown/future code falls back to `internal` (500). Ships the `ApiError` carrier + `envelope()` formatter + `statusForCode(code,override)` (a site keeps its current status; the sweep changes shape, not status). **Contract-only — nothing imports it yet, zero behaviour change.** R02 ([#1989](https://example.com/builders#/task/1989)) wires the middleware + `res.fail()`; R03 ([#1990](https://example.com/builders#/task/1990)) fails the build on a new hand-rolled flat error; R04 ([#1991](https://example.com/builders#/task/1991)) sweeps the ~219 handlers behind that gate. Rejected: flat `{error,message}` (today's mess, no room to grow), numeric codes (opaque), collapsing to 12 codes only (loses signal, breaks callers), per-module registries (defeats "one shape").](<redacted>.md) | API contract / goal 36 |
|
|
193
198
|
| 0122 | [Onboarding diagram renders: untracked + rendered at deploy from a host-provisioned Chrome (task [#2027](https://example.com/builders#/task/2027), BONGOS-V1) — completes the diagram half [ADR 0110](<redacted>.md) deferred. The `.svg`/`.png` renders were committed on every render-capable ship and kept colliding on parallel branches; ADR 0110 deferred them because `mermaid-cli`/puppeteer downloads its own Chromium on first run (a multi-minute fetch that stalls the 2 GB droplet). Decision: provision Google Chrome once on the host; `render-diagrams.sh` auto-detects an installed browser (or honors `PUPPETEER_EXECUTABLE_PATH`) and sets `PUPPETEER_SKIP_DOWNLOAD` so it never fetches (falls back to the download only on a dev laptop), passing a committed `puppeteer-config.json` with `--no-sandbox` (Ubuntu 24.04 restricts unprivileged user namespaces) and pinning `mermaid-cli@11`; deploy-prod.sh + box-source-fetch render from the tracked `.mmd` (LOUD `::ERROR::`, best-effort); `git rm --cached` + gitignore the `*.svg`/`*.png` (the `.mmd` + `assertions.json` stay tracked, so the nightly diagram-drift cron is unaffected); ship runs `gen-diagrams --no-render`. Verified rendering all 5 diagrams on the prod droplet before landing. Rejected: on-deploy Chromium download (the droplet stall), snap chromium (flaky headless), keep-tracked + strengthen the ADR 0082 resolver (hides the collision).](<redacted>.md) | internal / ADR 0110 |
|
|
194
199
|
| 0121 | [Greenfield vs. brownfield onboarding — scope `bongos init --adopt` (task [#1986](https://example.com/builders#/task/1986), BONGOS-V1, goal 35's `adopt-existing-repo` criterion). Today the creation flow is greenfield-only: `scripts/gds/init.js` opens *"stand up a NEW instance"*, refuses to run where `config/branding.json` exists (`--force` overwrites), and seeds a synthetic `"V1 — Prove the loop"`; it inspects nothing about an existing tree. Decision: TWO explicit modes over goal 35's one canonical step sequence (task 1981), branching at exactly the SCAFFOLD leg — greenfield (`bongos init`, today) vs brownfield (`bongos init --adopt`); every other leg (provision, OAuth, verify, brand) identical, so the CLI (1982) + hall wizard (1983) forms inherit adopt for free. `--adopt` must: (1) LAYER not clobber — add the pinned `@bongos/core` dep + lean `config/` + `.claude/` additively, merge-or-report on pre-existing files, never reuse the `--force`-overwrites path; (2) reflect REAL state — capture the repo's actual version/goal instead of the synthetic V1, import the existing backlog (GitHub issues / TODO / roadmap) into GDS tasks; (3) DETECT the existing stack + layout so modules / repo-map / fitness-boundary fit; (4) a CONFLICT PRE-FLIGHT report (migration namespace, fitness boundary, protected-path / rank gates, existing CI), `--dry-run` default, gated behind an accept step. Scope-only — no adopt code ships; seeds a 5-task dependency-wired breakdown. Rejected: overloading `--force` to merge (footgun), a separate `bongos adopt` command (leg duplication / drift), auto-import with no pre-flight, building adopt-mode now (1986 is scope-only).](<redacted>.md) | Cloud Bongos / onboarding |
|
|
195
200
|
| 0123 | [Idle-swept dev boxes lose uncommitted work: an on-box autosave-push, not a VM snapshot (task [#837](https://example.com/builders#/task/837) SPIKE). `box.js`'s idle sweep hard-deletes the droplet once a box reads idle — nothing on that path checks git status, and park's snapshot was deliberately removed from the automated path for cost. Decision: a new on-box cron (sibling to `box-heartbeat.sh`) that, when `/workspace` is dirty, commits + pushes to the box's own `claude/<name>` claim branch using the same one-shot-credentialed-then-scrubbed pattern `box-source-fetch.sh` already uses for fetch — bounding loss to the autosave interval (~5 min) instead of unbounded. Open dependency: confirm/mint a push-scoped box credential (today's `BOX_SOURCE_FULL_TOKEN` is fetch-only by convention). Rejected: reintroducing snapshot-before-destroy (the exact cost the team removed), a control-plane git-status check before sweep (needs new box-exec capability, would pin dirty boxes active forever), relying on `/builder-end`'s manual dirty-check alone (doesn't fire on the automatic sweep path at all). Scope-only — no autosave code ships; seeds a follow-up implementation task.](<redacted>.md) | internal / dev-box |
|
|
@@ -214,16 +219,20 @@ This keeps the decision history honest and traceable.
|
|
|
214
219
|
| 0140 | [cloudbongos.com re-provisioned to the ADR 0108 standalone model, DB-PRESERVING — closing ADR 0139's skipped WS-0 (executed 2026-07-12). The site was a lift-and-shift clone of the `example` repo frozen on stale code — every `/builders/*` asset 404'd — because its pull-deploy silently no-op'd against the unchanged game repo (`origin` was `example-owner/example`, not a cloudbongos instance); it had never run the restored code. WS-0's original fresh-DB re-provision would now WIPE the migrated history/balances, so a build-alongside-then-flip variant was used: a dedicated private `cloudbongos/cloudbongos-instance` repo (lean config over the neutral brand + a pinned `@bongos/core@1.17.3`), materialized beside the untouched old checkout, proven on a spare port against the live DB (migrate = clean no-op; leaderboard read migrated balances), then `cloudbongos.service` flipped to the instance + engine and the ADR 0129 pull-timer repointed at the instance repo. Engine source stays authoritative at `example-owner/cloud-bongos` (not relocated); verified DB backup + retained old checkout/unit/deploy-script give instant rollback. Enabled org-wide deploy keys on the `cloudbongos` org so the box's read-only deploy key could attach.](<redacted>.md) | infra / Cloud Bongos |
|
|
215
220
|
| 0141 | [Cross-site identity — cloudbongos.com becomes a federated identity provider (the "hub") so one platform account reaches any hosted project's builders hall, browser AND CLI, with no second GitHub sign-in (Goal 41 design of record; task [#1002082](https://cloudbongos.com/builders#/task/1002082); implemented across [#1002106](https://cloudbongos.com/builders#/task/1002106)–[#1002115](https://cloudbongos.com/builders#/task/1002115)). Core principle: **identity is federated, authority is not** — the hub carries identity ONLY (github_id, login, display name, avatar), never rank/credits/admission, so [ADR 0016](<redacted>.md)'s self-owned-authority boundary is preserved by construction. Federation is opt-in + reversible via a config switch (`auth.idp`): a hosted instance provisioned with `auth.idp` + `<PREFIX>_HUB_*` routes web sign-in through the hub's `/sso/authorize`→`/sso/token` (OAuth authorization-code, single-use 60s codes, server-to-server client-secret redemption) and CLI sign-in through the hub's device flow + an audience-scoped Ed25519-signed assertion verified offline at a new `/auth/device/exchange`; a self-hosted instance leaves `auth.idp` unset and keeps owning its own GitHub app (the direct-GitHub path, unchanged fallback). Adds a hub `platform-identity` module (accounts / sso_clients / sso_authorization_codes / projects / project_memberships / builder_project_activity), a project→hub membership check-in inside upsertBuilder (coarse role, never rank), a My Projects view with project-executed join/leave, a curated public Explore showcase, per-builder cross-project activity rollups, and an Archon-only cross-project scouting view (upgrading the coarse [ADR 0127](<redacted>.md) fleet gauges to a github_id-keyed store). Two credential types by design: per-client hashed secret (instance→hub server-to-server auth) + a hub Ed25519 keypair (CLI assertions, asymmetric so one instance can't forge for another). Was scoped as "ADR 0135"; that number was reassigned, so it lands here as 0141.](<redacted>.md) | auth / Cloud Bongos identity |
|
|
216
221
|
| 0142 | [Project context is prose-only; the structured host-identity contract is branding (audit H11 / finding F7; task [#1002224](https://example.com/builders#/task/1002224), goal 1000042). Branding flows as a structured neutral⊕instance→validated-resolver contract because CODE reads those strings; project context (vision/world/visual-style in `docs/project-context.md`, scaffolded from the shipped `docs/project-context.template.md`) is deliberately narrative — read by AI agents, never parsed by code — so it stays prose-only with no parallel resolver/schema/env layer (YAGNI: no consumer; branding already covers code-read strings; the publish boundary + the CLAUDE.md/AGENTS.md stub already enforce the split at the file level per [ADR 0062](<redacted>.md) §7). A new host value that code must read goes in branding, not a new project-context resolver. Revisit only when code (not agents) must read project context as validated, structured data.](<redacted>.md) | core / host-split |
|
|
217
|
-
| 0144 | [Federated single-logout: hub-initiated back-channel session revocation (task [#1002238](https://cloudbongos.com/builders#/task/1002238), goal 41; follow-up to [ADR 0141](<redacted>.md)). ADR 0141 gave single sign-ON but not sign-OFF — a federated instance mints its own local session after the SSO handshake, so hub logout left every project still signed in. Owner chose **back-channel** (server-to-server) over front-channel (browser-driven, breaks under third-party-cookie blocking, incomplete if the tab closes) and soft-sessions (per-request hub re-check — too heavy, hard hub dependency). Core `POST /auth/logout` emits a `builder.logout` seam EVENT (no-op unless a module listens); the hub `platform-identity` module reacts by minting a **hub-signed EdDSA logout token** (`mintHubLogoutToken` — same envelope/key/~120s-TTL/aud-binding as the login assertion) per entered project and POSTing it to each `{origin}/api/gds/auth/backchannel-logout`; that core endpoint (dormant unless `idpConfigured()`, else 404) verifies via `verifyHubLogoutToken` against the hub pubkey and calls `revokeAllSessionsForBuilder`. The token carries only `{sub, purpose:"backchannel-logout"}` — **never** identity/rank (ADR 0016 boundary holds); the **`purpose` marker makes login assertions and logout tokens mutually non-interchangeable** (a login assertion lacks purpose → rejected as logout; a logout token lacks github_login → rejected as login). Entered-instance list = existing `<redacted>` (over-notify is safe — revoke is idempotent); fan-out is best-effort, per-instance 4s timeout, `Promise.allSettled`-isolated, never blocks/fails the logout (hub session revoked synchronously). No new schema, no doorway change; core `1.18.5→1.18.7`; re-vendors BOTH hub + federated instances. V1 = hub-initiated only (instance→hub propagation + a precise per-session ledger + unreachable-instance retry are follow-ups).](<redacted>.md) | auth / Cloud Bongos identity |
|
|
218
|
-
|
|
|
222
|
+
| 0144-a | [Federated single-logout: hub-initiated back-channel session revocation (task [#1002238](https://cloudbongos.com/builders#/task/1002238), goal 41; follow-up to [ADR 0141](<redacted>.md)). ADR 0141 gave single sign-ON but not sign-OFF — a federated instance mints its own local session after the SSO handshake, so hub logout left every project still signed in. Owner chose **back-channel** (server-to-server) over front-channel (browser-driven, breaks under third-party-cookie blocking, incomplete if the tab closes) and soft-sessions (per-request hub re-check — too heavy, hard hub dependency). Core `POST /auth/logout` emits a `builder.logout` seam EVENT (no-op unless a module listens); the hub `platform-identity` module reacts by minting a **hub-signed EdDSA logout token** (`mintHubLogoutToken` — same envelope/key/~120s-TTL/aud-binding as the login assertion) per entered project and POSTing it to each `{origin}/api/gds/auth/backchannel-logout`; that core endpoint (dormant unless `idpConfigured()`, else 404) verifies via `verifyHubLogoutToken` against the hub pubkey and calls `revokeAllSessionsForBuilder`. The token carries only `{sub, purpose:"backchannel-logout"}` — **never** identity/rank (ADR 0016 boundary holds); the **`purpose` marker makes login assertions and logout tokens mutually non-interchangeable** (a login assertion lacks purpose → rejected as logout; a logout token lacks github_login → rejected as login). Entered-instance list = existing `<redacted>` (over-notify is safe — revoke is idempotent); fan-out is best-effort, per-instance 4s timeout, `Promise.allSettled`-isolated, never blocks/fails the logout (hub session revoked synchronously). No new schema, no doorway change; core `1.18.5→1.18.7`; re-vendors BOTH hub + federated instances. V1 = hub-initiated only (instance→hub propagation + a precise per-session ledger + unreachable-instance retry are follow-ups).](<redacted>.md) | auth / Cloud Bongos identity |
|
|
223
|
+
| 0144-b | [Re-home the dev-box fleet onto the cloudbongos.com plane (native runners, local DB, isolated fleet tag)](<redacted>.md) | infra / dev boxes |
|
|
224
|
+
| 0145-a | [A free "own-repo" hosted project tier can NOT be a bare `cloudbongos.com` subdomain running the owner's code — a pre-build security review found it unsafe two ways (owner design review, [task 1002249](https://cloudbongos.com/builders#/task/1002249); follow-up to [ADR 0141](<redacted>.md)/[0143](<redacted>.md)). (1) The hub `gds_session` cookie is `Domain=.cloudbongos.com`-scoped (so `builders.`/`status.` share one sign-in), so an owner-controlled `<slug>.cloudbongos.com` instance would receive every signed-in visitor's live hub token → account takeover; `httpOnly`/`sameSite:lax` don't help (same registrable domain = same site). The co-tenant instances that safely federate on cloudbongos.com subdomains (e.g. `demo.`) do so only because they run PLATFORM code, not owner code — so the precedent doesn't extend. (2) A control-plane-local `standalone` runs owner code on the hub box as `lars` with passwordless sudo in its `npm ci` deploy → root-on-hub (pre-existing, independent of federation). The federation protocol itself is sound (origin-bound, asymmetric Ed25519); the weakness is PLACEMENT + EXECUTION. **Decision:** a free own-repo tier is delivered ONLY behind (a) domain separation — free instances under a different registrable domain than the hub, so they never get the cookie — AND (b) per-project isolation (own droplet / container, distinct UID, `--ignore-scripts`, no passwordless sudo); only then is federation safe to extend. Owner chose this direction over the co-tenant free tier (safe/$0 but no own repo) and the honest-copy status quo. Build blocked on owner decisions: which separate domain, droplet-vs-container isolation, and the cost model (paid tier vs capped subsidy — [§4](../../CLAUDE.md)). Points 1&2 of the review (repo-step polish) were unrelated + already shipped ([task 1002247](https://cloudbongos.com/builders#/task/1002247)).](<redacted>.md) | auth / provisioning / Cloud Bongos identity |
|
|
225
|
+
| 0145-b | [The Dev Box app becomes a branding-driven module (one source, per-instance builds)](<redacted>.md) | dev-box app / modules |
|
|
219
226
|
| 0147 | [Carry ADR 0110's deploy-regen of generated docs to STANDALONE instances ([task 1002314](https://cloudbongos.com/builders#/task/1002314); the owner's "docs didn't transfer from primea"). The monorepo keeps its generated nav docs live because ADR 0110's regen is wired into `deploy-prod.sh`; standalone instances ([ADR 0108](<redacted>.md)) never got it — their bespoke pull-deploy regenerated nothing and `init`/`upgrade` only did the API docs, so cloudbongos.com's `docs/` was frozen with just the 3 API files. Two defects: (1) `gen-session-index.js` hardcoded the CORE root (`path.resolve(__dirname,'..','..')`) so on a standalone box it indexed `node_modules/@bongos/core`, not the instance — now `resolveInstanceRoot()`; (2) no standalone regen path. Fix: `upgrade.regenerateNavDocs()` + a `regen-instance-docs.js` CLI, wired at every refresh point (init seed, `bongos upgrade`, `provision.js` standalone deploy, and a bespoke `deploy.sh` one-liner); `INSTANCE_GITIGNORE` ignores the whole-file docs. Scope is `session-index` ONLY — the one gitignored, whole-file, instance-content nav doc; `repo-map` is core-scoped (empty for a code-less standalone; brownfield map = task 1001655), `file-map` + API docs are tracked/committed (file-map seeding at standup = follow-up), diagrams are the headless-browser exception.](<redacted>.md) | docs pipeline / Cloud Bongos |
|
|
220
227
|
| 0146 | [Configurable reward mode — an instance chooses whether a shipped task pays BOTH reward streams or ONLY the cost-plus one ([task 1002270](https://cloudbongos.com/builders#/task/1002270), goal 1000037; follow-up to [ADR 0120](<redacted>.md)). A shipped task pays two streams onto one balance (the [ADR 0054](<redacted>.md) "two conventions, one ledger" tension): the per-task author ESTIMATE (`credits_reward` × kind multiplier — unitless, [ADR 0023](<redacted>.md)) AND the cost-plus SESSION reward (`round(true_cost_usd × 1.20)`, 1 credit = $1 — ADR 0054). New `branding.reward.mode`: `"cost-plus-and-estimate"` (DEFAULT — pay both, exactly today's behavior; every existing instance unchanged on upgrade) or `"cost-plus-only"` (the cost-plus reward is the SOLE equity reward; the per-task estimate PAYOUT is withheld and its idea-promotion slice cascades to 0). Resolved by a pure `rewardPolicy()`; gated in the single per-task credit writer (`insertMultipliedConfirmCredit`) — entirely inside the economy module, ZERO lifecycle changes. `credits_reward` still rides the task row for display + the no-zero-reward claim gate; only the payout is suppressed. Unknown mode → default (a typo can't silently zero rewards). Cloud Bongos selects `cost-plus-only`; achievements + the net-negative +1 bonus are out of scope. Backward-compatible + reversible — no clawback, no migration.](<redacted><redacted>.md) | GDS / economy |
|
|
228
|
+
| 0148 | [Task-scoped box source access (rank-scope → claim-scope)](<redacted>.md) — paused by ADR 0193 | dev boxes / source scope |
|
|
221
229
|
| 0149 | [`bongos dev` — a one-command LOCAL launcher for a standalone instance repo (owner ask, 2026-07-19: "we need a better option to stand up the local host"). A freshly-created project's `config/branding.json` points at `http://localhost:<port>` — configured to run on the owner's machine — but the platform shipped no command to START it there; `bongos onboard` provisions a CLOUD server and Docker serves a vanilla brand on `:3000`. Standing it up by hand was a six-part assembly (Postgres → createdb → migrate → PORT/PGDATABASE/PGHOST/OAuth env → server → login) with three silent mismatches: the server reads PORT from env not branding (platform-server.js:44); the pool DB name collapses to the OS user while migrate.sh defaults to `example` (pool.js:47 vs migrate.sh:52) unless PGDATABASE is set for BOTH; and node-pg defaults to the Linux peer socket `/var/run/postgresql` (pool.js:46), absent under Homebrew on a Mac. `bongos dev` does the whole thing idempotently from one source of truth (branding): ensures Postgres, createdb, migrate (`MEDUSA_GDS_ONLY=1`), starts the server on the branding port, and signs in. The background server is MANAGED (pid/log per instance) with `bongos dev stop`/`status` — the fix for the orphaned-server accident (a stale `:3005` answering a new project's login). Pure resolution + process mgmt in `scripts/gds/dev-lib.js` (unit-tested `tests/dev_lib.mjs`); orchestration in `scripts/gds/dev.js`. Alternatives rejected: cloud dev box (needs hosting + cost — kept for remote work), Docker (vanilla brand/wrong port — kept for Windows), recipe-only (leaves the six steps). Not bundled into the standalone on-ramp binary (needs a real checkout).](<redacted>.md) | Cloud Bongos / dev experience |
|
|
222
230
|
| 0150 | [Box first-boot bring-up on vendored instances — resolve `infra/` from the instance root + fail loud on empty user-data (spike BV1.R97, [task 1002385](https://cloudbongos.com/builders#/task/1002385); C2 of goals 1000051/1000052, "make cloudbongos boxes stop coming up bare"). Root cause: `box.js` sets `REPO_ROOT` two dirs up from itself, which on a VENDORED instance is the `@bongos/core` package — but the R89 packaging split kept `infra/` (the DO cloud-init template + inlined `box-source-fetch.sh`/cloudflared/ttyd/heartbeat scripts) as INSTANCE content, not in the core publish-manifest. So `readCloudInit` reads a missing template, its `try/catch` returns `null`, and the caller's only guard (`0 > 64KiB`) passes → the box is created with EMPTY user-data and boots bare Ubuntu (no clone, no toolchain, no terminal); `fileGzB64` is likewise fail-soft (warns, returns `''`) so a partial `infra/` half-wires. Live re-confirmed 2026-07-19: instance-root `infra/` present, vendored-core `infra/` gone (the [ADR 0144](<redacted>.md) node_modules workaround wiped by a reinstall), fleet idle so the bug is latent-but-live (next provision boots bare). Fix (R98, [task 1002386](https://cloudbongos.com/builders#/task/1002386)): a `resolveBoxInfraDir()` locating `infra/` via [ADR 0108](<redacted>.md)'s `resolveInstanceRootExplicit()` (cloudbongos `bongos.config.json` = `{"instanceRoot":"."}`) + an env override + a walk-up-out-of-`node_modules` fallback, AND fail-loud backstops (throw on missing template; abort provision on empty user-data). R99 ([task 1002387](https://cloudbongos.com/builders#/task/1002387)) then ensures the toolchain builds on the box. Reinstall-proof + portable; core-only, no migration/doorway change.](<redacted>.md) | GDS / dev boxes |
|
|
223
231
|
| 0151 | [Governance: **permissions are the atom, ranks become seeded roles** — the foundation of the BONGOS-V1 Governance goal (1000030), spike BV1.R90 ([task 1001838](https://cloudbongos.com/builders#/task/1001838)); settles the model in an ADR before R93–R114 build. Authority stops being an all-or-nothing rank threshold ([ADR 0016](<redacted>.md)/0018/0034) and becomes granular **permissions** (code constants that map 1:1 to real routes/surfaces — a permission no route consumes doesn't exist), bundled into assignable **roles**; the four live ranks become four seeded roles whose grants reproduce today's thresholds EXACTLY (the compat seed — cumulative nested supersets, so the resolver's UNION preserves the at-or-above threshold), and `builders.rank` stays as the identity/graduation column. A per-request **uncached** `resolveBuilderPermissions()` (kernel port, preserving ADR 0016's instant-demotion) + a `requirePermission()` gate that coexists with `requireRank` via the seed → gates migrate route-by-route, no flag day. The **complete authority model** (C3) represents four axes as first-class types, not rank special-cases: rank-seeded roles, machine **principals** (`system_role`=bfg/discord_bot, exact-identity gate, never UI-grantable), **session-scopes** (box tokens, deny-by-default allow-list intersected with the builder's roles), and **ownership-scoped** perms (`X.manage.own` ≠ `.any`; goal owner/manager per [ADR 0106](<redacted>.md)). The safety keystone (C6): every permission carries a **`system` flag** — `system:true` (deploy, protected-path, rank-set, version-close, grade-bypass, cross-tier edges) is **non-grantable** and LOCKED in the tab, so no configurable role can hand out the crown jewels; and the today-hardcoded `PROTECTED_GLOBS`/`EXPECTED_RANKS` map becomes a **governance-owned protected-surface registry** that the OUT-OF-BAND substrate (pre-push hook, grader pre-pass, `main-audit.js`, gate-review — [ADR 0043](<redacted>.md)) READS but never moves in-process: **policy owns / substrate enforces** — the tab can re-map the floor but a forced `system` grant still can't deploy (abuse-matrix + live-demo proofs, R112/R114). Capability in **core** (`modules/governance/`, default:true); role templates are per-instance **config** (`config/governance.json` over `config/governance.neutral.json` over env, mirroring `src/branding.js`), live assignments in the DB. Collapses the ~6 duplicated numeric rank-tier maps (the Builder-25-incident drift class) behind the resolver. Zero day-one behavior change (parity test R103); one deliberate fail-closed divergence (unwired divine ranks map to no role instead of latently inheriting Archon). Includes the full enumerated permission catalog (every key + `system` flag) grounded in the 2026-07-24 authority sweep. No new recurring cost. **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](<redacted>.md) | governance / permissions / Cloud Bongos |
|
|
224
|
-
| 0152 | [The landing gate is served by the **core app**, not by the front proxy — the cutover step of the landing migration ([task 1002411](https://example.com/builders#/task/1002411), L05 of goal 1000035). The reference instance's orb-gate front page was `file_server`'d by Caddy straight out of a checkout of an UNRELATED product repo (`root * <checkout>/brand/cloud-bongos`), so the landing had two sources of truth, no deploy path of its own, and a cross-product coupling the core could not express. [L01](https://example.com/builders#/task/1002407) had already ported the bundle into the core as `public-landing/` and [L02](https://example.com/builders#/task/1002408) had already taught `platform-server.js` to serve it (`/` -> 302 `/gate/`, `/gate/` serves the index raw, `express.static` covers the assets) — so by the time L05 came up, BOTH halves of its original plan ("give the landing (a) its own Caddy config and (b) its own deploy script") had dissolved. **Decision:** the app owns the landing end to end; the proxy drops `root` + `file_server` and keeps exactly three jobs on that surface — TLS, an optional pre-launch `basic_auth` lock, and the `@hidden` guard that keeps `gate/README.md` unreachable. No separate Caddy config to write (the resulting block IS the generic pure-proxy shape `provision.js`'s `caddyBlock()` already emits per instance, [ADR 0111](<redacted>.md) §4; host-specific hostnames/certs stay on the host per [ADR 0062](<redacted>.md) §7) and no separate deploy script to write (the landing ships INSIDE the pinned core package, so it rides the instance's existing pull-deploy — one artifact, one version, one path). Keeps `redir / /gate/ 302` because Caddy orders `redir` before `basic_auth`, so "/" behaves identically before and after, and the app issues the same 302 once the lock is lifted. Cost accepted: the landing now sits in the app's request path (a dead app = a dead front page — but the gate's whole purpose is to reach the app), and a landing tweak now needs a core release. Verified on the live instance: identical status codes across the apex and both vanity doors before/after, relative redirects (no origin leak), and a **severance test** — with the foreign repo's landing folder moved aside entirely the site still served the byte-identical gate, proving the dependency is cut rather than merely unused. Retiring the foreign copy is L06 ([task 1002412](https://example.com/builders#/task/1002412)).](<redacted>.md) | platform serving / instance infra |
|
|
225
|
-
| 0152 | [**Metic gains task abandonment, paired with an un-abandon at the same rank** (owner directive 2026-07-27: "Metics should also be able to abandon tasks"; [task 1002476](https://cloudbongos.com/builders#/task/1002476)). [ADR 0090](<redacted>.md) moved task authoring (create/edit/promote/deps/criteria) down to the trusted working rank but kept `POST /tasks/:id/abandon` Archon-only as "terminal scope removal" — so the rank that runs idea-triage, seeds the tasks, and shapes them still had to hand every duplicate/moot/superseded chore back to the owner to retire. That hand-back is janitorial, not adjudication. The real hazard was never the grant: abandon was **one-way through the API** (`/promote` took only backlog/blocked, `PATCH` refuses `status`), so a mis-retire needed hand-SQL on prod, and an abandoned task is invisible in every claimable list and every version rollup (migration 017 excludes it from `version_progress`) — nobody notices until the work silently doesn't happen. So the gate moves to `requireRank('metic','archon')` **and** `/promote` gains a third source state, `abandoned → backlog` (never straight to ready — a retired task re-enters un-workable and takes a second deliberate promote), backed by `db.restoreTask`, the mirror of `abandonTask`, which clears the `'Abandoned: <reason>'` stamp and only that. Unchanged: the shipped-task and active-claim refusals, idempotency, and the `audit_log` record of who retired what and who brought it back. The `EXPECTED_RANKS` lifecycle pin moves in lockstep (a *further* downgrade stays a hard CI fail); under [ADR 0151](<redacted>.md) the `task.abandon` floor moves archon→metic, landing as `governance_003` because the compat seed is run-once — purely additive, and the drift guard now compares the **union** of grant migrations to the catalog and fails loudly on a future revocation.](<redacted>.md) | GDS / permissions |
|
|
232
|
+
| 0152-a | [The landing gate is served by the **core app**, not by the front proxy — the cutover step of the landing migration ([task 1002411](https://example.com/builders#/task/1002411), L05 of goal 1000035). The reference instance's orb-gate front page was `file_server`'d by Caddy straight out of a checkout of an UNRELATED product repo (`root * <checkout>/brand/cloud-bongos`), so the landing had two sources of truth, no deploy path of its own, and a cross-product coupling the core could not express. [L01](https://example.com/builders#/task/1002407) had already ported the bundle into the core as `public-landing/` and [L02](https://example.com/builders#/task/1002408) had already taught `platform-server.js` to serve it (`/` -> 302 `/gate/`, `/gate/` serves the index raw, `express.static` covers the assets) — so by the time L05 came up, BOTH halves of its original plan ("give the landing (a) its own Caddy config and (b) its own deploy script") had dissolved. **Decision:** the app owns the landing end to end; the proxy drops `root` + `file_server` and keeps exactly three jobs on that surface — TLS, an optional pre-launch `basic_auth` lock, and the `@hidden` guard that keeps `gate/README.md` unreachable. No separate Caddy config to write (the resulting block IS the generic pure-proxy shape `provision.js`'s `caddyBlock()` already emits per instance, [ADR 0111](<redacted>.md) §4; host-specific hostnames/certs stay on the host per [ADR 0062](<redacted>.md) §7) and no separate deploy script to write (the landing ships INSIDE the pinned core package, so it rides the instance's existing pull-deploy — one artifact, one version, one path). Keeps `redir / /gate/ 302` because Caddy orders `redir` before `basic_auth`, so "/" behaves identically before and after, and the app issues the same 302 once the lock is lifted. Cost accepted: the landing now sits in the app's request path (a dead app = a dead front page — but the gate's whole purpose is to reach the app), and a landing tweak now needs a core release. Verified on the live instance: identical status codes across the apex and both vanity doors before/after, relative redirects (no origin leak), and a **severance test** — with the foreign repo's landing folder moved aside entirely the site still served the byte-identical gate, proving the dependency is cut rather than merely unused. Retiring the foreign copy is L06 ([task 1002412](https://example.com/builders#/task/1002412)).](<redacted>.md) | platform serving / instance infra |
|
|
233
|
+
| 0152-b | [**Metic gains task abandonment, paired with an un-abandon at the same rank** (owner directive 2026-07-27: "Metics should also be able to abandon tasks"; [task 1002476](https://cloudbongos.com/builders#/task/1002476)). [ADR 0090](<redacted>.md) moved task authoring (create/edit/promote/deps/criteria) down to the trusted working rank but kept `POST /tasks/:id/abandon` Archon-only as "terminal scope removal" — so the rank that runs idea-triage, seeds the tasks, and shapes them still had to hand every duplicate/moot/superseded chore back to the owner to retire. That hand-back is janitorial, not adjudication. The real hazard was never the grant: abandon was **one-way through the API** (`/promote` took only backlog/blocked, `PATCH` refuses `status`), so a mis-retire needed hand-SQL on prod, and an abandoned task is invisible in every claimable list and every version rollup (migration 017 excludes it from `version_progress`) — nobody notices until the work silently doesn't happen. So the gate moves to `requireRank('metic','archon')` **and** `/promote` gains a third source state, `abandoned → backlog` (never straight to ready — a retired task re-enters un-workable and takes a second deliberate promote), backed by `db.restoreTask`, the mirror of `abandonTask`, which clears the `'Abandoned: <reason>'` stamp and only that. Unchanged: the shipped-task and active-claim refusals, idempotency, and the `audit_log` record of who retired what and who brought it back. The `EXPECTED_RANKS` lifecycle pin moves in lockstep (a *further* downgrade stays a hard CI fail); under [ADR 0151](<redacted>.md) the `task.abandon` floor moves archon→metic, landing as `governance_003` because the compat seed is run-once — purely additive, and the drift guard now compares the **union** of grant migrations to the catalog and fails loudly on a future revocation.](<redacted>.md) | GDS / permissions |
|
|
226
234
|
| 0153 | [**Sequential chain execution — one kickoff walks a dependency chain** ([task 1002484](https://cloudbongos.com/builders#/task/1002484)). [ADR 0015](<redacted>.md) made `dependencies[]` load-bearing and auto-promotes a task's dependents `backlog`→`ready` on ship — but nothing *picks the unblocked task up*, so on the live instance **146 of 303 open tasks sit behind a predecessor** and a 24-task planning seed needed 24 separate human kickoffs. The only "sequential" affordance was a clipboard prompt on the Tasks board (`<redacted>`), which is text rather than execution and cannot even select the gated successors, because they are still `backlog`. Requested independently by two builders (ideas 1000461, 1000398). **Decision:** a chain runner with **no schema change** (the polymorphic `dependencies` table + `task_is_fully_unblocked` + the promote-on-ship trigger already encode the order; a `chain_id`/queue column would be a second, drift-prone source of truth) and **no new claim guard** (one-active-claim-per-session is keyed on the *working tree*, so ship-before-next-claim is already the enforced shape — and batch-claiming a chain is impossible anyway, since successors fail `DEPS_NOT_SHIPPED` until their predecessor ships; the ship IS what unblocks the next step). Deterministic plumbing (`scripts/gds/sequence.js` — scope, topological order, halt matrix, all read off the task row) is split from judgement (`.claude/skills/builder-sequence/`, which defers to `/builder-claim` + `/dev` + `/builder-ship`). **Halt, do not skip** (owner decision): on `needs_migration`, `security_sensitive`, `protected_path`, `rank_floor`, `thin_spec`, `judgement_kind`, `not_claimable`, `deps_unmet`, `in_flight`, or `cycle`, the runner stops and reports with the task still claimed — because in a dependency graph a skipped task is a *predecessor*, so skipping strands everything behind it while the run keeps posting apparent progress. Not built: chains as a priced claimable unit on the board (idea 1000398), which is additive.](<redacted>.md) | GDS / methodology |
|
|
235
|
+
| 0154 | [A goal's done-when criteria are authored by that goal's owner or managers](<redacted>.md) | GDS / goals |
|
|
227
236
|
| 0155 | [**Adopting a private repo: widen the onboarding OAuth scope to `repo`** ([task 1002561](https://example.com/builders#/task/1002561)) — amends [ADR 0143](<redacted>.md) §1 by taking the exit 0143 itself named (“revisit to `repo` only if private support is needed”). Standing up a project on an EXISTING PRIVATE repo died at the provisioner’s scaffold push with GitHub `403 Write access to repository not granted`: `scopeForWebFlow` requested `public_repo`, which grants write to **public** repos only. Confirmed as scope and not credentials three ways — the live `github_oauth_tokens` row was `public_repo,read:user` and still VALID during the failure (a dead token 401s; this 403 text is GitHub’s canonical scope-insufficient reply); every greenfield project worked only because `createUserRepo` hardcodes `private:false`, so the platform had only ever pushed to repos it had just made public; and the picker already rendered a **“Private” badge** for a case `public_repo` made unreachable. Owner chose (B) widen-and-keep-pushing over (A) never-touch-the-user’s-repo (user runs `bongos init --adopt` + pushes; needs a deploy key for the instance to *run*) and (C) public-only, accepting the cost this reverses: the persisted token can now write to **every repo the user owns**. Containment from 0143 is unchanged and is what makes it tolerable — encrypted (secret-box), **1h TTL**, lazily swept, requested ONLY on the wizard’s `?repos=1` path (hall + CLI stay `read:user`), never stored when secret-box is off. Three guards ride along: (1) widening the *request* does not upgrade *stored* tokens, so new `grantedScopeCoversRepos()` gates `resolveRepoToken` and a surviving `public_repo` row takes the existing `repo_scope_required` (403) reconnect path instead of 200-ing while **silently omitting the user’s private repos** — it PARSES GitHub’s comma-separated grant rather than substring-matching, since `public_repo` *contains* “repo” and a naive match accepts exactly the stale token being guarded against; (2) `createUserRepo` keeps `private:false` — now a **product** choice (instances are open-source/capture-resistant by design), deliberately not a parameter, test-pinned so the widen can’t change what we create; (3) consent copy states the breadth in our own words first, since GitHub words `repo` as full control of all repositories. A central **GitHub App** (fine-grained `Contents:write` on only the selected repo) remains the principled fix and this makes its case stronger — a future ADR adopting it supersedes this. **This alone does not make adopt work:** provisioning is independently greenfield-only (`git init` a fresh tree + push to `main`, which an existing repo rejects as unrelated history) — [task 1002562](https://example.com/builders#/task/1002562) fixes that and, because the wider scope makes a mistaken `--force` a real data-loss route on a user’s repo, carries a test asserting the adopt push never forces; [1002563](https://example.com/builders#/task/1002563) verifies live.](<redacted>.md) | onboarding / auth / provisioning |
|
|
228
237
|
| 0156 | [**A builder's own display name wins, always** ([task 1002568](https://cloudbongos.com/builders#/task/1002568)). A builder renamed themselves in hall Settings, the save worked, and the name reverted to their GitHub account name at the next sign-in. `upsertBuilder` (`src/bongos/db-kernel.js`) is named for account creation but runs on **every authentication**, and its `ON CONFLICT (github_id) DO UPDATE` re-set `display_name = EXCLUDED.display_name` unconditionally — where `EXCLUDED` carries whatever the caller just read off GitHub. All four login paths feed it (`pollDeviceFlow`, `completeWebFlow`, `completeIdpFlow`, `completeDeviceExchange`), so CLI re-auth alone was enough to undo the save the same day. **Decision:** the row records *who last set the name*. A new core column `builders.display_name_source` (`'github' | 'user'`, migration `core_197`) is stamped `'user'` by `setBuilderDisplayName` — the only self-rename path — and the upsert's conflict branch guards the assignment with `CASE WHEN builders.display_name_source = 'user' THEN builders.display_name ELSE EXCLUDED.display_name END`. GitHub keeps seeding and re-syncing the name for everyone who never renamed; it never overwrites one the builder chose. `github_login`/`avatar_url` re-sync unchanged. **Rejected:** the one-line fix of deleting the assignment entirely — it freezes `display_name` at signup for *everyone*, so a GitHub rename (or a bare login seeded because the account had no GitHub display name) would never propagate again; that trades a visible bug affecting the few who renamed for a silent one affecting the many who never opened Settings. Not surfaced in `GET /me/display-name` or the OpenAPI spec (internal persistence state, no UI consumer). The federated hub's `platform_identity_accounts` needs no matching guard — the `CASE` runs regardless of what `EXCLUDED` carries, and that table is identity-only with no self-service editor. `tests/display_name.mjs` previously covered only the pure validator, which is why this shipped; it now reads the real SQL and asserts both directions.](<redacted>.md) | GDS / builder experience |
|
|
229
238
|
| 0157 | [**Archon is rank and identity only; the operational block delegates to Metic** ([task 1002584](https://cloudbongos.com/builders#/task/1002584), owner decision 2026-08-05). Archon had accumulated **50 route gates**, almost none about *authority* — reading the roster, listing dev boxes, viewing the audit log, adjudicating a vuln report, tearing down a droplet. Each arrived Archon-gated by [ADR 0016](<redacted>.md)'s fail-closed default for new privileged routes; nothing came back to lower them, so “fail closed” became “the owner is the only operator”. Generalises the one-power delegations of [ADR 0090](<redacted>.md) and [ADR 0152](<redacted>.md) instead of relitigating route by route. **20 permissions move archon→metic** — the governance pages (`/watch`, `/harbor`, `/gate`), session search, the dev-box + provisioning fleets, security adjudication, the audit log, roster reads, cross-builder memory reads, project curation, budgets, idea ratification, override decisions, release-on-behalf, criterion unsatisfy, goal reorder, version create, newcomer restock, peer-vote tally, Discord channel ops, autonomy precheck. **Only possible as a 1-file change** because BV1.R104/R105 had just migrated the gates to `requirePermission`, so the floor is declared once in `catalog.js` — the same decision was a 41-call-site diff a day earlier, and that is the concrete [ADR 0151](<redacted>.md) payoff. **13 stay Archon:** rank (`builder.rank.set`), identity (status/admission/session-revoke), and — refusing the literal reading of the directive — **four escalation keys** (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`, `governance.manage`) that form a chain to self-promotion: scope a goal into the permission core → claim a task there (protected paths already floor at *metic* per [ADR 0043](<redacted>.md)) → confirm past the failed grade → approve your own red CI gate → it lands on `main`. Moving all 20 would have made the delegation **self-reversible**, so keeping rank Archon-only depends on keeping those four Archon-only; any future delegation must re-run that check. Also held back: `version.close` (paired with rank as the powers that never delegate) and `dependency.cross_tier.manage` (`system: true` — clearing that flag to move it would widen what any CUSTOM role can hold, a second-order change nobody asked for). Mints **`gate_approval.read`** so the Gate page opens at Metic while approving stays Archon, and finishes the three reads R104/R105 skipped — otherwise the read would be **stricter than the write** (a Metic able to confirm a vuln report but not list one). Route pins move from ranks to `perm:` atoms so a floor change needs no pin edit. **Two flagged consequences:** `/security/docs` (attacker-usable exploit detail) is now Metic-readable, since it shares `security.report.adjudicate` — the one delegation widening who may *read* rather than *act*; and [ADR 0154](<redacted>.md)'s “the backward ratchet stays Archon” carve-out is superseded (satisfy/unsatisfy stay separate atoms, so confirming still never confers un-confirming). ADR 0016 is untouched end to end — floors move *inside* the boundary, never the enforcement point.](<redacted>.md) | governance / permissions |
|
|
@@ -239,14 +248,15 @@ This keeps the decision history honest and traceable.
|
|
|
239
248
|
| 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 ≥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
249
|
| 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
250
|
| 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
|
-
| 0167 | [**The module catalog: `source` (which tree ships it) and `provenance` (who built it) are two axes** ([task 1001772](https://cloudbongos.com/builders#/task/1001772) — the last task of goal 1000029). Closes [ADR 0107](<redacted>.md) §2/§3: a browsable catalog of enable-able core modules, each with author + origin credit, in the hall AND via `bongos module list --catalog`. The bug it fixes: the Modules tab derived its own/core-loaded split from the manifest's `origin` alone, but attribution is OPTIONAL and core's own modules declare none (nothing upstreamed them) — so on a split instance every core module read as *built here* and the catalog you could opt into read as empty. One field was answering two questions. Fix: a shared model (`src/module-loader/catalog.js`) that both the hall route and the CLI build from, carrying `source` — read STRUCTURALLY from the loader's discovery root ([ADR 0108](<redacted>.md) §1: core package vs instance tree), falling back on coincident roots to whether `package.json` names the core — beside the unchanged `provenance` credit axis. Plus `credit: {author, origin, inferred}`, which fills a core module's blank attribution with the platform name and MARKS it inferred (`*` in the CLI, a hoverable `<abbr>` in the hall) so it never reads as a claim the author typed. The tab now sections by `source` ("Module catalog" / "Built by this project"), gains a search box, and draws "Submit for review" from instance-source modules only. Rejected: stamping author/origin into 15 core manifests (repetitive, and module 16 forgets), overloading `origin` (destroys the credit axis), inferring from `default:true` (§6 makes accepted catalog modules default-OFF — exactly backwards), a DB catalog table (premature; the manifests already are the catalog).](<redacted>.md) | modules / upstreaming |
|
|
251
|
+
| 0167-a | [**The module catalog: `source` (which tree ships it) and `provenance` (who built it) are two axes** ([task 1001772](https://cloudbongos.com/builders#/task/1001772) — the last task of goal 1000029). Closes [ADR 0107](<redacted>.md) §2/§3: a browsable catalog of enable-able core modules, each with author + origin credit, in the hall AND via `bongos module list --catalog`. The bug it fixes: the Modules tab derived its own/core-loaded split from the manifest's `origin` alone, but attribution is OPTIONAL and core's own modules declare none (nothing upstreamed them) — so on a split instance every core module read as *built here* and the catalog you could opt into read as empty. One field was answering two questions. Fix: a shared model (`src/module-loader/catalog.js`) that both the hall route and the CLI build from, carrying `source` — read STRUCTURALLY from the loader's discovery root ([ADR 0108](<redacted>.md) §1: core package vs instance tree), falling back on coincident roots to whether `package.json` names the core — beside the unchanged `provenance` credit axis. Plus `credit: {author, origin, inferred}`, which fills a core module's blank attribution with the platform name and MARKS it inferred (`*` in the CLI, a hoverable `<abbr>` in the hall) so it never reads as a claim the author typed. The tab now sections by `source` ("Module catalog" / "Built by this project"), gains a search box, and draws "Submit for review" from instance-source modules only. Rejected: stamping author/origin into 15 core manifests (repetitive, and module 16 forgets), overloading `origin` (destroys the credit axis), inferring from `default:true` (§6 makes accepted catalog modules default-OFF — exactly backwards), a DB catalog table (premature; the manifests already are the catalog).](<redacted>.md) | modules / upstreaming |
|
|
243
252
|
| 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 |
|
|
244
|
-
| 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 |
|
|
253
|
+
| 0167-b | [**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 |
|
|
245
254
|
| 0169 | [**`bongos serve`: the `bongos dev` boot path, on a real host** ([task 1002398](https://cloudbongos.com/builders#/task/1002398), goal 1000052). [ADR 0149](<redacted>.md) gave an owner one command to stand an instance up on their laptop; "now put it on a server" had no equally good answer — Docker serves the *vanilla* brand on `:3000`, `bongos onboard` provisions a whole droplet through the control plane, and [`instance-deploy.md`](../recipes/instance-deploy.md) left the systemd unit as a hand-written prose step. The six steps (deps → Postgres → createdb → migrate → env → server → health) were already right for a host; only four *decisions inside them* were laptop-shaped. So: a `--server` **mode** on the same script (`bongos serve`), never a second boot path, so the two can't drift. (1) **Supervision** — a systemd unit (`Restart=always`, `WantedBy=multi-user.target`) instead of a pid file that dies on reboot, falling back to the pid-managed process where systemd is absent rather than refusing. (2) **Bind address, derived from the origin** — `https://…` means a proxy terminates TLS, so bind `127.0.0.1`; plain `http://` on a public host means nothing is in front, so bind `0.0.0.0`. The common misconfiguration (app publicly reachable on its raw port behind a proxy) becomes impossible by default. (3) **Peer-auth bootstrap** — a fresh `apt-get install postgresql` cluster has only the `postgres` role, so a deploy user's `createdb` fails with `role "deploy" does not exist`; that reads like a broken install and is really an unbootstrapped cluster, so we create the login role as the superuser. (4) **Health means the PUBLIC origin** — the loopback probe proves the app; DNS/proxy/TLS are where a host actually fails, so it probes the advertised origin too and names the missing layer (advisory — DNS may legitimately not be wired). Secrets go to a root-owned `0600` `EnvironmentFile`, never `Environment=` (`systemctl show` leaks those, and the laptop path's `buildEnv()` spreads the caller's whole shell). Every decision is a pure injectable in `dev-lib.js`, proved in `tests/dev_lib.mjs` with no host. Rejected: a separate `deploy.sh` (drift — the point is the *same* six steps), Docker-on-the-host (makes brand+port a compose override), always binding `0.0.0.0` (exposes the raw port), refusing without systemd (breaks containers/BSD).](<redacted>.md) | onboarding / deploy |
|
|
246
255
|
| 0173 | [**The legacy multi-ship reward shortfall is NOT backfilled — it closes as a one-time transition cost** ([task 1002520](https://cloudbongos.com/builders#/task/1002520), goal 1000037). Before `core_196` the cost-plus reward booked ONE row per session, so a multi-ship session (the normal shape — ship, new worktree, claim again) was paid at its first ship's level and never topped up; `core_196`'s watermark accounting `(session_id, reward_basis)` fixed that forward and deferred the retroactive half to an "owner-signed-off backfill, tracked separately". This is that decision, and it is **decline**. Measured live 2026-08-16, both of the task's open questions answered: (1) the task's own premise is wrong — `reward_basis = 0` is the column's DEFAULT, so it means "booked before top-ups existed", not "underpaid", and **over-selects by ~60%** (478 legacy sessions, but 287 are single-ship and were paid in full; only 191 are candidates); (2) re-pricing each candidate's stored `session_records.model_usage` at `round(true_cost_usd × 1.20)` gives **7,697 credits owed** across 119 sessions — example-owner 5,296 (69%), jaxri 2,319, scootermack 82 — with **72 candidates unreconstructable** (empty `model_usage`) and 16 already OVER-paid by 1,131 that nobody proposed clawing back. Declined because it mirrors the ADR 0146 clawback's own logic (the accounting changed; neither side of the boundary reopens), because paying only the 119 that happened to leave a server-side trace is arbitrary against the 38% that did not, and because correcting shortfalls while ignoring overpayment is a policy choice the owner declined to adopt. Consequence recorded plainly: three builders stay permanently short. **Does NOT close task 1002448** — that is the Windows `encodeProjectPath` population, never *recorded* rather than paid at a stale watermark, still recoverable and still owed.](<redacted>.md) | economy / rewards |
|
|
247
|
-
| 0172 | [**The sub-Archon rank roles are owner-editable; the Archon role is not; the substrate keys are non-grantable to everything** ([task 1002961](https://cloudbongos.com/builders#/task/1002961), goal 1000030). Amends ADR 0151 §2's "the four seeded rank-roles are immutable through this API" wall — the GRANT half only. Their grants were always ordinary `governance_role_permissions` rows (the `governance_002` seed writes them); the read-only rendering was route policy, not structure. Three walls now, where there was one: the **Archon** rank-role stays frozen as the LOCKOUT GUARD (it is the only role holding `governance.manage` by seed, so freezing it means no click sequence can revoke the permission needed to reach this tab); the three **sub-Archon** rank-roles become grant-editable, the seven **in-app** Archon powers included (owner's explicit choice, escalation consequence stated); and `substrate` becomes a declared catalog axis — `deploy.push_main`/`path.protected.modify`/`infra.ssh` are non-grantable to EVERY role forever, because ADR 0043 enforces them out of band, so a grant row confers nothing and the toggle would be the UI lying. Grantability goes per-role (`isGrantableTo(key, role)`); `isGrantable(key)` survives as the narrow custom-role question config.js strips against. **Custom roles are deliberately NOT widened** — a rank-role moves only by a rank write, a custom role is assignable by anyone holding `governance.manage`. Assignment stays rank-derived (R95b) untouched. Adds `POST /governance/roles/:roleKey/reset` (one transaction, filtered through `isGrantableTo`, refused on a custom role) because a drifted ~60-row rank has no way back by clicking. Rejected: substrate keys behind a warning (a toggle that succeeds and delegates nothing), an editable Archon role behind a confirm (a confirm is not a guard; real recovery is hand-written SQL on the droplet). **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](<redacted>.md) | governance / permissions |
|
|
256
|
+
| 0172-a | [**The sub-Archon rank roles are owner-editable; the Archon role is not; the substrate keys are non-grantable to everything** ([task 1002961](https://cloudbongos.com/builders#/task/1002961), goal 1000030). Amends ADR 0151 §2's "the four seeded rank-roles are immutable through this API" wall — the GRANT half only. Their grants were always ordinary `governance_role_permissions` rows (the `governance_002` seed writes them); the read-only rendering was route policy, not structure. Three walls now, where there was one: the **Archon** rank-role stays frozen as the LOCKOUT GUARD (it is the only role holding `governance.manage` by seed, so freezing it means no click sequence can revoke the permission needed to reach this tab); the three **sub-Archon** rank-roles become grant-editable, the seven **in-app** Archon powers included (owner's explicit choice, escalation consequence stated); and `substrate` becomes a declared catalog axis — `deploy.push_main`/`path.protected.modify`/`infra.ssh` are non-grantable to EVERY role forever, because ADR 0043 enforces them out of band, so a grant row confers nothing and the toggle would be the UI lying. Grantability goes per-role (`isGrantableTo(key, role)`); `isGrantable(key)` survives as the narrow custom-role question config.js strips against. **Custom roles are deliberately NOT widened** — a rank-role moves only by a rank write, a custom role is assignable by anyone holding `governance.manage`. Assignment stays rank-derived (R95b) untouched. Adds `POST /governance/roles/:roleKey/reset` (one transaction, filtered through `isGrantableTo`, refused on a custom role) because a drifted ~60-row rank has no way back by clicking. Rejected: substrate keys behind a warning (a toggle that succeeds and delegates nothing), an editable Archon role behind a confirm (a confirm is not a guard; real recovery is hand-written SQL on the droplet). **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](<redacted>.md) | governance / permissions |
|
|
248
257
|
| 0171 | [**Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path** ([task 1002277](https://cloudbongos.com/builders#/task/1002277), R01 of goal 1000045). The C1 cross-project profile reads the platform-identity rollup through the module's **declared-but-never-registered `platform-identity.hub` port**: the module registers a real provider and core `GET /builders/:id/profile` `resolveOptional`s it beside `reward` — zero network, `null` (clean degrade) on every non-hub instance. Federated instances **link out** instead of inlining (amends profile-page spec Decision 1: an instance-side copy is either stale-on-hide or a per-view hub call). Consent: `profile_state` on `platform_identity_accounts`, **fail-closed** — `active` iff touched by a hub-session `/sso/authorize` (device-poll/check-in create `provisional`; backfill = all provisional, lazy upgrade; no core-`builders` join — type-unsafe vs `system:*` ids and a module-migration boundary breach), so the P03 owner decision can only ever widen, never claw back. The C2 hide flag is `hide_stats` on the account (NOT per-instance `behavior_prefs`), enforced **inside** the module's read functions — `listScoutingRollup` itself gains the `active AND NOT hide_stats` predicate so the live Metic-floor `/people`+`/scouting` surfaces honor it by construction. Hub profile responses go `no-store` (hide must beat the 60s cache). Publishing is gated on hardening the self-reported rollup (membership-row requirement, rank-vocab validation, disciplines caps, check-in→rollup sequencing). Found + filed: the `LATERAL unnest` fan-out inflating rollup sums (task 1002911), the unreserved `u` slug (task 1002912), the missing platform T&C artifact (task 1002913), criterion amendments (task 1002914). Rejected: instance→hub per-view pull (always-on dependency), hub→instance mirroring (stale-on-hide), storing the flag in builder-settings, backfilling `active` from hub `builders` rows.](<redacted>.md) | platform identity / profiles / privacy |
|
|
249
258
|
| 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](<redacted>.md) | platform identity / public surface |
|
|
259
|
+
| 0172-b | [Per-craft compensation: the ideator credit lane, separate from cost-plus](<redacted>.md) | economy / compensation |
|
|
250
260
|
| 0174 | [**Governance is renamed government, its positions are ranks, and "role" is reserved for the three crafts** ([task 1003002](https://cloudbongos.com/builders#/task/1003002), R01 of goal 1000068). Owner directive, 2026-08-16. The word **role** named two unrelated things: the permission system's ladder tiers (`governance_roles`, the "rank-roles") and the three crafts (`tasks.discipline` = engineer/artist/ideator) — and goal 1000066 was actively claiming it for the crafts (`three-roles-only`, `users-hold-roles`), putting the two on a collision course. Resolved at full depth rather than as a label pass, because a labels-only fix leaves the UI and the code disagreeing, which is the condition that produced the collision. `modules/governance/` → `modules/government/`; the kernel module key, the config files, `/api/bongos/governance/*` → `/government/*`, and the permission keys `governance.manage` → `government.manage` (~50 sites) + `page.view.governance` → `page.view.government`. **The entity IS the rank — no second noun**: `governance_roles` → `government_ranks`, `role_key` → `rank_key`, `rank-roles.js` → `ranks.js`; "position" stays prose-only, since introducing it as a code term would recreate the qualifier problem "rank-role" already demonstrates. The cross-module kernel port `governance.roleSync` → `government.rankSync` must land atomically across its provider and both consumers (`modules/lifecycle/db.js`, `src/bongos/db.js`) because it is a transaction participant on the rank-write path. Hand-composed bundles stay in the one table as **custom ranks** — the owner's call over splitting them into a separate "appointments" concept, with the objection recorded (a custom bundle is not a ladder position, and ADR 0172 §4 already treats the two kinds as different). Enforced by a `fitness.js` rule, allowlist shipped with reasons. **Discord's server roles keep the word** — that noun belongs to Discord's API. Amends ADR 0151 + ADR 0172 (the editable-rank-roles one) in **vocabulary only**; ladder slugs, `builders.rank`, `requireRank`, and every floor/grant/assignment semantic are untouched. Rejected: renaming the ladder slugs (turns a bounded rename into a trust-boundary migration), a permanent compat shim (both vocabularies stay correct forever).](<redacted>.md) | government / permissions |
|
|
251
261
|
| 0175 | [**The Board Room: Full Idea ratification is the first upward-pointing gate** ([task 1003014](https://cloudbongos.com/builders#/task/1003014), R01 of goal 1000069). Every existing gate points DOWN the rank ladder; the owner asked for one that could slow THEM down — *"otherwise I can just ship and build as I want, finding loopholes."* The board's subjects are exactly **two**, both rare by construction: a **Full Idea** and a **change to the constitution itself** (`subject_type` from day one). Because the vote gates no build path, the **ADR 0162 tension dissolves rather than being compromised** — nothing self-executes because there is nothing downstream to execute; ratification confers legitimacy and does NOT auto-create the goal. A vote is a **timed sitting, not a quorum**: absent members are *nulled* (dropped from the denominator, never counted as a no), so the clock is the deadlock exit and no shotgun/fork-with-treasury clause is needed at this scale — default 90 minutes, closing early on full turnout, and yes, a short window means whoever is awake governs (the owner's explicit intent). **Consent, not majority**: silence is consent, one reasoned objection returns the idea, and a zero-vote close PASSES. A rejection must carry free text **plus one of the five Full Idea sections** — which is what **forces the hall to be the voting surface**, since a Discord reaction could only ever be feedback-free; Discord is therefore an outbound notification mirror that casts nothing, and its own mobile app is the mobile push (no mobile surface is built). The completeness grade is the **floor** (a window opens automatically on pass), the vote is the **merit**; concurrent windows are normal. Who governs is **configuration** — day one is monarchy expressed as config on the real machinery, never a separate code path — and **amendments are decided under whatever rule is currently in force**, so flipping out of monarchy is a legal monarchy act and no bootstrap exception is needed. Open ballot; an author may never vote on their own item. Franchise derives from **rank, not role**, and that is forced: `PATCH /me/disciplines` is deliberately ungated, so a craft-scoped chamber would be one-request privilege escalation. Unblocks goal 1000067 / IC5, which consumes the pass event and *"invents no parallel vote"*. Rejected: quadratic vote weighting and the attribution-economy knobs (γ, τ, the 30% stack cap — v1's franchise is not credit-weighted at all), eternity clauses, and any legal-entity integration.](<redacted>.md) | government / ideas / governance |
|
|
252
262
|
| 0176 | [**Private-repo standup: a per-repo read-only deploy key, minted with the owner's token** ([task 1002735](https://cloudbongos.com/builders#/task/1002735), goal 1000032; walk finding N4, seen live on `mercury` 2026-08-17). A standalone instance's repo is cloned/pushed once with the owner's `repo`-scoped token (ADR 0155), but the durable `origin` rides the shared SSH deploy alias — which GitHub answers "Repository not found" for a private repo it isn't registered on, so every private standup failed at the recurring `git pull --ff-only origin main`, **token or not**. Fix: when the target repo answers *private* to the owner's stored token, provisioning mints the instance its OWN ed25519 deploy key beside the checkout (`.git/cloudbongos-deploy-key` — untracked, survives `git reset --hard`, dies with the checkout), registers it on the repo **read-only** as `cloudbongos-deploy-<slug>` (idempotent by title; a stale same-title key is replaced, never duplicated), then wires the CHECKOUT: `origin` re-pointed at plain `git@github.com` and `core.sshCommand` pinned to the key with `-F /dev/null` so no `~/.ssh/config` identity can shadow it. Public repos take the unchanged shared-alias path; pushes keep preferring the owner-token HTTPS URL (task 1002317), which is why read-only suffices; teardown best-effort revokes by title. Rejected: **GitHub App installation tokens** (hourly expiry means every unattended pull needs a minting sidecar or a hub round-trip) and **registering the shared platform key on the repo** (one shared secret granting the whole platform durable access to private code; per-repo keys keep the blast radius to one repo and are visible to the owner in their own repo settings). Follow-ups: the manage page's flip-to-private stays refused until already-provisioned instances get the same wiring, and builder dev boxes of a private-repo project need their own leg.](<redacted>.md) | provisioning / security |
|
|
@@ -257,20 +267,21 @@ This keeps the decision history honest and traceable.
|
|
|
257
267
|
| 0181 | [**Teardown accepts every state but `torn_down`, and deliberately keeps the address** ([task 1003042](https://cloudbongos.com/builders#/task/1003042), goal 1000058). Walk finding N5: an instance at `status='error'` could never be removed — the owner's teardown answered 409 `not_teardownable` and the archon `force-teardown`, which exists as the escape hatch, answered the SAME refusal, because both read one hand-kept whitelist `['provisioning','active','tearing_down']` that omitted `requested` and `error`. No role had a path out, so errored rows accumulated permanently (instance 15 `swag` from 2026-08-07). The whitelist justified itself as "nothing running to tear down", which is FALSE for `error`: a provision failing mid-run lands there AFTER it may have created the database, taken a port, and written the systemd unit, Caddy block and DNS record — a partial standup is the thing most in need of reclaiming. `TEARDOWN_FROM` is now DERIVED from `INSTANCE_STATES` minus `torn_down`, so a future state is teardownable by DEFAULT — the bug was an allow-list that silently omitted states, and for a failure mode whose signature is "a row with no exit", the safe default is that an exit exists. The runner needed no change (its steps are each `allowFail`); only the web guard refused. The one surviving refusal is now ONE shared helper: the copies had drifted, and force-teardown's positional `res.fail` form put the bare code `not_teardownable` on the wire as the human text. **The `port` and `domain` are deliberately NOT released** — releasing the port alone would be WORSE than the status quo, because the `<slug>.caddy` block survives teardown and proxies the hostname to a PORT NUMBER, so a later instance given that port would answer at the torn-down project's still-live address (verified live: two projects torn down 2026-08-13 still resolved on 2026-08-19). Holding the port is what keeps the address inert at a harmless 502; releasing both is [task 1002897](https://cloudbongos.com/builders#/task/1002897). Retention is also correct for revive, which re-uses the same hostname. Copy was corrected to match: it had promised "the address stops answering", which was never true.](0181-teardown-state-gate-and-address-retention.md) | provisioning / lifecycle |
|
|
258
268
|
| 0182 | [**Project visibility is TWO axes: a derived publish gate and a stored join door** ([task 1002320](https://cloudbongos.com/builders#/task/1002320), goal 1000046). Goal 1000046 seeded 31 tasks each citing "per R01" — this ADR — so nothing in the goal could start until it landed: it gated **29 open tasks**, the largest single chokepoint in the repo, while itself parked at `backlog` with no dependencies. The spec its planning task cites was never written, so this is the design of record. The goal's criterion says **four** states (Public/Private/Stealth/Unlisted) and R04 says **three** stored plus a derived dark-matter; both are right and conflating them is the trap. There are two INDEPENDENT axes: **publishability** (DERIVED from the identity gate — name + description, per the owner interview) decides whether a project is on the map at all, and **the join door** (STORED `visibility ∈ public|private|stealth`, owner-set) decides how it renders and who gets in. Four user-facing states = 3 × publishable + one derived state that collapses all three. The column name `visibility` is a mild lie kept for consistency with `goals.visibility` (ADR 0112) — it does NOT decide visibility, publishability does — and the ADR is the mitigation. All three stored states are VISIBLE; only the derived state is invisible, which is what makes the 3+1 split honest rather than a workaround. `private` inherits ADR 0112's exact meaning (visible-but-closed); `stealth` exposes EXISTENCE but not IDENTITY, and its redaction is **server-side in the feed** — the naive client-side version leaks to anyone opening the network tab, and the orbs page is a static site OUTSIDE this repo (R09) so it could never be enforced there. Three mechanisms touch "does this appear" and are separated: `status` (operational), `visibility` (owner), `featured` (Archon editorial) — on the map iff `status='active' AND publishable`, with `featured` only ranking among those. That is a real behaviour change: today's predicate is `featured=true AND status='active'`, so **featuring is currently load-bearing for visibility at all** and the goal's owner-control premise silently does not work without it. Visibility is owned by the INSTANCE and the hub row is a CACHE (ADR 0141 §3 + the admin-console spike's D1) — so `owner_github_id` being "advisory, not an authority" is CORRECT rather than a gap, R05 moves to an instance surface, and R08's absent-report default is fail-closed (unreported ⇒ dark matter, off the map rather than exposed by omission). Per-state join rules compose with the separate `open|closed` join policy ([task 1002817](https://cloudbongos.com/builders#/task/1002817)) by one rule: **visibility sets the ceiling, join policy may only narrow** — otherwise the two settings are a coin-flip decided by evaluation order. Finally, `stealth` COLLIDES with the existing instance-wide pre-launch password gate (`STEALTH_PASSWORD`, `src/bongos/stealth-gate.js`), which means nearly the opposite (a whole instance made unreadable vs a project publicly on the map, anonymously); per ADR 0174 the product meaning keeps the word and the infra rename is filed separately rather than decided by an ADR about visibility. Rejected: a stored 4-valued enum (makes `unlisted` compete with `private`, and lets publishable+unlisted disagree), a separate `published` boolean (can drift from the identity it summarises; derived cannot), and keeping `featured` as the visibility gate (an owner who sets public and appears nowhere has not been given control).](<redacted>.md) | platform-identity / project visibility |
|
|
259
269
|
| 0183 | [**A done-when criterion closes itself; the review queue becomes a place to look** ([task 1002069](https://cloudbongos.com/builders#/task/1002069), goal 1000072, owner request 2026-07-05). ADR 0086 §6 made `satisfied` reachable exactly one way — a Metic+ calling `POST /done-when/:id/satisfy` — even though the signal it gated on is unambiguous (**every** gating task shipped) and a human confirming it adds no information. When nobody runs `/goal-review` the result is not a careful pause but a **lying board**: this session measured goal 1000072 at `6/9 shipped, 0/5 criteria`. Now one helper, `doneWhen.autoSatisfyShippedCriteria(exec, {taskId})`, runs from **two** places for two different reasons — inside `shipTask`'s txn (scoped, **immediate**, on the txn client so a rolled-back ship cannot leave a goal achieved) and unscoped on every `publish-reconciler` tick (**backlog drain** for criteria whose tasks shipped before this existed, since nothing will ship against them again, plus **self-heal**). Idempotent by construction — it reads only `satisfied=false` rows — which is what makes an every-tick sweep safe. **The load-bearing subtlety is the delivery guard.** Auto-close needs BOTH "nothing still pending" AND **≥ 1 task actually `shipped`**; the second is not redundant, because since [task 1002651](https://cloudbongos.com/builders#/task/1002651) made `abandoned` terminal-not-pending, a criterion whose tasks were **all cut** satisfies the first clause *vacuously* and would auto-close on ZERO delivered work, cascading its goal to `achieved` (measured: `buildCriterionProgress` returns `pending_review:true` for exactly that shape). Yet `pending_review` **keeps** that wider meaning on purpose — it asks "should a human LOOK at this", and 1002651 chose that an all-abandoned criterion surfaces so someone decides whether it still means anything. So: everything that auto-closes was flagged; not everything flagged auto-closes. An earlier draft "aligned" the two predicates and broke that intent — `tests/criterion_progress.mjs` caught it; **do not re-align them**, the gap is the design. `/goal-review` is kept, demoted from gate to viewing surface; manual `POST /satisfy` stays for what automation cannot judge (a criterion with no linked tasks never auto-closes). Honest limits recorded: auto-satisfy cannot help a criterion nothing is LINKED to, and the in-txn close logs before commit (accepted — threading the summary out through `withTx` would cost ~5 lines in the over-threshold `db.js` under shrink-only ratchet policy). Rejected: sweep-only (up to 5 min to see your own goal close), hook-only (never drains the backlog), a stored `pending_review` column (second source of truth), deleting `/goal-review`.](<redacted>.md) | lifecycle / goal tier |
|
|
260
|
-
| 0184 | [**A task's visual is its value summary in another medium, and inherits that audience** ([task 1003109](https://cloudbongos.com/builders#/task/1003109), goal 1000072, from owner idea 1000400 filed in Discord `#ideas` 2026-06-24). A shipped task can now carry ONE optional image beside its `value_summary` — a screenshot of the surface that changed, a before/after, a diagram — so a reader who does not read code can **see** what shipped instead of reading about it. The rest of that idea had already landed (the note itself; the spend half via `/cost`); the picture was the whole gap, and the only path a builder had was a Discord post — outside the ledger, on a CDN whose links expire. The decision that keeps it from rotting is a REFUSAL to invent a second privacy axis: **a visual has no audience of its own.** One predicate says who may see it — `task-visuals.isPubliclyViewable` (public = `shipped|confirmed`, byte-for-byte the test `routes/public.js:projectPublicTask` already applies to a title/summary) — so any future surface rendering `value_summary` inherits a settled answer for the picture beside it. Three consequences are load-bearing. (1) The serve gate lives **inside** the handler, not in mounted middleware, because the audience is a property of the ROW; the stored filename therefore carries the task id (`task-<id>-<16hex>.<ext>`), which is simultaneously the path-traversal defense and how the route finds the task, and every step fails CLOSED (unparseable name → absent task → not-public → `requireBuilder`, never past it). (2) The public shipped feed **mirrors the predicate in SQL** — `db.recentShippedTasks` nulls the URL for a `security_sensitive` row, rather than selecting the flag and filtering in JS, so the unauthenticated dashboard never advertises an image the serve route would refuse AND never learns the flag; change one, change the other. (3) The single deliberate narrowing vs `value_summary`: a security-sensitive task's image stays builders-only even once shipped — a summary is a sentence its author chose, a screenshot is whatever was on the screen. Nothing is HIDDEN by that; non-public means un-published, not invisible. **Optionality is a contract, not a default** ("where relevant" is the owner's wording): `ship.js --visual` screens the file BEFORE the claim resolves (bad input costs a retyped command, never a claim) and uploads AFTER (a failed picture warns and prints the re-attach command; it never fails a ship that already landed), a task with no visual renders **nothing at all** — no placeholder, so it never reads as unfinished — and `DELETE /tasks/:id/visual` exists because an image is not prose (task 1003102's write-once rule guards a written CLAIM, not the picture beside it). Not an arbitrary-file host: extension allowlist + magic-byte sniff + 4 MB cap + server-minted name, **SVG deliberately excluded** (an executable document served from our own origin), `nosniff`/`inline`/`default-src 'none'; sandbox` on the way out, files under `var/task-visuals/` outside the git tree so a deploy's `git reset --hard` cannot wipe them. Rejected: a per-visual visibility flag (two axes over one fact drift, and the first disagreement leaves a reader guessing which is true); requiring `modules/discord/bug-attachments.js` (ADR 0083 forbids module→module, and the intake genuinely differs — that one DOWNLOADS from a CDN and needs an SSRF allowlist; duplication is re-stated under ADR 0091 §2 and cross-referenced in both headers); full-size images in every feed (the ledger, a profile and the dashboard are scannable lists — they get a thumbnail linking to the file; the task page is where you go to look).](<redacted>.md) | lifecycle / hall-ui / status-ui |
|
|
261
|
-
| 0184 | [**A ship must account for itself: `shipTask` requires a land proof or a declared no-artifact** ([task 1002483](https://cloudbongos.com/builders#/task/1002483), goal 1000024). `db.shipTask` is the confirmed→shipped flip and, since [ADR 0120](<redacted>.md), where the CREDIT is booked — yet its only precondition was `WHERE status = 'confirmed'`, nothing about whether code reached main. The two AUTOMATED paths each proved the land first; the MANUAL route passed nothing, so a task confirmed with no pushed code could be shipped by its claim holder **or by any Metic+ sweeping `/merge-mode`**, booking its `task.shipped` credit. It stayed untracked for two months because its tracking task was closed in June asserting task 1001547 had delivered the gate — it had not, blocker 1000047's own final update says so, and the code agreed with the blocker rather than the closure note: **a hole hidden by a record nobody checked**, which is the same failure the gate is about. Decision: the gate lives at the WRITE, not the route (a route gate leaves the invariant unenforced at the layer that moves the ledger, so the next caller reopens it silently), and `shipTask` is FAIL-CLOSED on a required `landAssurance`. TWO kinds, and the second is why this does not make things worse: `proof` (this task's commit is on main) and **`no_artifact`** — a deliberate DB-only ship, an explicitly supported shape (CLAUDE.md "Direct DB writes are work too") that [task 1002478](https://cloudbongos.com/builders#/task/1002478) documents as a dead end below Archon. A naive proof-only gate would have made 1002478 strictly worse; naming no-artifact as a KIND turns it from an absence the gate must be talked out of into a recorded answer. Both carry a reason, audited post-commit and best-effort (an audit miss must never roll back a landed ship). `ship.js` now declares its own `--db-only`/`--allow-empty` lands, without which this fix would have regressed the very flow CLAUDE.md calls supported. Rejected: (b) gate only the route — leaves the write unguarded; (c) record the residual as tolerable — defensible on blast radius and genuinely close, but it would have been the FOURTH record asserting this was handled with nothing checkable behind it. Does NOT gate confirm: a codeless confirm is still accepted, it simply cannot reach `shipped` or pay.](<redacted>.md) | lifecycle / integrity |
|
|
270
|
+
| 0184-a | [**A task's visual is its value summary in another medium, and inherits that audience** ([task 1003109](https://cloudbongos.com/builders#/task/1003109), goal 1000072, from owner idea 1000400 filed in Discord `#ideas` 2026-06-24). A shipped task can now carry ONE optional image beside its `value_summary` — a screenshot of the surface that changed, a before/after, a diagram — so a reader who does not read code can **see** what shipped instead of reading about it. The rest of that idea had already landed (the note itself; the spend half via `/cost`); the picture was the whole gap, and the only path a builder had was a Discord post — outside the ledger, on a CDN whose links expire. The decision that keeps it from rotting is a REFUSAL to invent a second privacy axis: **a visual has no audience of its own.** One predicate says who may see it — `task-visuals.isPubliclyViewable` (public = `shipped|confirmed`, byte-for-byte the test `routes/public.js:projectPublicTask` already applies to a title/summary) — so any future surface rendering `value_summary` inherits a settled answer for the picture beside it. Three consequences are load-bearing. (1) The serve gate lives **inside** the handler, not in mounted middleware, because the audience is a property of the ROW; the stored filename therefore carries the task id (`task-<id>-<16hex>.<ext>`), which is simultaneously the path-traversal defense and how the route finds the task, and every step fails CLOSED (unparseable name → absent task → not-public → `requireBuilder`, never past it). (2) The public shipped feed **mirrors the predicate in SQL** — `db.recentShippedTasks` nulls the URL for a `security_sensitive` row, rather than selecting the flag and filtering in JS, so the unauthenticated dashboard never advertises an image the serve route would refuse AND never learns the flag; change one, change the other. (3) The single deliberate narrowing vs `value_summary`: a security-sensitive task's image stays builders-only even once shipped — a summary is a sentence its author chose, a screenshot is whatever was on the screen. Nothing is HIDDEN by that; non-public means un-published, not invisible. **Optionality is a contract, not a default** ("where relevant" is the owner's wording): `ship.js --visual` screens the file BEFORE the claim resolves (bad input costs a retyped command, never a claim) and uploads AFTER (a failed picture warns and prints the re-attach command; it never fails a ship that already landed), a task with no visual renders **nothing at all** — no placeholder, so it never reads as unfinished — and `DELETE /tasks/:id/visual` exists because an image is not prose (task 1003102's write-once rule guards a written CLAIM, not the picture beside it). Not an arbitrary-file host: extension allowlist + magic-byte sniff + 4 MB cap + server-minted name, **SVG deliberately excluded** (an executable document served from our own origin), `nosniff`/`inline`/`default-src 'none'; sandbox` on the way out, files under `var/task-visuals/` outside the git tree so a deploy's `git reset --hard` cannot wipe them. Rejected: a per-visual visibility flag (two axes over one fact drift, and the first disagreement leaves a reader guessing which is true); requiring `modules/discord/bug-attachments.js` (ADR 0083 forbids module→module, and the intake genuinely differs — that one DOWNLOADS from a CDN and needs an SSRF allowlist; duplication is re-stated under ADR 0091 §2 and cross-referenced in both headers); full-size images in every feed (the ledger, a profile and the dashboard are scannable lists — they get a thumbnail linking to the file; the task page is where you go to look).](<redacted>.md) | lifecycle / hall-ui / status-ui |
|
|
271
|
+
| 0184-b | [**A ship must account for itself: `shipTask` requires a land proof or a declared no-artifact** ([task 1002483](https://cloudbongos.com/builders#/task/1002483), goal 1000024). `db.shipTask` is the confirmed→shipped flip and, since [ADR 0120](<redacted>.md), where the CREDIT is booked — yet its only precondition was `WHERE status = 'confirmed'`, nothing about whether code reached main. The two AUTOMATED paths each proved the land first; the MANUAL route passed nothing, so a task confirmed with no pushed code could be shipped by its claim holder **or by any Metic+ sweeping `/merge-mode`**, booking its `task.shipped` credit. It stayed untracked for two months because its tracking task was closed in June asserting task 1001547 had delivered the gate — it had not, blocker 1000047's own final update says so, and the code agreed with the blocker rather than the closure note: **a hole hidden by a record nobody checked**, which is the same failure the gate is about. Decision: the gate lives at the WRITE, not the route (a route gate leaves the invariant unenforced at the layer that moves the ledger, so the next caller reopens it silently), and `shipTask` is FAIL-CLOSED on a required `landAssurance`. TWO kinds, and the second is why this does not make things worse: `proof` (this task's commit is on main) and **`no_artifact`** — a deliberate DB-only ship, an explicitly supported shape (CLAUDE.md "Direct DB writes are work too") that [task 1002478](https://cloudbongos.com/builders#/task/1002478) documents as a dead end below Archon. A naive proof-only gate would have made 1002478 strictly worse; naming no-artifact as a KIND turns it from an absence the gate must be talked out of into a recorded answer. Both carry a reason, audited post-commit and best-effort (an audit miss must never roll back a landed ship). `ship.js` now declares its own `--db-only`/`--allow-empty` lands, without which this fix would have regressed the very flow CLAUDE.md calls supported. Rejected: (b) gate only the route — leaves the write unguarded; (c) record the residual as tolerable — defensible on blast radius and genuinely close, but it would have been the FOURTH record asserting this was handled with nothing checkable behind it. Does NOT gate confirm: a codeless confirm is still accepted, it simply cannot reach `shipped` or pay.](<redacted>.md) | lifecycle / integrity |
|
|
262
272
|
| 0185 | [**The spark-handoff credit split: 70% developer, 30% spark author** ([task 1002923](https://cloudbongos.com/builders#/task/1002923), BV1.R147, goal 1000062 — completing the chain [task 1002920](https://cloudbongos.com/builders#/task/1002920) mark → [task 1002921](https://cloudbongos.com/builders#/task/1002921) queue → [task 1002922](https://cloudbongos.com/builders#/task/1002922) pickup). [ADR 0172](<redacted>.md) built the ideator streams and paid `captured_by`, the only person an idea then had. The spark handoff created a second: R146 develops a spark IN PLACE, preserving the author while recording a `developed_by`. **R146 therefore shipped paying NOTHING on purpose** — not an oversight but a refusal to guess, because `awardIdeaFullPassCredit`'s idempotency predicate is `(builder_id, reason)`, so paying the DEVELOPER would not have blocked a later payment to the AUTHOR and one idea would have paid twice; the cost of that caution was that a developed Full Idea received stream 2 **never** unless its author resubmitted. Owner decision (2026-08-22): **70/30 favouring the developer**, on BOTH existing streams, one ratio so there is one rule to explain. The reasoning is behavioural, not sentimental — developing someone else's spark is the scarce act the queue depends on, and at an even split it pays HALF what authoring your own Full Idea pays for the same writing work, so the rational move is always to write your own and the R145 queue stays a list nobody reads. The author keeps 30% plus their share of stream 1 on every task shipped under the idea, forever. `IDEA_DEVELOPER_SHARE = 0.70` is a **policy constant in `credits.js`** beside the pass bar and quick factor (ADR 0172's own consequence, the ADR 0023 precedent): a share of someone's earnings deserves a review and a commit, not a runtime edit. **The structural half is what retires R146's caution:** the economy module now derives the payees itself (`ideaCreditParties` reads `captured_by` + `developed_by`), so the caller can no longer choose a recipient — the develop route hands over an idea and a score, never a payee, and books the split like any other path (fail-open; the development has already committed). Three arithmetic properties, each a real bug if missed: the total is **conserved** (developer rounded, author gets the REMAINDER — rounding both pays 6 out of a 5-credit payout); a **self-developed** idea collapses to ONE payee, because two rows for one builder would silently UNDER-pay under the per-builder idempotency predicate, not double-pay (the comparison normalises via `String()` since `bigint` arrives as a string, so a type mismatch cannot become a phantom payee); and **zero-value rows are dropped**. Ids otherwise pass through unconverted — `Number()`-ing a `bigint` risks precision. `builder_id` in the idempotency predicate, which entered as an index optimisation (the R143/IC2 grader finding), is now **load-bearing for correctness**: two payees share one `reason`, so a reason-only predicate would refuse the second, and a partially-booked split completes on the next call. **Not decided here:** stream 3's split (ships with the Board Room, task 1002999), and whether `origin='develop'` in `ideas_task_lineage` belongs to whoever promotes a DEVELOPED idea into a task — ADR 0172 §5 says the row is "set at promotion and at develop (R146)", but R146 creates no task, so that origin stays deliberately unwired rather than invented; it changes who stream 1 pays.](<redacted>.md) | economy / ideator lane |
|
|
263
273
|
| 0186 | [**Per-project idea templates: the seam is already built; the economics are the open problem** ([task 1002932](https://cloudbongos.com/builders#/task/1002932), BV1.R156, goal 1000062 — a SPIKE, output is this ADR plus a task list, no implementation). The literal question — how does a founder define their own idea shape without a schema change or forking the scorer — is **already answered by shipped work**: R141 gave every template function an optional `registry` defaulting to the built-in one, so validation and scoring both read the declaration as data; `ideas_grades.template_key` is per-row with a format CHECK whose own comment says a project key "stays addressable"; and `provisioning_instances.settings jsonb` is an existing per-project home. **No schema change, no fork.** Decision A: a **`project-settings` kernel port**, sibling of `builder-settings`, read via `resolveOptional` so a vanilla instance with no provisioning module degrades to the built-in registry; `base-v1` stays the default and the fallback for an unresolvable key. Rejected a new `ideas_templates` table (second home for data that has one) and a config file (per-instance, but one instance hosts many projects). **Decision B is the finding that matters.** Completeness is normalised 0–100 WITHIN its template (mean of per-field ratios), while karma reads it against an absolute bar (`IDEA_FULL_PASS_BAR = 60`, `IDEA_FULL_PASS_CREDIT = 5`) — so template choice becomes a credit-minting lever: `base-v1` needs ~200 characters for 5 credits, a 1-field template with `substanceMin` 10 needs ~10, a ~20× arbitrage handed to exactly the projects this ADR empowers. Shipping A without B would make the feature a way to print credits. The fix separates the two questions one number is answering: **completeness stays relative** (it is the right signal for triage ordering and the hall meter) and **karma stops reading it**, switching to an absolute substance total the scorer already computes per field and currently discards after averaging. An interim guard (≥3 required fields, ≥120 total `substanceMin`) bounds the arbitrage without fixing it. Decision C: an optional per-field `weight` (default 1) turning the unweighted mean into a weighted one — today's behaviour is the all-weights-1 case, so it is a data addition and one arithmetic line. Decision D: the five base fields are **not** mandatory (forcing them would make "define your own" cosmetic), replaced by two constraints — a template must declare ≥1 required field, and a stored template is validated at WRITE time so a malformed one is refused there rather than discovered inside the capture path, which must never throw. **Boundary note:** goal 1000062's wall is `[ideas, hall-ui]`, so the port task (provisioning + kernel) and the karma task (economy) cannot be claimed under it.](<redacted>.md) | ideas / templates |
|
|
264
|
-
| 0187 | [**Collab's four carried-forward questions, answered** ([task 1003170](https://cloudbongos.com/builders#/task/1003170), goal 1000075). The goal deliberately carried four open questions under the owner's instruction to "answer them in the goal, do not let them be decided by accident" — each being the kind an implementation settles silently by picking whichever branch was easier to code. **(1) Closeness is a WEIGHTED BLEND** of co-claims, co-build sessions, goals shipped together and (per 4a) comms/mentions — not one signal. Two parts of the decision, not follow-ups: the weights are DATA rather than a buried constant, and "why am I close to them" must be answerable on the surface that shows it, because a number that ranks colleagues and cannot say what it is made of becomes unfair with nothing to point at. Consent is unchanged — ADR 0171 fail-closed, hide enforced in the READ path, a hidden builder is not a node in anyone's graph. **(2) Passing RECOMMENDS, never transfers**: the recipient still claims it, claim/release stays the only path to holding work, declining is free, and the ledger records who recommended it and why. Transfer-on-pass was rejected because it would stop the claim ledger meaning "this person chose this work". **(3) Co-build is a GOAL-level fact, not a shared claim — and the reward question was WRONG-PREMISED.** Owner, verbatim: nobody will "co run the task on their claims"; collaboration happens at the goal. So each builder holds and is paid for their own task exactly as today and there is NO split to design — recorded because it is a deletion: a reward-splitting mechanism would otherwise have been designed, reviewed and shipped for a situation that does not occur. **(4) Mingle pairs CROSS-CRAFT** (engineer with artist or ideator), same-craft rejected as duplicating the craft-matched Collab path; the two-switch consent rule stands, since one switch is not consent for a real-world social commitment. **4a adds scope:** Discord mirrors INWARD so comms and mentions are visible in-hall — which puts attacker-authorable text on an in-hall surface, so the trust posture for mirrored content is this goal's to answer rather than assume (Discord is outbound-only and outside the boundary today, ADR 0032/0033). It is BLOCKED on task 1002946 (the Agora, goal 1000064, unstarted pending team buy-in), and this goal must not reimplement the Agora to get around it. No criterion was added for it deliberately — one blocked on another goal's unstarted work would make 1000075 unachievable for reasons unrelated to collaboration.](<redacted>.md) | collab / social hall |
|
|
265
|
-
| 0187 | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
|
|
274
|
+
| 0187-a | [**Collab's four carried-forward questions, answered** ([task 1003170](https://cloudbongos.com/builders#/task/1003170), goal 1000075). The goal deliberately carried four open questions under the owner's instruction to "answer them in the goal, do not let them be decided by accident" — each being the kind an implementation settles silently by picking whichever branch was easier to code. **(1) Closeness is a WEIGHTED BLEND** of co-claims, co-build sessions, goals shipped together and (per 4a) comms/mentions — not one signal. Two parts of the decision, not follow-ups: the weights are DATA rather than a buried constant, and "why am I close to them" must be answerable on the surface that shows it, because a number that ranks colleagues and cannot say what it is made of becomes unfair with nothing to point at. Consent is unchanged — ADR 0171 fail-closed, hide enforced in the READ path, a hidden builder is not a node in anyone's graph. **(2) Passing RECOMMENDS, never transfers**: the recipient still claims it, claim/release stays the only path to holding work, declining is free, and the ledger records who recommended it and why. Transfer-on-pass was rejected because it would stop the claim ledger meaning "this person chose this work". **(3) Co-build is a GOAL-level fact, not a shared claim — and the reward question was WRONG-PREMISED.** Owner, verbatim: nobody will "co run the task on their claims"; collaboration happens at the goal. So each builder holds and is paid for their own task exactly as today and there is NO split to design — recorded because it is a deletion: a reward-splitting mechanism would otherwise have been designed, reviewed and shipped for a situation that does not occur. **(4) Mingle pairs CROSS-CRAFT** (engineer with artist or ideator), same-craft rejected as duplicating the craft-matched Collab path; the two-switch consent rule stands, since one switch is not consent for a real-world social commitment. **4a adds scope:** Discord mirrors INWARD so comms and mentions are visible in-hall — which puts attacker-authorable text on an in-hall surface, so the trust posture for mirrored content is this goal's to answer rather than assume (Discord is outbound-only and outside the boundary today, ADR 0032/0033). It is BLOCKED on task 1002946 (the Agora, goal 1000064, unstarted pending team buy-in), and this goal must not reimplement the Agora to get around it. No criterion was added for it deliberately — one blocked on another goal's unstarted work would make 1000075 unachievable for reasons unrelated to collaboration.](<redacted>.md) | collab / social hall |
|
|
275
|
+
| 0187-b | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
|
|
266
276
|
| 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](<redacted>.md) | security / enforcement durability |
|
|
267
277
|
| 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](<redacted>.md) | quality ratchets / debt governance |
|
|
268
278
|
| 0190 | [**A changed project setting reaches a running instance by an in-place env patch and a restart — never a template re-write** ([task 1003140](https://cloudbongos.com/builders#/task/1003140), goal 1000058). Task 1003139 made a NEW instance read `platform_visibility` / `joinability` from the web.env the runner writes; a RUNNING instance loads its branding pack once, memoized and deep-frozen for the process lifetime, so a changed value could not reach it at all — the settings were write-only state on the platform. Decision: `PATCH /provisioning/instances/:id/settings` writes the platform row first, then for an ACTIVE instance on a shape the runner can reach enqueues a `settings-apply` intent (migration provisioning_010); the control-plane runner reads the instance's web.env as it stands, upserts ONLY the two settings lines (`upsertEnvVars`), and RESTARTS the service. The in-place patch is the load-bearing choice: the natural move — re-running `webEnvBody` as domain-attach does — regenerates the file from the row, and the row does not know the OAuth client secret or the App private key that `placeManifestCreds` placed later, so a template re-write silently blanks sign-in on the very instance the owner just touched. `SETTINGS_APPLY_FROM = {active}` because a pre-active instance gets the same values at standup and a torn-down one has nothing to restart (the provision intent cannot stand in — it excludes `active`, task 1002691); `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` because a dedicated droplet has no post-hoc exec path, and a push that cannot land is refused at the route AND the runner rather than reported. The response is a receipt, not a claim: it says the restart is coming and that the instance keeps its current setting until then, and the read-back lives on the instance's own public `GET /instance` manifest, whose `brand.project` now carries the resolved policy (`clientBranding` allow-list widened deliberately). Rejected: the template re-write; re-provisioning as the push; skipping the restart (impossible while the pack is memoized); claiming the new state on PATCH return.](<redacted>.md) | provisioning / settings channel |
|
|
269
279
|
|
|
270
280
|
| 0191 | [**Majority, and an author who may vote but never decide alone** ([task 1003273](https://cloudbongos.com/builders#/task/1003273); diagnosis and companion in [task 1003271](https://cloudbongos.com/builders#/task/1003271)). Supersedes ADR 0175 §3's pass-rule set and §§7/11's flat anti-self rule. The owner reported voting as broken; nothing was broken. Nine items sat open under `first_ratifier` with four archons active — any one of three non-authors could have ratified any of them in a click — but every item was authored by the one person looking at them, and ADR 0175's anti-self rule was enforced by rendering NO BALLOT, so from the owner's seat the feature did not exist. The lesson: a rule enforced by removing the control is indistinguishable from a broken feature. Two decisions. (1) `majority` joins PASS_RULES: strictly more than half of the resolved MEMBERSHIP must vote yes — of the membership, not of the votes cast, so low turnout cannot quietly pass things; a tie is not a majority. The three rules are NOT a ladder of strictness, they differ on WHAT SILENCE MEANS — `first_ratifier` and `consent` pass an unvoted item, `majority` returns it — which is the axis that matters and now the comment in config.js. (2) An author MAY vote and may NEVER DECIDE ALONE: the rule moved from refusing the cast to the two places something is actually decided — `shouldCloseAfterVote` needs a non-author vote under every rule, and the majority tally needs a non-author YES. Their yes still counts toward the numerator; their `no` is excluded from the objection test (else silence would be an author's safest act); a self-cast earns no karma. The ADR 0175 §7 amendment carve-out survives as the author rule's SCOPE, not an exception: `authorRuleApplies` is full_idea-only, so a sole Archon still ratifies their own amendment. Also records why the CLOCK is a consequence of majority and not an independent fix — `tallyOutcome` passes on ZERO votes, so adding a deadline under `first_ratifier` would have closed all nine items as ratified-unread, paying karma and credits on ideas nobody read; `BOARD_DEFAULTS.window_minutes` therefore stays `null` in the neutral core. States the cost plainly: three affirmative votes are needed on a four-member board, and a solo instance still cannot ratify its own ideas. Discord stays outbound-only (ADR 0175 §8 re-affirmed).](<redacted>.md) | government / board room |
|
|
271
|
-
| 0192 | [**A gated project refuses strangers at the app's front door, and the hub reads what the project runs from the project itself** ([task 1003043](https://cloudbongos.com/builders#/task/1003043), goal 1000058). Task 1003040 gave every project a `platform_visibility` setting and 1003139/1003140 carried it into the instance's `branding().project.platformVisibility` — resolved, enforced by nothing. Decision: ONE app-level middleware (`src/bongos/platform-visibility-gate.js`), mounted beside the pre-launch gate ahead of every human and public-read surface — the landing, the hub pages, the status host, the hall shell, `/public/*` and every module's `rank: public` route — rather than per-surface wiring, which misses the next surface nobody told it about. "Member" is a live, non-deactivated session on this instance, the same `resolveSession` lookup `requireBuilder` uses, per request, no cache; a lookup that throws fails CLOSED. The exempt list is the door, the manifest and the probes and nothing wider: `/api/*/auth/*` (a door that cannot be opened locks the members out too), `/api/*/instance` (what a stranger reads to learn how to sign in AND what the runner reads after a settings restart to learn whether the value was consumed — including the push that turns this gate on), `/healthz`, `/version`, `/metrics`; the pre-launch gate's blanket `/api/` exemption is deliberately not copied. A refused page gets a self-contained 401 door (no stylesheet, no branding bundle, no script) with one control — Sign in, returning (relatively) to the page knocked on; a refused API call gets `res.fail('unauthenticated', 401)`, exactly requireBuilder's envelope, which is why it mounts after `attachFail`. A member's answer leaves marked `private, no-store` whatever the route said — the public read API sends `public, max-age`, and a CDN edge would otherwise serve a member's copy to the next stranger. The exempt list covers every API prefix (the versioned contract too), `/downloads/*` and Caddy's TLS ask; a request's session is resolved once (`resolveSession` memoizes per request). The hub's manage page shows the CHOICE from the platform's row and what the project RUNS from the project's own public manifest (`brand.project`), and never confuses the two: `push.message` is rendered verbatim, `saved` moves only the radio, the "runs" line is written only from the manifest and says "doesn't report this setting yet" for a core that predates the channel. Rejected: per-surface wiring; a 302 into OAuth for a refused page (sends assets and crawlers to GitHub); copying the `/api/` exemption; painting the radio from the manifest or the runs line from `saved`; reusing the pre-launch gate.](<redacted>.md) | instance / project visibility |
|
|
272
|
-
| 0192 | [**A work category orients a human and authorises nothing** ([task 1003275](https://cloudbongos.com/builders#/task/1003275), goal 1000072, criterion `legibility-categories`). A goal gains a human CATEGORY from an owner-named set of eight (Builders Hall, Context Management, Government (Decision Making), Modules, Security, Economics, Legal, User/ABI Experience) so a person scanning the board can tell at a glance which goals are about money, which about the walls, which about the hall — today each goal's own words say so, but only if you read all thirty-eight. The DECISION is the constraint, not the feature: the owner said twice that this organisation has no hierarchical purpose and no effect on the agentic management of the work, and the system ALREADY has a load-bearing hierarchy (ADR 0086 — the scope_modules wall, rank floors, goal membership). The goal's own prose predicts the failure: a second thing that looks like a hierarchy gets mistaken for one within a release, and the mistake arrives as ONE plausible line in a gate ('skip Legal goals in the feed'), after which work is refused because of a label nobody meant as a permission. So: no authority, claim-eligibility, feed or dependency surface may read a category — enforced by the `scripts/gds/category-advisory-guard.js` fitness check, which scans every decision surface with comments stripped (a gate may CITE the rule, never branch on it) AND asserts the feature still exists, so a renamed-away subject fails loudly instead of passing vacuously (the ADR 0022 assurance-of-assurance finding). Uncategorised is LEGAL — nullable, no default, required by no write — because the criterion is explicit that Claude failing to place a goal is a designed prompt, not an error. A task stores no category and INHERITS its goal's through `tasks.goal_id`; a stored copy would be a second answer to one question. The set is a table, not an enum (runtime-extensible without a migration per addition) and retiring is `active = false`, never a DELETE, so a goal keeps rendering a label someone tidied away. The three route-rank pins are a tripwire: if one ever needs a WIDER gate than its neighbours, the label has grown into a permission. Deliberately NOT built: a per-category page or filtered navigation view — a chip is a label, a drill-down destination is a tier. Making a category gate anything is an owner decision and needs its own ADR first.](<redacted>.md) | GDS / goals |
|
|
281
|
+
| 0192-a | [**A gated project refuses strangers at the app's front door, and the hub reads what the project runs from the project itself** ([task 1003043](https://cloudbongos.com/builders#/task/1003043), goal 1000058). Task 1003040 gave every project a `platform_visibility` setting and 1003139/1003140 carried it into the instance's `branding().project.platformVisibility` — resolved, enforced by nothing. Decision: ONE app-level middleware (`src/bongos/platform-visibility-gate.js`), mounted beside the pre-launch gate ahead of every human and public-read surface — the landing, the hub pages, the status host, the hall shell, `/public/*` and every module's `rank: public` route — rather than per-surface wiring, which misses the next surface nobody told it about. "Member" is a live, non-deactivated session on this instance, the same `resolveSession` lookup `requireBuilder` uses, per request, no cache; a lookup that throws fails CLOSED. The exempt list is the door, the manifest and the probes and nothing wider: `/api/*/auth/*` (a door that cannot be opened locks the members out too), `/api/*/instance` (what a stranger reads to learn how to sign in AND what the runner reads after a settings restart to learn whether the value was consumed — including the push that turns this gate on), `/healthz`, `/version`, `/metrics`; the pre-launch gate's blanket `/api/` exemption is deliberately not copied. A refused page gets a self-contained 401 door (no stylesheet, no branding bundle, no script) with one control — Sign in, returning (relatively) to the page knocked on; a refused API call gets `res.fail('unauthenticated', 401)`, exactly requireBuilder's envelope, which is why it mounts after `attachFail`. A member's answer leaves marked `private, no-store` whatever the route said — the public read API sends `public, max-age`, and a CDN edge would otherwise serve a member's copy to the next stranger. The exempt list covers every API prefix (the versioned contract too), `/downloads/*` and Caddy's TLS ask; a request's session is resolved once (`resolveSession` memoizes per request). The hub's manage page shows the CHOICE from the platform's row and what the project RUNS from the project's own public manifest (`brand.project`), and never confuses the two: `push.message` is rendered verbatim, `saved` moves only the radio, the "runs" line is written only from the manifest and says "doesn't report this setting yet" for a core that predates the channel. Rejected: per-surface wiring; a 302 into OAuth for a refused page (sends assets and crawlers to GitHub); copying the `/api/` exemption; painting the radio from the manifest or the runs line from `saved`; reusing the pre-launch gate.](<redacted>.md) | instance / project visibility |
|
|
282
|
+
| 0192-b | [**A work category orients a human and authorises nothing** ([task 1003275](https://cloudbongos.com/builders#/task/1003275), goal 1000072, criterion `legibility-categories`). A goal gains a human CATEGORY from an owner-named set of eight (Builders Hall, Context Management, Government (Decision Making), Modules, Security, Economics, Legal, User/ABI Experience) so a person scanning the board can tell at a glance which goals are about money, which about the walls, which about the hall — today each goal's own words say so, but only if you read all thirty-eight. The DECISION is the constraint, not the feature: the owner said twice that this organisation has no hierarchical purpose and no effect on the agentic management of the work, and the system ALREADY has a load-bearing hierarchy (ADR 0086 — the scope_modules wall, rank floors, goal membership). The goal's own prose predicts the failure: a second thing that looks like a hierarchy gets mistaken for one within a release, and the mistake arrives as ONE plausible line in a gate ('skip Legal goals in the feed'), after which work is refused because of a label nobody meant as a permission. So: no authority, claim-eligibility, feed or dependency surface may read a category — enforced by the `scripts/gds/category-advisory-guard.js` fitness check, which scans every decision surface with comments stripped (a gate may CITE the rule, never branch on it) AND asserts the feature still exists, so a renamed-away subject fails loudly instead of passing vacuously (the ADR 0022 assurance-of-assurance finding). Uncategorised is LEGAL — nullable, no default, required by no write — because the criterion is explicit that Claude failing to place a goal is a designed prompt, not an error. A task stores no category and INHERITS its goal's through `tasks.goal_id`; a stored copy would be a second answer to one question. The set is a table, not an enum (runtime-extensible without a migration per addition) and retiring is `active = false`, never a DELETE, so a goal keeps rendering a label someone tidied away. The three route-rank pins are a tripwire: if one ever needs a WIDER gate than its neighbours, the label has grown into a permission. Deliberately NOT built: a per-category page or filtered navigation view — a chip is a label, a drill-down destination is a tier. Making a category gate anything is an owner decision and needs its own ADR first.](<redacted>.md) | GDS / goals |
|
|
273
283
|
| 0193 | [**Pause task-scoped box slices: a box serves its rank scope (the full repo for Metic+)** ([task 1003285](https://cloudbongos.com/builders#/task/1003285), owner decision from the 2026-08-25 dev-box efficiency review). Pauses, does not repeal, ADR 0148 / goal 1000051: `routes/box.js` skips the lifecycle-port `scopeKeys` resolution unless `BOX_TASK_SCOPE=on`, so `decideSourceAccess` takes its documented rank fallback — full repo for Metic+, starter surface below. WHY: the slice was defense-in-depth git cannot hard-enforce (ADR 0031 §6's honest limit) while its costs were real on the live fleet — sessions on a box that cannot see a module (or its nested CLAUDE.md) misdiagnose themselves as stuck, the scope machinery kept failing in the field (task 1002729's base-slice collapse), and every live box user is Metic+ anyway, so the slice narrowed trusted builders while newcomers were already gated by rank. UNCHANGED: live-rank allow/deny per request (ADR 0016), the provision floor and demotion clawback, the read-only per-request credential (ADR 0055 — push still 403s, re-verified on hardware in task 1003253), and sub-Metic starter boxes. `GET /box/scope` now reports `task_scoping_paused` so the hall/app can say why `task_scope` is false; the app task-picker's "scoped to `<modules>` and nothing else" copy overstates while paused (cosmetic). Revert = set the env flag; make-permanent = delete the resolution + a follow-up ADR.](<redacted>.md) | dev-box |
|
|
274
284
|
| 0194 | [**A project's membership door is one decision, answered the same way everywhere a stranger meets it** ([task 1003044](https://cloudbongos.com/builders#/task/1003044), goal 1000058). The owner's 2026-08-16 joinability setting — open / apply / invite_only — was stored (1003040), pushed (1003139/1003140) and resolved into `branding().project.joinability`, enforced by nothing. Decision: ONE reader, `joinabilityMode()` in `auth-admission.js`, and `openEnrollmentEnabled()` becomes `mode === 'open'`, so the four sign-in call sites, `webAdmissionStatus` and the manifest's `admission.policy` follow untouched (SR-19: one gate, no drift). The legacy `OPEN_XENOS_ENROLLMENT` flag still opens an APPLY project — a self-hosted instance that set it keeps its door across the upgrade — but never reopens an explicit invite_only; an unreadable pack or a value this core cannot read fails CLOSED (invite_only), the ADR 0192 rule. The public `POST /access-requests` answers the door FIRST — before validation, before any lookup — `403 invite_only` / `409 open_enrollment` with plain sentences: no query, nothing revealed about who exists, and DB-free enough for the real booted app to prove. An Archon's invite is the row the gate already reads: `POST /access-requests/invite` writes `access_requests` at 'invited' (resolving a pending one in place), so sign-in consults nothing new — surfaced as "Invite by GitHub username" on the Watch page. The door is SAID wherever a stranger meets it, in the project's words: the hall landing renders one door per mode from the injected pack, the web 403 names the invite-only door and the way in, the CLI stops instead of filing, the hub's join relay names the door in the hub's own words (a project is trusted to build, not to write prose into the platform's UI) instead of painting a refusal as "requested" or "unreachable". `admission.policy` stays binary (pinned; agrees for free); `joinabilityMode` joins the doorway — a human MINOR by ADR 0161 §2, which the publish lane's LIVE test refused (HEAD's version must be a CI carrier's), so it rides the next CI patch and the contradiction is filed as an idea. The hub's `#mJoinability` clones `#mVisibility` rather than a factory — a third setting is where a factory pays. Rejected: folding the flag into `branding()` (memoized vs per-call; the manifest must stay the pushed value); a 200 for the open door (an older CLI would wait thirty minutes for an approval); re-deriving the mode inside the module; a second invite table.](<redacted>.md) | instance / project joinability |
|
|
285
|
+
| 0195 | [**ADR numbers are checked like migration numbers, and the twenty existing collisions are grandfathered by name** ([task 1003164](https://cloudbongos.com/builders#/task/1003164)). Two ADRs could share a number and nothing flagged it — git sees no path collision, the index auto-merges into two rows, and every gate passes; it was found by a human reading `ls docs/adr/`, after a task note had already cited a number the collision made point at an unrelated document. Migrations had this guard already (`stemCollisions()` hard-fails CI, because a re-used migration number silently skips a migration); ADRs had only a best-effort allocator at claim time and nothing afterwards. `scripts/gds/adr-namespace.js` is the sibling module, wired into fitness as Check 31 and so into the required `unit` gate. Building it found twenty live collisions, not one, plus fourteen index defects of the same merge class — two ADRs listed twice and twelve with no row at all, ten of them the second file of a colliding pair, which is how a collision hides. The twenty pairs are grandfathered by EXACT FILENAME rather than renumbered: renumbering breaks every citation to them, which is the harm the check exists to prevent, and set-equality keeps the exemption as narrow as the history that earned it — a third file on a legacy number, or a rename of either half, still fails, and the list may only shrink. The index half is keyed on LINK TARGET, not number (a number rule cannot express a grandfathered pair): every file has exactly one row, every row points at a file that exists. Row ordering warns and never blocks — ten legacy rows are out of order and no PR author caused that (the ADR 0189 split). Rejected: renumbering; grandfathering by number; a numeric cutoff like LEGACY_BARE_CORE_MAX (a new ADR takes a number above the max, so it would catch nothing); fixing it in the claim-time allocator (which already tries, against a stale tree — that is how the collision happened).](<redacted>.md) | docs / numbering discipline |
|
|
275
286
|
|
|
276
|
-
> ⚠️ **Numbering
|
|
287
|
+
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.384",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.384",
|
|
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.
|
|
3
|
+
"version": "1.19.384",
|
|
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",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// adr-namespace.js — ADR numbering discipline (task 1003164). The ADR half of the guard
|
|
4
|
+
// `migration-namespace.js` already gives migrations. Pure helpers over file lists + index
|
|
5
|
+
// text, plus the fitness check that reads the tree.
|
|
6
|
+
//
|
|
7
|
+
// PROBLEM. An ADR number is a CITATION TARGET. ADRs cite each other by number, prose and
|
|
8
|
+
// task notes cite them by number, and `docs/adr/README.md` is the index every agent is
|
|
9
|
+
// told to consult. Two files can carry the same number and NOTHING notices: the filenames
|
|
10
|
+
// differ (`0181-ship-requires-an-assurance.md` vs
|
|
11
|
+
// `0181-teardown-state-gate-and-address-retention.md`), so there is no path collision, no
|
|
12
|
+
// merge conflict, and git adds both cleanly. README.md auto-merges too, ending up with two
|
|
13
|
+
// `| 0181 |` rows. Unit, fitness, docs-entropy and the grader all passed; it was found on
|
|
14
|
+
// 2026-08-22 only because a human read `ls docs/adr/`. By then task 1000900's
|
|
15
|
+
// close-rationale had already cited "ADR 0180", which the collision had made point at an
|
|
16
|
+
// unrelated document — a citation that is wrong forever.
|
|
17
|
+
//
|
|
18
|
+
// THE ASYMMETRY THIS CLOSES. Migrations have exactly this protection already: a re-used
|
|
19
|
+
// migration number silently SKIPS a migration, so `stemCollisions()` hard-fails CI. ADRs
|
|
20
|
+
// got `next-number.js` — which computes the next free number at claim time, on a
|
|
21
|
+
// possibly-stale tree, best-effort — and then nothing enforced it afterwards. Same shared,
|
|
22
|
+
// monotonic, human-allocated number line; one guarded, one not. A stale claim card, two
|
|
23
|
+
// builders claiming the same day, or a branch that sits long enough all reproduce it.
|
|
24
|
+
//
|
|
25
|
+
// WHY GRANDFATHER RATHER THAN RENUMBER. When this landed the live tree already held TWENTY
|
|
26
|
+
// colliding numbers, not the one the bug report found. Renumbering nineteen pairs would
|
|
27
|
+
// break every existing citation to them — which is the very harm this check exists to
|
|
28
|
+
// prevent — and README.md states the rule outright: a number, once assigned, is never
|
|
29
|
+
// reused. So the existing pairs are frozen into LEGACY_DUPLICATE_ADRS by their EXACT
|
|
30
|
+
// filenames (the shape migration-namespace.js uses for its own grandfathering, and for
|
|
31
|
+
// RENAMED_MODULE_PREFIXES). Frozen by exact filename, not by number, is the load-bearing
|
|
32
|
+
// part: a THIRD file joining a legacy number still fails, and so does a rename of either
|
|
33
|
+
// half. The list can only shrink. See ADR 0195.
|
|
34
|
+
//
|
|
35
|
+
// The index half is checked by LINK TARGET, not by number, because a number rule alone
|
|
36
|
+
// cannot express the grandfathered pairs: the invariant is that every ADR file has exactly
|
|
37
|
+
// one row and every row points at a file that exists. That catches the dangling row AND
|
|
38
|
+
// the duplicated row AND the file nobody indexed — the last of which is how a collision
|
|
39
|
+
// actually hides, since the second file of a colliding pair is the one that never gets a
|
|
40
|
+
// row.
|
|
41
|
+
|
|
42
|
+
const fs = require('node:fs');
|
|
43
|
+
const path = require('node:path');
|
|
44
|
+
const { CORE_ROOT } = require('./fitness-lib.js');
|
|
45
|
+
|
|
46
|
+
// ---- parsing ---------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
// `NNNN-kebab-title.md` — the naming rule in docs/adr/README.md §File naming.
|
|
49
|
+
const ADR_RE = /^(\d{4})-(.+)\.md$/;
|
|
50
|
+
|
|
51
|
+
// Parse an ADR basename into { number, num, slug, ok }. `number` is the 4-digit STRING
|
|
52
|
+
// (the citation form, zero-padded); `num` is the integer for ordering/allocation. A
|
|
53
|
+
// non-ADR name (README.md, a draft) returns { ok: false }.
|
|
54
|
+
function parseAdrName(basename) {
|
|
55
|
+
const m = ADR_RE.exec(String(basename == null ? '' : basename));
|
|
56
|
+
if (!m) return { number: null, num: null, slug: null, ok: false };
|
|
57
|
+
return { number: m[1], num: parseInt(m[1], 10), slug: m[2], ok: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The 4-digit number string of an ADR basename, or null if it is not an ADR.
|
|
61
|
+
function adrNumberOf(basename) {
|
|
62
|
+
const p = parseAdrName(basename);
|
|
63
|
+
return p.ok ? p.number : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- grandfathered collisions ---------------------------------------------
|
|
67
|
+
|
|
68
|
+
// Numbers already shared by two files when this check landed (task 1003164, 2026-08-27).
|
|
69
|
+
// Frozen by EXACT filename: a third file on one of these numbers, or a rename of either
|
|
70
|
+
// half, is still a hard failure. This list may shrink (if a pair is ever consolidated) and
|
|
71
|
+
// must never grow — a new collision is the bug the check exists to catch.
|
|
72
|
+
const LEGACY_DUPLICATE_ADRS = Object.freeze({
|
|
73
|
+
'0008': Object.freeze(['0008-family-based-tile-generation.md', '0008-google-chat-oauth-user-auth.md']),
|
|
74
|
+
'0024': Object.freeze(['0024-cloneable-repo-local-first-memory.md', '0024-multi-agent-system-architecture.md']),
|
|
75
|
+
'0025': Object.freeze(['0025-offsite-backup-vendor-digitalocean-spaces.md', '0025-structured-criterion-task-link.md']),
|
|
76
|
+
'0044': Object.freeze(['0044-mediterranean-palette-replacement.md', '0044-per-box-live-game-preview.md']),
|
|
77
|
+
'0057': Object.freeze(['0057-container-cost-ledger.md', '0057-discord-archon-approval-channels.md']),
|
|
78
|
+
'0072': Object.freeze(['0072-bongos-app-mac-signed-first-windows-deferred.md', '0072-dev-box-code-staleness-visibility.md']),
|
|
79
|
+
'0073': Object.freeze(['0073-builder-needs-signal-and-byok-gemini-key.md', '0073-secrets-scan-exclude-uri-detector.md']),
|
|
80
|
+
'0082': Object.freeze(['0082-productized-key-storage-provisioning.md', '0082-server-side-merge-conflict-auto-resolution.md']),
|
|
81
|
+
'0087': Object.freeze(['0087-bongos-app-architecture-and-handoff-contract.md', '0087-compete-on-governance-not-tooling-cursor.md']),
|
|
82
|
+
'0095': Object.freeze(['0095-borrowed-memory-and-retrieval-concepts.md', '0095-cross-agent-context-management.md']),
|
|
83
|
+
'0097': Object.freeze(['0097-one-active-claim-per-session-and-worktree-binding.md', '0097-retroactive-reward-backfill.md']),
|
|
84
|
+
'0103': Object.freeze(['0103-core-first-extraction-cloud-bongos-trunk.md', '0103-gdsv4-bongos-consolidation.md']),
|
|
85
|
+
'0144': Object.freeze(['0144-devbox-rehome-onto-cloudbongos-plane.md', '0144-federated-single-logout-backchannel.md']),
|
|
86
|
+
'0145': Object.freeze(['0145-devbox-app-branding-driven-module.md', '0145-free-hosted-project-tier-isolation-and-domain-separation.md']),
|
|
87
|
+
'0152': Object.freeze(['0152-landing-gate-served-by-the-core-app.md', '0152-metic-task-abandonment.md']),
|
|
88
|
+
'0167': Object.freeze(['0167-gate-trust-link-agpl-default.md', '0167-module-catalog-source-vs-provenance.md']),
|
|
89
|
+
'0172': Object.freeze(['0172-editable-rank-roles-substrate-axis.md', '0172-per-craft-compensation-ideator-credit-lane.md']),
|
|
90
|
+
'0184': Object.freeze(['0184-a-task-visual-inherits-its-summary-audience.md', '0184-ship-requires-an-assurance.md']),
|
|
91
|
+
'0187': Object.freeze(['0187-collab-four-decisions.md', '0187-oauth-handshake-cookie-lifetime.md']),
|
|
92
|
+
'0192': Object.freeze(['0192-a-category-orients-and-authorises-nothing.md', '0192-platform-visibility-member-door.md']),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Is this exact set of files the frozen legacy pair for that number? Set-equality, so
|
|
96
|
+
// adding a third file or renaming either half stops being grandfathered.
|
|
97
|
+
function isGrandfathered(number, files) {
|
|
98
|
+
const legacy = LEGACY_DUPLICATE_ADRS[number];
|
|
99
|
+
if (!legacy) return false;
|
|
100
|
+
if (files.length !== legacy.length) return false;
|
|
101
|
+
const a = [...files].sort();
|
|
102
|
+
const b = [...legacy].sort();
|
|
103
|
+
return a.every((f, i) => f === b[i]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- collision detector ----------------------------------------------------
|
|
107
|
+
|
|
108
|
+
// Given ADR basenames, return [{ number, files[] }] for every number claimed by more than
|
|
109
|
+
// one file, EXCLUDING the frozen legacy pairs. The direct analogue of
|
|
110
|
+
// migration-namespace.js stemCollisions().
|
|
111
|
+
function numberCollisions(basenames) {
|
|
112
|
+
const byNumber = new Map();
|
|
113
|
+
for (const name of basenames || []) {
|
|
114
|
+
const p = parseAdrName(name);
|
|
115
|
+
if (!p.ok) continue;
|
|
116
|
+
if (!byNumber.has(p.number)) byNumber.set(p.number, new Set());
|
|
117
|
+
byNumber.get(p.number).add(name);
|
|
118
|
+
}
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const [number, set] of byNumber) {
|
|
121
|
+
const files = [...set].sort();
|
|
122
|
+
if (files.length > 1 && !isGrandfathered(number, files)) out.push({ number, files });
|
|
123
|
+
}
|
|
124
|
+
return out.sort((a, b) => a.number.localeCompare(b.number));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The next free ADR number (integer): max + 1, 1 on an empty tree. Mirrors next-number.js
|
|
128
|
+
// nextFree so a caller can allocate without reaching for git.
|
|
129
|
+
function nextAdrNumber(basenames) {
|
|
130
|
+
let max = 0;
|
|
131
|
+
for (const name of basenames || []) {
|
|
132
|
+
const p = parseAdrName(name);
|
|
133
|
+
if (p.ok && p.num > max) max = p.num;
|
|
134
|
+
}
|
|
135
|
+
return max + 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- index (docs/adr/README.md) --------------------------------------------
|
|
139
|
+
|
|
140
|
+
// An index row: `| NNNN | [Title](NNNN-slug.md) | topic |`. The leading cell may carry a
|
|
141
|
+
// disambiguating suffix (`0008-a`) for a grandfathered pair, so the number is read from
|
|
142
|
+
// the leading cell and the row's OWN file from the link target that starts with it — row
|
|
143
|
+
// prose cites other ADRs by link too, and only the own-number target is the row's subject.
|
|
144
|
+
const INDEX_ROW_RE = /^\|\s*(\d{4})(?:-[a-z])?\s*\|/;
|
|
145
|
+
const INDEX_LINK_RE = /\]\((\d{4}-[^)]*\.md)\)/g;
|
|
146
|
+
|
|
147
|
+
// Parse README text into [{ number, target, targets, lineNo, ok }]. `target` is the row's
|
|
148
|
+
// own ADR filename, or null when the row names none (or names more than one of its own
|
|
149
|
+
// number, which is ambiguous and reported as a violation).
|
|
150
|
+
function parseIndexRows(readmeText) {
|
|
151
|
+
const rows = [];
|
|
152
|
+
const lines = String(readmeText == null ? '' : readmeText).split('\n');
|
|
153
|
+
lines.forEach((line, i) => {
|
|
154
|
+
const m = INDEX_ROW_RE.exec(line);
|
|
155
|
+
if (!m) return;
|
|
156
|
+
const number = m[1];
|
|
157
|
+
INDEX_LINK_RE.lastIndex = 0;
|
|
158
|
+
const own = [...line.matchAll(INDEX_LINK_RE)].map((x) => x[1]).filter((t) => t.startsWith(number + '-'));
|
|
159
|
+
rows.push({ number, target: own.length === 1 ? own[0] : null, targets: own, lineNo: i + 1, ok: own.length === 1 });
|
|
160
|
+
});
|
|
161
|
+
return rows;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The index invariant, checked by LINK TARGET: every ADR file has exactly one row, and
|
|
165
|
+
// every row points at a file that exists. Returns violation strings.
|
|
166
|
+
function indexViolations({ basenames = [], rows = [] } = {}) {
|
|
167
|
+
const violations = [];
|
|
168
|
+
const files = new Set((basenames || []).filter((n) => parseAdrName(n).ok));
|
|
169
|
+
|
|
170
|
+
const rowsByTarget = new Map();
|
|
171
|
+
for (const r of rows) {
|
|
172
|
+
if (!r.ok) {
|
|
173
|
+
violations.push(
|
|
174
|
+
`docs/adr/README.md:${r.lineNo}: the index row for ${r.number} names ${r.targets.length} link(s) to its own number `
|
|
175
|
+
+ `(${r.targets.join(', ') || 'none'}) — a row must link its own ADR file exactly once so the index can be checked against the tree.`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!rowsByTarget.has(r.target)) rowsByTarget.set(r.target, []);
|
|
179
|
+
rowsByTarget.get(r.target).push(r.lineNo);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const [target, lineNos] of [...rowsByTarget.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
183
|
+
if (!files.has(target)) {
|
|
184
|
+
violations.push(
|
|
185
|
+
`docs/adr/README.md:${lineNos[0]}: index row points at docs/adr/${target}, which does not exist — a dangling index row `
|
|
186
|
+
+ `(the other half of the collision problem: a merge can leave a row behind after the file it named moved or was renumbered).`);
|
|
187
|
+
}
|
|
188
|
+
if (lineNos.length > 1) {
|
|
189
|
+
violations.push(
|
|
190
|
+
`docs/adr/README.md: ${target} has ${lineNos.length} index rows (lines ${lineNos.join(', ')}) — exactly one row per ADR; `
|
|
191
|
+
+ 'a duplicated row is what an auto-merged index leaves behind.');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const f of [...files].sort()) {
|
|
196
|
+
if (!rowsByTarget.has(f)) {
|
|
197
|
+
violations.push(
|
|
198
|
+
`docs/adr/${f} has no row in docs/adr/README.md — every ADR is indexed. (This is how a number collision hides: the `
|
|
199
|
+
+ 'second file of a colliding pair is the one that never gets indexed, so the tree and the index quietly disagree.)');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return violations;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Rows should read in ascending number order. WARNING only, never a hard failure: the live
|
|
207
|
+
// index carries legacy rows an auto-merge placed out of order, and blocking on history
|
|
208
|
+
// would fail every PR for a mess nobody's diff created (the ADR 0189 split — declaration
|
|
209
|
+
// rot hard-fails, inherited drift warns).
|
|
210
|
+
function indexOrderWarnings(rows) {
|
|
211
|
+
const warnings = [];
|
|
212
|
+
let prev = null;
|
|
213
|
+
for (const r of rows) {
|
|
214
|
+
if (prev !== null && r.number < prev) {
|
|
215
|
+
warnings.push(`docs/adr/README.md:${r.lineNo}: row ${r.number} sits after ${prev} — the index reads in ascending order.`);
|
|
216
|
+
}
|
|
217
|
+
if (prev === null || r.number > prev) prev = r.number;
|
|
218
|
+
}
|
|
219
|
+
return warnings;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- the fitness check -----------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const ADR_DIR = path.join(CORE_ROOT, 'docs', 'adr');
|
|
225
|
+
|
|
226
|
+
function adrFilesIn(absDir) {
|
|
227
|
+
try { return fs.readdirSync(absDir).filter((f) => parseAdrName(f).ok).sort(); }
|
|
228
|
+
catch { return []; } // no docs/adr in a vanilla instance → nothing to check
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check 31 — two ADRs may not share a number, and the index must agree with the tree.
|
|
232
|
+
function checkAdrNumbering() {
|
|
233
|
+
const files = adrFilesIn(ADR_DIR);
|
|
234
|
+
const violations = [];
|
|
235
|
+
|
|
236
|
+
for (const c of numberCollisions(files)) {
|
|
237
|
+
violations.push(
|
|
238
|
+
`ADR number ${c.number} is claimed by ${c.files.length} files (${c.files.map((f) => `docs/adr/${f}`).join(', ')}) — `
|
|
239
|
+
+ 'an ADR number is a citation target, so a duplicate makes every citation to it ambiguous forever. Renumber the NEWER '
|
|
240
|
+
+ 'file to the next free number (node scripts/gds/next-number.js) and move its README row with it.');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let readme = null;
|
|
244
|
+
try { readme = fs.readFileSync(path.join(ADR_DIR, 'README.md'), 'utf8'); } catch { /* no index → nothing to reconcile */ }
|
|
245
|
+
const rows = readme === null ? [] : parseIndexRows(readme);
|
|
246
|
+
if (readme !== null) violations.push(...indexViolations({ basenames: files, rows }));
|
|
247
|
+
const warnings = readme === null ? [] : indexOrderWarnings(rows);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
name: 'ADR numbers unique + index agrees with the tree (task 1003164)',
|
|
251
|
+
ok: violations.length === 0,
|
|
252
|
+
hardFail: violations.length > 0,
|
|
253
|
+
violations,
|
|
254
|
+
warnings,
|
|
255
|
+
note: `${files.length} ADR file(s), ${rows.length} index row(s), `
|
|
256
|
+
+ `${Object.keys(LEGACY_DUPLICATE_ADRS).length} grandfathered duplicate number(s) (frozen by exact filename — the list may only shrink). `
|
|
257
|
+
+ `Next free number: ${String(nextAdrNumber(files)).padStart(4, '0')}.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
ADR_RE,
|
|
263
|
+
LEGACY_DUPLICATE_ADRS,
|
|
264
|
+
parseAdrName,
|
|
265
|
+
adrNumberOf,
|
|
266
|
+
isGrandfathered,
|
|
267
|
+
numberCollisions,
|
|
268
|
+
nextAdrNumber,
|
|
269
|
+
parseIndexRows,
|
|
270
|
+
indexViolations,
|
|
271
|
+
indexOrderWarnings,
|
|
272
|
+
checkAdrNumbering,
|
|
273
|
+
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1319,6 +1319,12 @@ const CHECKS = [
|
|
|
1319
1319
|
// doc-comment gate that blocks a ship buys filler prose, not understanding.
|
|
1320
1320
|
// Making it blocking is an owner decision and needs its own ADR.
|
|
1321
1321
|
require('./doc-comment-coverage.js').checkDocCommentCoverage,
|
|
1322
|
+
// Check 31 — task 1003164: two ADRs may not share a number, and docs/adr/README.md
|
|
1323
|
+
// must agree with the tree. The ADR half of the guard Check 11b already gives
|
|
1324
|
+
// migrations: same shared, monotonic, human-allocated number line, and until this
|
|
1325
|
+
// landed only one of the two was enforced. An ADR number is a citation target, so a
|
|
1326
|
+
// duplicate is wrong forever (ADR 0195).
|
|
1327
|
+
require('./adr-namespace.js').checkAdrNumbering,
|
|
1322
1328
|
];
|
|
1323
1329
|
|
|
1324
1330
|
function runAll() {
|
|
@@ -1364,6 +1370,7 @@ module.exports = {
|
|
|
1364
1370
|
checkKernelImportsNoDomain,
|
|
1365
1371
|
checkModuleMigrationsAdditive,
|
|
1366
1372
|
checkMigrationNamespacing, // Check 11b — ADR 0108 §5 cross-repo migration filenames
|
|
1373
|
+
checkAdrNumbering: require('./adr-namespace.js').checkAdrNumbering, // Check 31 — task 1003164 / ADR 0195: ADR numbers unique + index agrees with the tree
|
|
1367
1374
|
checkDesignContract,
|
|
1368
1375
|
checkKernelPromptsNoHardcodedIdentity, // BV1.R76 / task 1705
|
|
1369
1376
|
scanPromptIdentity, // BV1.R76 — negative-test hook
|
package/src/module-api.js
CHANGED
|
@@ -1206,8 +1206,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1206
1206
|
// landed since 1.19.381 with no explicit bump. run 33094545251. (task 1002620)
|
|
1207
1207
|
// 1.19.383 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1208
1208
|
// landed since 1.19.382 with no explicit bump. run 33101389971. (task 1002620)
|
|
1209
|
+
// 1.19.384 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1210
|
+
// landed since 1.19.383 with no explicit bump. run 33116184189. (task 1002620)
|
|
1209
1211
|
// ---------------------------------------------------------------------------
|
|
1210
|
-
const CORE_VERSION = '1.19.
|
|
1212
|
+
const CORE_VERSION = '1.19.384'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1211
1213
|
|
|
1212
1214
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1213
1215
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/fitness.mjs
CHANGED
|
@@ -354,6 +354,124 @@ test('migration-namespacing actually CATCHES a cross-source stem collision (the
|
|
|
354
354
|
assert.equal(collisions[0].stem, '188_x');
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
// --- Check 31: ADR numbers unique + index agrees with the tree (task 1003164 / ADR 0195) ---
|
|
358
|
+
//
|
|
359
|
+
// The ADR half of Check 11b's guard. The fixture is the REAL collision: on 2026-08-22 two
|
|
360
|
+
// branches each landed an ADR numbered 0181 under different slugs, so git saw no path
|
|
361
|
+
// collision, README.md auto-merged into two rows, and every gate passed.
|
|
362
|
+
|
|
363
|
+
const adrNs = require(path.join(ROOT, 'scripts', 'gds', 'adr-namespace.js'));
|
|
364
|
+
|
|
365
|
+
test('ADR numbering: the live tree is clean (all 20 known duplicates grandfathered)', () => {
|
|
366
|
+
const r = fitness.checkAdrNumbering();
|
|
367
|
+
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
368
|
+
assert.match(r.note, /ADR file\(s\)/, 'note reports the file + row counts');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('ADR numbering actually CATCHES two ADRs sharing a number (the 0181 pair, not a no-op gate)', () => {
|
|
372
|
+
const collisions = adrNs.numberCollisions([
|
|
373
|
+
'0180-dependency-intake-and-drift-policy.md',
|
|
374
|
+
'0181-ship-requires-an-assurance.md',
|
|
375
|
+
'0181-teardown-state-gate-and-address-retention.md',
|
|
376
|
+
'0182-something-else.md',
|
|
377
|
+
]);
|
|
378
|
+
assert.equal(collisions.length, 1, 'the shared number must be reported exactly once');
|
|
379
|
+
assert.equal(collisions[0].number, '0181');
|
|
380
|
+
assert.deepEqual(collisions[0].files, [
|
|
381
|
+
'0181-ship-requires-an-assurance.md',
|
|
382
|
+
'0181-teardown-state-gate-and-address-retention.md',
|
|
383
|
+
], 'BOTH paths are named — the report has to say which two files to look at');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('ADR numbering: a grandfathered pair passes, but a THIRD file on that number does not', () => {
|
|
387
|
+
const pair = adrNs.LEGACY_DUPLICATE_ADRS['0008'];
|
|
388
|
+
assert.equal(pair.length, 2, 'fixture: 0008 is a known legacy pair');
|
|
389
|
+
|
|
390
|
+
// the exact frozen pair is exempt
|
|
391
|
+
assert.deepEqual(adrNs.numberCollisions([...pair]), [], 'the frozen legacy pair must pass');
|
|
392
|
+
|
|
393
|
+
// a third file joining the number is NOT — the exemption is set-equality, not "number 0008 is free"
|
|
394
|
+
const third = adrNs.numberCollisions([...pair, '0008-a-brand-new-adr.md']);
|
|
395
|
+
assert.equal(third.length, 1, 'a third file on a grandfathered number must still be flagged');
|
|
396
|
+
assert.equal(third[0].files.length, 3);
|
|
397
|
+
|
|
398
|
+
// so is a RENAME of either half — schema-migrations-style, the frozen names are the exemption
|
|
399
|
+
const renamed = adrNs.numberCollisions([pair[0].replace('.md', '-v2.md'), pair[1]]);
|
|
400
|
+
assert.equal(renamed.length, 1, 'renaming half of a grandfathered pair un-grandfathers it');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('ADR numbering: the index must have one row per file, pointing at a file that exists', () => {
|
|
404
|
+
const files = ['0001-a.md', '0002-b.md'];
|
|
405
|
+
const row = (n, slug) => `| ${n} | [Title](${n}-${slug}.md) | topic |`;
|
|
406
|
+
|
|
407
|
+
// clean: one row per file, every row resolves
|
|
408
|
+
const clean = adrNs.indexViolations({
|
|
409
|
+
basenames: files,
|
|
410
|
+
rows: adrNs.parseIndexRows([row('0001', 'a'), row('0002', 'b')].join('\n')),
|
|
411
|
+
});
|
|
412
|
+
assert.deepEqual(clean, [], 'a matching index must pass: ' + clean.join('; '));
|
|
413
|
+
|
|
414
|
+
// DANGLING row — a row for a number with no file (the other half of the collision problem:
|
|
415
|
+
// the 2026-08-22 renumber had to reposition a row by hand)
|
|
416
|
+
const dangling = adrNs.indexViolations({
|
|
417
|
+
basenames: files,
|
|
418
|
+
rows: adrNs.parseIndexRows([row('0001', 'a'), row('0002', 'b'), row('0003', 'ghost')].join('\n')),
|
|
419
|
+
});
|
|
420
|
+
assert.equal(dangling.length, 1, 'exactly one violation for the dangling row');
|
|
421
|
+
assert.match(dangling[0], /does not exist/, 'the message names the missing file');
|
|
422
|
+
assert.match(dangling[0], /0003-ghost\.md/);
|
|
423
|
+
|
|
424
|
+
// DUPLICATED row — what an auto-merged index leaves behind (the live 0116/0117 case)
|
|
425
|
+
const dupe = adrNs.indexViolations({
|
|
426
|
+
basenames: files,
|
|
427
|
+
rows: adrNs.parseIndexRows([row('0001', 'a'), row('0002', 'b'), row('0002', 'b')].join('\n')),
|
|
428
|
+
});
|
|
429
|
+
assert.equal(dupe.length, 1);
|
|
430
|
+
assert.match(dupe[0], /has 2 index rows/, 'the message counts the rows and names their lines');
|
|
431
|
+
|
|
432
|
+
// UNINDEXED file — how a collision hides: the second file of a colliding pair is the one
|
|
433
|
+
// that never gets a row, so `ls docs/adr/` and the index quietly disagree
|
|
434
|
+
const missing = adrNs.indexViolations({
|
|
435
|
+
basenames: [...files, '0003-unindexed.md'],
|
|
436
|
+
rows: adrNs.parseIndexRows([row('0001', 'a'), row('0002', 'b')].join('\n')),
|
|
437
|
+
});
|
|
438
|
+
assert.equal(missing.length, 1);
|
|
439
|
+
assert.match(missing[0], /has no row in docs\/adr\/README\.md/);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('ADR numbering: a row is read by its OWN-number link, so citing other ADRs in the prose is fine', () => {
|
|
443
|
+
// Index rows routinely cite sibling ADRs inside the title cell. Only the link whose
|
|
444
|
+
// filename starts with the row's own number is the row's subject.
|
|
445
|
+
const rows = adrNs.parseIndexRows(
|
|
446
|
+
'| 0100 | [Two-repo model, superseding [0099](0099-old.md) and see [0101](0101-next.md)](0100-two-repo.md) | topic |',
|
|
447
|
+
);
|
|
448
|
+
assert.equal(rows.length, 1);
|
|
449
|
+
assert.equal(rows[0].target, '0100-two-repo.md', 'the own-number link is the row subject');
|
|
450
|
+
assert.deepEqual(
|
|
451
|
+
adrNs.indexViolations({ basenames: ['0100-two-repo.md'], rows }),
|
|
452
|
+
[], 'citing other ADRs in a row must not be read as extra rows',
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('ADR numbering: out-of-order rows WARN and never hard-fail (inherited drift, not a PR author\'s doing)', () => {
|
|
457
|
+
const row = (n) => `| ${n} | [T](${n}-s.md) | topic |`;
|
|
458
|
+
assert.deepEqual(adrNs.indexOrderWarnings(adrNs.parseIndexRows([row('0001'), row('0002')].join('\n'))), []);
|
|
459
|
+
|
|
460
|
+
const warns = adrNs.indexOrderWarnings(adrNs.parseIndexRows([row('0001'), row('0003'), row('0002')].join('\n')));
|
|
461
|
+
assert.equal(warns.length, 1, 'the backwards row is reported');
|
|
462
|
+
assert.match(warns[0], /ascending order/);
|
|
463
|
+
|
|
464
|
+
// and ordering contributes to `warnings`, never to `violations`
|
|
465
|
+
const live = fitness.checkAdrNumbering();
|
|
466
|
+
assert.equal(live.hardFail, false, 'the live index is out of order in places and must still pass');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test('ADR numbering: nextAdrNumber allocates above the highest number on disk', () => {
|
|
470
|
+
assert.equal(adrNs.nextAdrNumber(['0001-a.md', '0181-b.md', '0181-c.md']), 182, 'max + 1, duplicates included');
|
|
471
|
+
assert.equal(adrNs.nextAdrNumber([]), 1, 'an empty tree starts at 1');
|
|
472
|
+
assert.equal(adrNs.nextAdrNumber(['README.md', 'notes.txt']), 1, 'non-ADR files do not count');
|
|
473
|
+
});
|
|
474
|
+
|
|
357
475
|
// --- goal scope map imports PROTECTED_GLOBS (BV1.R54 / task 1508, ADR 0086 §7) ---
|
|
358
476
|
|
|
359
477
|
test('scope map import check: the live module-scope-map.js passes', () => {
|