@bongos/core 1.19.1032 → 1.19.1034

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.bongos-core.json +322 -117
  2. package/.claude/skills/builder-start/SKILL.md +1 -1
  3. package/.claude/skills/grade-audit/SKILL.md +1 -1
  4. package/.claude/skills/grade-recover/SKILL.md +2 -1
  5. package/.claude/skills/grader-health/SKILL.md +5 -1
  6. package/.claude/skills/merge-mode/SKILL.md +1 -1
  7. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
  8. package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
  9. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
  10. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  11. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
  12. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
  13. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
  14. package/docs/adr/README.md +4 -4
  15. package/docs/api/openapi.json +1 -1
  16. package/docs/architecture.md +4 -3
  17. package/docs/canonical-permissions.md +2 -2
  18. package/docs/copy-inventory.md +34 -30
  19. package/docs/copy-registry.json +69 -33
  20. package/docs/file-map.md +4 -3
  21. package/docs/module-api-changelog.md +4 -0
  22. package/docs/recipes/autobongos-windows-host.md +3 -1
  23. package/docs/recipes/local-dev.md +45 -3
  24. package/docs/recipes/packaging-the-core.md +3 -1
  25. package/docs/recipes/redteam-patrol.md +2 -2
  26. package/docs/recipes/upgrading-the-core.md +1 -1
  27. package/modules/autonomy/cadence.js +52 -0
  28. package/modules/autonomy/gauge.js +57 -9
  29. package/modules/government/protected-surfaces.json +1 -0
  30. package/modules/grading/grade-engine.js +267 -0
  31. package/modules/grading/grader-subagent.js +4 -3
  32. package/modules/grading/grader-workers/index.js +67 -11
  33. package/modules/grading/grader-workers/worker-verdict.js +32 -2
  34. package/modules/grading/grader.js +109 -88
  35. package/modules/hall-ui/public/deploy.js +35 -0
  36. package/modules/hall-ui/public/project-settings.css +7 -0
  37. package/modules/hall-ui/records/builders-roster.md +10 -0
  38. package/modules/hall-ui/records/catalog.md +10 -0
  39. package/modules/hall-ui/records/collaboration.md +14 -0
  40. package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
  41. package/modules/hall-ui/records/oversight-family.md +19 -0
  42. package/modules/hall-ui/records/panel-family.md +19 -0
  43. package/modules/hall-ui/records/reading-rooms.md +12 -0
  44. package/modules/hall-ui/records/record-family.md +13 -0
  45. package/modules/hall-ui/records/reference-room.md +9 -0
  46. package/modules/hall-ui/records/roster-and-ledger.md +9 -0
  47. package/modules/hall-ui/records/sky.md +23 -0
  48. package/modules/lifecycle/dependency-advisory.js +2 -1
  49. package/modules/lifecycle/routes/tasks.js +19 -4
  50. package/modules/lifecycle/ship-preflight.js +91 -0
  51. package/modules/provisioning/control-plane-guard.js +14 -13
  52. package/modules/provisioning/core-upgrade.js +30 -17
  53. package/modules/provisioning/public-refusal.js +44 -0
  54. package/modules/provisioning/routes/disconnect.js +2 -1
  55. package/modules/provisioning/routes/env-manifest.js +5 -4
  56. package/modules/provisioning/routes/provisioning.js +16 -15
  57. package/modules/provisioning/routes/repo-private.js +2 -1
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +7481 -0
  61. package/scripts/gds/address-park-sweep.sh +3 -1
  62. package/scripts/gds/autobongos-run.js +91 -3
  63. package/scripts/gds/backfill-cost.js +20 -9
  64. package/scripts/gds/baseline-staleness.js +1 -1
  65. package/scripts/gds/claim.js +2 -2
  66. package/scripts/gds/credential-presence.js +281 -0
  67. package/scripts/gds/db-backup-nightly.sh +9 -4
  68. package/scripts/gds/db-backup-offsite.sh +4 -2
  69. package/scripts/gds/deploy-main-audit-gate.sh +11 -4
  70. package/scripts/gds/dev-lib.js +212 -5
  71. package/scripts/gds/dev.js +102 -50
  72. package/scripts/gds/doctor.js +3 -2
  73. package/scripts/gds/grade-correlation-audit.js +280 -0
  74. package/scripts/gds/grade-replay.js +237 -0
  75. package/scripts/gds/hooks/pre-push.js +7 -3
  76. package/scripts/gds/main-audit.js +19 -10
  77. package/scripts/gds/memory-map.js +3 -1
  78. package/scripts/gds/onboarding-config.js +3 -2
  79. package/scripts/gds/package-core.js +72 -1
  80. package/scripts/gds/plain-cards.js +4 -0
  81. package/scripts/gds/preflight.js +12 -4
  82. package/scripts/gds/provision-core-upgrade.js +282 -80
  83. package/scripts/gds/provision-repo-auth.js +15 -0
  84. package/scripts/gds/provision.js +1 -1
  85. package/scripts/gds/publish-credential-check.js +3 -0
  86. package/scripts/gds/push-main.js +14 -3
  87. package/scripts/gds/push-path-brief.js +3 -2
  88. package/scripts/gds/redteam-patrol.js +4 -4
  89. package/scripts/gds/release-notes.js +271 -0
  90. package/scripts/gds/run-routine.js +18 -5
  91. package/scripts/gds/run-unit-tests.js +14 -3
  92. package/scripts/gds/seed-board-room-tasks.js +1 -1
  93. package/scripts/gds/session-cost.js +12 -3
  94. package/scripts/gds/ship-deploy-target.js +11 -6
  95. package/scripts/gds/ship-flow.js +5 -3
  96. package/scripts/gds/ship-grade-steps.js +33 -0
  97. package/scripts/gds/ship-land.js +241 -16
  98. package/scripts/gds/ship-merge.js +3 -2
  99. package/scripts/gds/ship-preflight-steps.js +86 -11
  100. package/scripts/gds/ship.js +1 -1
  101. package/scripts/gds/stray-unit-sweep.sh +3 -1
  102. package/scripts/gds/upgrade-migrate.js +67 -0
  103. package/scripts/gds/upgrade.js +9 -7
  104. package/scripts/hall-preview/README.md +20 -3
  105. package/scripts/hall-preview/live-identity.js +1 -1
  106. package/scripts/hall-preview/refresh-fixtures.js +71 -11
  107. package/scripts/hall-preview/scrub-identity.js +296 -0
  108. package/src/bongos/routes/security.js +3 -3
  109. package/src/build-info.js +4 -2
  110. package/src/module-api.js +1 -1
  111. package/tests/adr_0089_scope.mjs +70 -0
  112. package/tests/adr_0162_citation.mjs +184 -0
  113. package/tests/autobongos_cadence.mjs +74 -0
  114. package/tests/autobongos_loop.mjs +114 -0
  115. package/tests/autonomy_gauge.mjs +56 -0
  116. package/tests/consumer_layout_boot.mjs +363 -31
  117. package/tests/core_upgrade_door.mjs +40 -6
  118. package/tests/core_upgrade_runner.mjs +329 -5
  119. package/tests/credential_presence.mjs +153 -0
  120. package/tests/db_backup_nightly.mjs +21 -3
  121. package/tests/db_backup_offsite.mjs +81 -0
  122. package/tests/deploy_audit_gate.mjs +5 -2
  123. package/tests/deploy_page_projects.mjs +63 -2
  124. package/tests/dev_lib.mjs +2 -1
  125. package/tests/dev_windows.mjs +324 -0
  126. package/tests/env_knobs_neutral_prefix.mjs +203 -0
  127. package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
  128. package/tests/government_protected_surfaces.mjs +10 -0
  129. package/tests/grade_correlation_audit.mjs +159 -0
  130. package/tests/grade_engine.mjs +249 -0
  131. package/tests/grade_engine_replay.mjs +99 -0
  132. package/tests/grader_prompt_window_bound.mjs +132 -0
  133. package/tests/grader_unbacked_score.mjs +194 -0
  134. package/tests/hall_preview_scrub.mjs +214 -0
  135. package/tests/hall_records.mjs +85 -0
  136. package/tests/helpers.mjs +25 -0
  137. package/tests/instance_db_migrated_on_code_move.mjs +8 -1
  138. package/tests/integration_parked_ratchet.mjs +24 -1
  139. package/tests/plain_cards.mjs +1 -1
  140. package/tests/provisioning_public_refusal.mjs +195 -0
  141. package/tests/publish_status_branch.mjs +128 -0
  142. package/tests/redteam_patrol.mjs +1 -1
  143. package/tests/release_notes.mjs +327 -0
  144. package/tests/shell_stat_portability.mjs +49 -0
  145. package/tests/ship_land_conflict.mjs +181 -0
  146. package/tests/ship_land_red_pr.mjs +236 -0
  147. package/tests/ship_preflight.mjs +226 -3
  148. package/tests/start_env_guard.mjs +16 -0
  149. package/tests/sweep_app_account.mjs +15 -2
  150. package/tests/upgrade.mjs +20 -1
  151. package/tests/upgrade_migrate.mjs +195 -0
@@ -34,7 +34,7 @@ Delivery is now deterministic (task 1288). The **card-delivery hook** (`.claude/
34
34
 
35
35
  > **FIRST, read WHERE THIS INSTANCE'S BOARD LIVES — `config/branding.json` → `domains.buildersOrigin`.** Every route below branches on it, and getting it wrong is worse than an error. A **remote `https://` origin** means the board is **HOSTED**: it is a running website with the builder's real tasks in it. A **`localhost`** origin means the instance is genuinely local and has to be brought up. Prescribing the local cure on a hosted instance walks the builder through three obedient steps to a board that is not theirs.
36
36
 
37
- - **`bongos: command not found`, a "cannot find module" from `node scripts/gds/start.js`, or you can otherwise tell this is a fresh checkout** (a scaffolded instance whose `node_modules` is absent). The `bongos` CLI ships *inside* the not-yet-installed `@bongos/core` package, so nothing runs until deps are installed — this is the #1 fresh-clone wall. Note before prescribing anything: **`@bongos/core` is a PRIVATE package**, so a bare `npm install` **404s on any machine with no `NPM_TOKEN`**. That 404 is the expected tokenless outcome, **not** a yanked or missing version — never go hunting for "a version that still exists" to repoint the dependency at. Route by `buildersOrigin`:
37
+ - **`bongos: command not found`, a "cannot find module" from `node scripts/gds/start.js`, or you can otherwise tell this is a fresh checkout** (a scaffolded instance whose `node_modules` is absent). The `bongos` CLI ships *inside* the not-yet-installed `@bongos/core` package, so nothing runs until deps are installed — this is the #1 fresh-clone wall. Note before prescribing anything: **`@bongos/core` is a PUBLIC npm package** ([ADR 0283](../../../docs/adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md), since 2026-09-13), so a bare `npm install` needs **no `NPM_TOKEN`** and no npm login. A 404 on `@bongos/core` is therefore **not** a normal outcome: it means the dependency in `package.json` names something the registry does not have (a version that was never published, a mistyped name). A local `file:` tarball that is missing fails differently (ENOENT, not a 404). Read the exact spec, tell the builder what it points at, and do not quietly repoint it at a different version. Route by `buildersOrigin`:
38
38
  - **Hosted (remote origin)** — lead with the fact that **nothing needs installing**: the board is a website, so open `<buildersOrigin>/builders` to see and claim work right now. The CLI is an optional second client and needs a token. **Never route to `bongos dev` here** — it stands up a *local* instance with its *own empty database*, so it cannot show the hosted board and quietly answers a different question than the one asked.
39
39
  - **Local (`localhost` origin)** — the instance really is local: `npm install`, then `bongos dev` (one-command local bring-up), then re-run `/builder-start`.
40
40
  Either way, don't hand-run internal scripts; the package isn't unpacked yet.
@@ -75,7 +75,7 @@ A clean week is four one-liners.
75
75
 
76
76
  ## Constraints
77
77
 
78
- - **Never confirm, never flip, never re-grade (ADR 0162).** Verdicts are advisory and never self-executing here — this skill's entire output is a report plus queued `idea_inbox` rows. If a leg tempts you to "just fix it", the fix is a queued idea or a claimed task, not an action inside the audit.
78
+ - **Never confirm, never flip, never re-grade.** A verdict is advisory and never self-executing (ADR 0158 §2) — this skill's entire output is a report plus queued `idea_inbox` rows, and it adds no approval step to anyone's ship (ADR 0162, which retired review gates). If a leg tempts you to "just fix it", the fix is a queued idea or a claimed task, not an action inside the audit.
79
79
  - **Manual cadence.** Invoke by hand (weekly is the intended rhythm). Do not wire it to cron or depend on `.claude/scheduled-tasks/`.
80
80
  - **Idempotent re-runs.** The `[grade-audit] task <id>:` title marker + the inbox pre-check are what make running it twice harmless — keep both.
81
81
  - **Archon surfaces degrade gracefully.** Without `override_request.decide`, Leg 1 can only report "ledger unreadable at this rank" — say so rather than skipping silently.
@@ -38,9 +38,10 @@ The panel could not run (worker infra outage or a pre-spawn bypass). This is **n
38
38
 
39
39
  ### Branch B — fabricated / server-shape fail: the guard tripped or an axis was forged by an error
40
40
 
41
- Two known sub-shapes, both recognizable because the "fail" carries **no substantive findings**:
41
+ Three known sub-shapes, all recognizable because the "fail" carries **no substantive findings**. First make sure it really carries none: read the top-level `issues[]` — the API has lifted the stored findings there since task 1003488 (2026-09-02). Before that, a grade READ BACK showed no `issues` at all even when the panel had filed some, which is how task 1002721's real (and wrong) Quality blocker was taken for an empty fail.
42
42
 
43
43
  - **Server-guard trip** (`signals.server_guard.tripped` non-empty, e.g. `implausible_pass`): the server downgraded a client-asserted pass it found implausible — historically this guard has fired on genuinely clean, unanimous zero-issue passes. Explain the downgrade honestly, then re-grade: `node scripts/gds/ship.js <id> --regrade`.
44
+ - **An unbacked low score** (`signals.unbacked_scores` lists it; the card reads `UNBACKED LOW SCORE`): a reviewer scored below threshold without a major/blocker finding, so the severity gate downgraded it to manual-confirm instead of a hard fail (task 1002903). There is nothing to fix. Re-grade once; if it repeats on a diff you can defend, take the override path below with your evidence.
44
45
  - **A FAIL scored around 4.5/10 with zero issues and a null/zero axis** (the errored-worker class): a worker error was forwarded as a real axis score. Same response — re-grade; the panel rerun replaces the forged shape with a real verdict. Since task 1003318 a Quality worker that never ran (a `Prompt is too long` window overflow, `error: PROMPT_TOO_LONG`) no longer forges a 0: the server records `signals.cq_lens_unavailable` with a json-null axis and the card reads `QUALITY LENS UNAVAILABLE` — re-grade or manual-confirm; a 4.5 with a 0 axis now only comes from a server predating it.
45
46
 
46
47
  If an honest re-grade re-trips the guard on a diff you can defend, use the **auditable override path** — file an override-request with a real rationale (≥20 chars; only the task's most-recent claim holder or an Archon may file; an Archon then decides it):
@@ -54,13 +54,17 @@ The evaluation found every grade ran on a generation-stale pin. Read the live pi
54
54
 
55
55
  With `--probe`, run the live panel calibration: `node scripts/gds/grade-smoke.js`. It grades known-good and known-bad fixtures against the real panel and reports miscalibration. **Costs ~$0.30–0.90 per run; Metic+ only.** Never run it implicitly — only when the user passed `--probe` or asked for a live calibration.
56
56
 
57
+ ## Related: do the workers fail differently?
58
+
59
+ This skill checks whether the grader is RUNNING well. Whether its four workers carry independent signal — or are four copies of one opinion — is a different question with its own read-only report: `node scripts/gds/grade-correlation-audit.js` (pairwise agreement + Cohen's kappa between workers, each worker's kappa against the owner's override and manual-confirm decisions, refused below a minimum sample; no model calls, Metic+ for the override read). Point the owner at it when the question is panel composition rather than health.
60
+
57
61
  ## Report shape
58
62
 
59
63
  End with a compact findings list, most severe first: axis, evidence (the numbers you read), verdict (outage / drift / anomaly / stale-pin / healthy), and the exact remediation command or the skill to hand off to (`/grade-recover` for a single stuck task; a Bongos task via `capture.js` for systemic fixes). A clean bill of health is one line per axis.
60
64
 
61
65
  ## Constraints
62
66
 
63
- - **Read-only, describe-only (ADR 0162).** No confirms, no regrades run by this skill, no status flips, no new gates. The remediation lines are FOR the builder to run.
67
+ - **Read-only, describe-only (ADR 0158 §2 — a verdict never executes itself).** No confirms, no regrades run by this skill, no status flips, and no new approval step on anyone's ship (ADR 0162). The remediation lines are FOR the builder to run.
64
68
  - **Confirm before alarming.** Axis 1's outage-vs-rejection check comes before any "the grader is down" conclusion — a real FAIL streak on one surface is signal, not outage.
65
69
  - **`--probe` spends money** — explicit opt-in only, Metic+.
66
70
 
@@ -129,7 +129,7 @@ node scripts/gds/push-main.js # SEC #863: the sanctioned gated main-push (NOT
129
129
  ssh -o ConnectTimeout=10 lars@REDACTED_IP "~/deploy.sh"
130
130
  ```
131
131
 
132
- > **SEC #863 (ADR 0043):** push `main` only via `node scripts/gds/push-main.js`. A raw `git push origin main` is now blocked by the pre-push permission gate — that block is the friction that stops an ad-hoc bypass like the Builder 25 incident. `push-main.js` carries the `OTB_ALLOW_MAIN_PUSH` override because /merge-mode only ever lands tasks that already passed the grader + the Archon confirm gate. The post-push main audit (`node scripts/gds/main-audit.js`) re-checks regardless.
132
+ > **SEC #863 (ADR 0043):** push `main` only via `node scripts/gds/push-main.js`. A raw `git push origin main` is now blocked by the pre-push permission gate — that block is the friction that stops an ad-hoc bypass like the Builder 25 incident. `push-main.js` carries the `<PREFIX>_ALLOW_MAIN_PUSH` override (`BONGOS_` on a vanilla instance) because /merge-mode only ever lands tasks that already passed the grader + the Archon confirm gate. The post-push main audit (`node scripts/gds/main-audit.js`) re-checks regardless.
133
133
 
134
134
  The deploy script applies any new migrations, restarts the service, and curls /healthz.
135
135
 
@@ -114,6 +114,19 @@ The two deterministic server-side guards — route-rank ([ADR 0016](0016-trust-b
114
114
 
115
115
  The worker-contract (`buildPrompt` / `extractJson` / `computeVerdict`), the `runWorker` parallel runner, the `task_grades` + `grade_attempts` schema, the trivial-diff fast-path, and the server-side guard envelope are **kept**. This ADR generalizes their *inputs* (which model, which rubric, which evidence) — it does not throw them away. The content-addressed LLM cache ([ADR 0077](0077-content-addressed-llm-cache.md)) applies unchanged and now memoizes open-model calls too.
116
116
 
117
+ ### 8. Scope: what the engine does NOT own
118
+
119
+ Four other surfaces spend model tokens reading the repository, or stand guard over the build pipeline, so anyone planning the unification can mistake them for graders. They are deliberately **outside** the engine. None of them registers as a `grader.workType.<key>` provider, and none is migrated onto the engine. Each shares at most plumbing, and none shares the grade path, the verdict or the rubric. Verified against `main` on 2026-09-26 (task 1002844):
120
+
121
+ | Surface | What it is | What it shares with the grader | The one contract it preserves |
122
+ |---|---|---|---|
123
+ | **BFG session evaluator**: `scripts/gds/session-eval.js` ([ADR 0027](0027-bfg-session-inefficiency-evaluator.md)) | Scores how a builder *session* worked (ceremony vs work, footguns) from its uploaded digest | Nothing. It imports no grading code. | It judges the **session**, never the ship. It cannot change a grade or move a task's status. |
124
+ | **gate-review**: `scripts/gds/gate-review.js` | The gatekeeper ([ADR 0058](0058-automatic-gate-surface-gatekeeper.md)) that escalates a change **to the gate itself** before it lands | No grader call. It lists `modules/grading/grader.js`, `grader-workers/` and `grader-rubric.json` among its hard-floor paths, and reads the protected set from `src/bongos/permission-path-check.js`. | A change to the grader is reviewed by something **outside** the grader. It must never depend on the code it reviews. |
125
+ | **main-audit**: `scripts/gds/main-audit.js` ([ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md)) | The post-push audit of `main` (layer 3): real git history against real builder ranks | The rank/permission classifier `src/bongos/permission-path-check.js`. This is the same kernel checker the grader's permission-path pre-pass reaches through the module doorway. It is not grading code. | It runs **after the fact, whether or not a grade ran**, so it catches what bypassed the grader (a raw push, `--no-verify`). It must never assume the grade happened. |
126
+ | **Architect audit**: `scripts/gds/architect-audit.js` | First-principles "what to delete" audits that feed ideas into scope | Three subprocess/parse helpers from `modules/grading/grader.js`: `runSubagentCached`, `extractAssistantResult` and `extractJson`. This is plumbing: spawn a model, read its JSON. | It produces **ideas for a human to triage**, never a verdict on a ship. If the engine ever moves those helpers, the Architect needs the same three functions, not a work-type. |
127
+
128
+ **How to apply it.** An LLM call that decides whether a ship lands belongs in the engine. Everything else stays out, and a new exception is claimed by adding a row here. The one coupling to watch when the engine grows is the Architect's three plumbing helpers: keep them exported, or move the Architect's import in the same change.
129
+
117
130
  ---
118
131
 
119
132
  ## Consequences
@@ -172,6 +185,8 @@ A build cluster under GDS-V4 (or its successor), staged so nothing destabilizes
172
185
 
173
186
  Roll out behind a flag (the [ADR 0079](0079-value-gated-autonomous-engagement.md) "build disabled, arm later" pattern); the deterministic security gate is never flagged off.
174
187
 
188
+ **Progress (2026-09-25).** Step 3's engine and registry landed first, ahead of steps 1–2, because both build on it: `modules/grading/grade-engine.js` names the pipeline (`select → pre_pass → evidence → fan_out → aggregate → gate → persist`), owns both deterministic pre-passes and a deterministic veto no work-type's aggregate can override, and resolves work-types from its own registry or a module's `grader.workType.<key>` seam provider. The code panel is registered as work-type `code`, the default. Proven a no-op: a before/after run of 27 grading scenarios came out byte-identical, and a replay of 376 recorded production grades reproduced every stored decision except 22 from June 2026, which an older grader produced before two deliberate gate changes (the task 943 "grader unavailable" outcome and the task 574 missing-security-lens block). Task 1002839; the replay tool is `scripts/gds/grade-replay.js`.
189
+
175
190
  ---
176
191
 
177
192
  ## Sources
@@ -3,7 +3,7 @@
3
3
  - **Status:** Accepted
4
4
  - **Date:** 2026-08-17
5
5
  - **Task:** [#1003014](https://cloudbongos.com/builders#/task/1003014) (R01 of goal 1000069 — the Board Room)
6
- - **Related:** ADR 0151 (permissions-as-atoms), 0157 (Archon is rank and identity only), 0162 (verdicts advisory, never self-executing), 0172-per-craft (the ideator credit lane — stream 3), 0174 (government, not governance; "role" means a craft), 0037 (Discord channel layout), 0016 (trust boundary), 0086 (goals + scope walls)
6
+ - **Related:** ADR 0151 (permissions-as-atoms), 0157 (Archon is rank and identity only), 0162 (review gates retired — no human approval in the land path), 0158 §2 (a verdict is advisory, never self-executing), 0172-per-craft (the ideator credit lane — stream 3), 0174 (government, not governance; "role" means a craft), 0037 (Discord channel layout), 0016 (trust boundary), 0086 (goals + scope walls)
7
7
  - **Spec:** [`docs/specs/bongos-v1-board-room.md`](../specs/bongos-v1-board-room.md) — the planning session that produced this decision, with the full interview record
8
8
  - **Source ideas:** 1000738 (the Board Room), 1000754 (scoping brief)
9
9
 
@@ -24,7 +24,8 @@ floor would only point the gate downward again.
24
24
 
25
25
  Three constraints shaped what could actually be built.
26
26
 
27
- **1. The ADR 0162 discipline.** Verdicts are advisory and never self-executing. A gate that
27
+ **1. The ADR 0162 discipline.** No human approval sits in the land path (ADR 0162), and a
28
+ verdict is advisory and never self-executing (ADR 0158 §2). A gate that
28
29
  blocks a seeding until a quorum is reached would violate that; a gate that merely records an
29
30
  opinion would be theatre. The resolution turned out not to be a compromise between them —
30
31
  see Decision 1.
@@ -4,7 +4,7 @@
4
4
  - **Date:** 2026-08-19
5
5
  - **Tasks:** [#1003113](https://cloudbongos.com/builders#/task/1003113) (R02 of goal 1000074, the artist's loop) — builds on [#1003112](https://cloudbongos.com/builders#/task/1003112) (R01, the copy registry)
6
6
  - **Extends:** [ADR 0081](0081-tool-agnostic-design-layer.md) — the repo is the source of truth for design, with swappable tool adapters. This is the same claim for TEXT.
7
- - **Related:** ADR 0083 (module system, module-owned migrations), 0086 (goals + scope walls), 0016 (authority resolved server-side), 0162 (grader verdicts are advisory, never self-executing), 0175 (an objection requires a reason)
7
+ - **Related:** ADR 0083 (module system, module-owned migrations), 0086 (goals + scope walls), 0016 (authority resolved server-side), 0158 §2 (a verdict is advisory, never self-executing), 0162 (review gates retired — no human approval in the land path), 0175 (an objection requires a reason)
8
8
 
9
9
  ## Context
10
10
 
@@ -100,8 +100,8 @@ across an **edit**. So a flag routinely outlives the sentence it names, and `ann
100
100
  `target_state: 'gone'`.
101
101
 
102
102
  That is a **hint for a person**, not a verdict. Auto-closing on it would be the system deciding a human
103
- judgement is finished because a hash moved — the same family of error ADR 0162 rules out for grader
104
- verdicts. Every flag also stores a snapshot (text, file, line at flag time), taken server-side from the
103
+ judgement is finished because a hash moved — the same family of error ADR 0158 §2 rules out for audit
104
+ verdicts (a finding is addressed to a human; it is never itself the action). Every flag also stores a snapshot (text, file, line at flag time), taken server-side from the
105
105
  registry and never from the request body: a caller-supplied snapshot is a caller-supplied claim, and
106
106
  the snapshot's entire value is that it can be trusted later.
107
107
 
@@ -4,7 +4,7 @@
4
4
  - **Date:** 2026-08-24
5
5
  - **Tasks:** [#1003209](https://cloudbongos.com/builders#/task/1003209) (goal 1000079 — pay down the frozen debt and make enforcement durable; criterion C4 `reduction-durable`)
6
6
  - **Builds on:** [ADR 0078](0078-deterministic-first-cron.md) (deterministic-first routines + the `run-routine.js` dispatcher), [ADR 0180](0180-dependency-intake-and-drift-policy.md) §3 (the patrol's dependency checks), [ADR 0022](0022-secrets-policy.md) (the prevent → gate → patrol layering)
7
- - **Related:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (patrol findings are advisory — verdicts never self-execute), [ADR 0093](0093-tranche-2-core-carve-sequence.md) §1 (where `routine-timers.js` landed — the in-process poller seam this deliberately does *not* use)
7
+ - **Related:** [ADR 0158](0158-choke-point-verdict-taxonomy.md) §2 (patrol findings are advisory — a verdict never self-executes), [ADR 0093](0093-tranche-2-core-carve-sequence.md) §1 (where `routine-timers.js` landed — the in-process poller seam this deliberately does *not* use)
8
8
 
9
9
  ## 1. Context
10
10
 
@@ -104,7 +104,7 @@ findings were ever shown. `--file` now preflights the session and follows the
104
104
  green-and-vacuous.
105
105
 
106
106
  The exit code is a small table (`disposition()`), and the choice inside it is
107
- deliberate. Findings are **advisory** (ADR 0162) — they are filed for
107
+ deliberate. Findings are **advisory** (ADR 0158 §2) — they are filed for
108
108
  adjudication, not acted on — so a run that finds three things and files them is
109
109
  a **success**:
110
110
 
@@ -4,7 +4,7 @@
4
4
  - **Date:** 2026-08-24
5
5
  - **Tasks:** [#1003210](https://cloudbongos.com/builders#/task/1003210) (goal 1000079 — criterion C4 `reduction-durable`)
6
6
  - **Builds on:** [ADR 0180](0180-dependency-intake-and-drift-policy.md) (shrink-only baselines), [ADR 0188](0188-patrol-runs-on-ci-not-the-server.md) (the weekly patrol that now files this)
7
- - **Related:** [ADR 0091](0091-bounding-the-kernel-and-db-carve.md) §2 (the boundary-forced duplication that gets the first declared floor), [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (findings are advisory)
7
+ - **Related:** [ADR 0091](0091-bounding-the-kernel-and-db-carve.md) §2 (the boundary-forced duplication that gets the first declared floor), [ADR 0158](0158-choke-point-verdict-taxonomy.md) §2 (findings are advisory)
8
8
 
9
9
  ## 1. Context
10
10
 
@@ -5,7 +5,7 @@
5
5
  - **Tasks:** [#1003114](https://cloudbongos.com/builders#/task/1003114) (R03 of goal 1000074, the artist's loop) — builds on [#1003112](https://cloudbongos.com/builders#/task/1003112) (R01, the registry) and [#1003113](https://cloudbongos.com/builders#/task/1003113) (R02, the flag and the queue)
6
6
  - **Extends:** [ADR 0081](0081-tool-agnostic-design-layer.md) — the repo is the source of truth, with swappable tool adapters that land their output as a committed diff. This applies that contract to TEXT.
7
7
  - **Extends:** [ADR 0178](0178-the-copy-desk-flag-and-queue.md) — which pinned that `copy_desk_flags` may never hold replacement text, and named this task as the one that would have to keep that true while adding an editing affordance.
8
- - **Related:** 0083 (module system, ports), 0086 (goals), 0016 (authority resolved server-side), 0162 (a verdict is advisory, never self-executing), 0197 (the ui-design module)
8
+ - **Related:** 0083 (module system, ports), 0086 (goals), 0016 (authority resolved server-side), 0158 §2 (a verdict is advisory, never self-executing), 0162 (review gates retired — no human approval in the land path), 0197 (the ui-design module)
9
9
 
10
10
  ## Context
11
11
 
@@ -3,7 +3,8 @@
3
3
  - **Status:** Accepted
4
4
  - **Date:** 2026-09-03
5
5
  - **Tasks:** [#1003116](https://cloudbongos.com/builders#/task/1003116) (R05 of goal 1000074, the artist's loop) — the decision task the goal placed BEFORE [#1003115](https://cloudbongos.com/builders#/task/1003115) (R04, the review task), so that no code is wired to a gate whose authority is unsettled
6
- - **Reconciles:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) — review gates retired on `main`; a verdict is advisory and never self-executing
6
+ - **Reconciles:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) — review gates retired on `main`, so no human approval sits in the land path
7
+ - **Rule relied on:** [ADR 0158](0158-choke-point-verdict-taxonomy.md) §2 — a verdict is advisory and never self-executing
7
8
  - **Related:** 0178 (the copy desk flag + queue, which deliberately blocked nothing and named this decision as the one that would settle it), 0233 (a copy proposal is a task carrying a patch), 0016 (authority is resolved server-side, never from a local file), 0120 (pay on land), 0137 (land → release → deploy are three separate gates)
8
9
 
9
10
  ## Context
@@ -26,6 +27,11 @@ This matters, because the shorthand it is usually cited by — "a verdict is adv
26
27
  self-executing" — is a paraphrase, and reading the paraphrase instead of the ADR would rule out
27
28
  things 0162 explicitly keeps.
28
29
 
30
+ *Citation note (task 1003573):* the shorthand is not 0162's wording at all. It is, nearly
31
+ verbatim, the heading of [ADR 0158](0158-choke-point-verdict-taxonomy.md) §2 — a rule about what
32
+ an audit finding authorizes (nothing, until a human acts on it) — and 0162's text uses neither
33
+ "advisory" nor "self-executing". Cite 0158 §2 for that rule, and 0162 for what it retired.
34
+
29
35
  0162's own table of what still guards the safety machinery lists four controls and marks three of
30
36
  them **fails closed** — including the subagent grader panel, which runs on every GDS ship and parks
31
37
  a failing task at `completed`. So 0162 plainly permits an automatic gate that stops work. What it
@@ -59,7 +65,7 @@ never moves anything by itself: an artist saying the copy is bad does not stop a
59
65
  artist saying it is fine does not release one. What the system reads is the review task's *status*,
60
66
  which is ordinary GDS state that any rank can see and that moves by the ordinary means — the task
61
67
  is resolved, dismissed, or shipped. The artist's judgement is what they write **into** that task;
62
- it stays advisory in precisely 0162's sense.
68
+ it stays advisory in precisely ADR 0158 §2's sense.
63
69
 
64
70
  This is not a technicality to route around the rule. It is what makes the gate rank-consistent and
65
71
  machine-checkable rather than identity-pinned: a status is a fact, a judgement is a person.
@@ -164,7 +170,9 @@ they simply stop holding the pin.
164
170
  - **The paraphrase is corrected.** Several surfaces cite 0162 as "verdicts are advisory, never
165
171
  self-executing" (this project's skills and ADRs 0178 and 0233 among them). That shorthand is
166
172
  true of *verdicts* and false as a blanket claim about gates — 0162's own table has three
167
- controls that fail closed. Cite the test in §1 above rather than the slogan.
173
+ controls that fail closed. Cite the test in §1 above rather than the slogan. (Task 1003573
174
+ corrected those citations: the slogan is ADR 0158 §2's rule about verdicts, and a surface that
175
+ means 0162 now says what 0162 retired.)
168
176
  - **Accepted trade:** an artist who never does the work can delay a project's deploys. That is the
169
177
  point of `strict`, it is per project, it is owner-releasable, and the owner chose it as the
170
178
  default with that consequence stated.
@@ -160,7 +160,7 @@ This keeps the decision history honest and traceable.
160
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.](0087-bongos-app-architecture-and-handoff-contract.md) | Bongos app / feedback loop |
161
161
  | 0087-b | [Compete with Cursor on governance, not tooling](0087-compete-on-governance-not-tooling-cursor.md) | strategy / positioning |
162
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.](0088-consistent-character-generative-animation.md) | GDS / art pipeline |
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).](0089-modular-multi-model-adversarial-grader.md) | GDS / grader + quality gate |
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 in progress: the central engine + work-type registry landed first (task 1002839, `modules/grading/grade-engine.js`, the code panel registered as work-type `code`). Deliberately OUTSIDE the engine (§8): the BFG session evaluator, gate-review, main-audit and the Architect audit — none grades a ship; the Architect shares only three subprocess/parse helpers, and main-audit and gate-review only the rank-permission checker.](0089-modular-multi-model-adversarial-grader.md) | GDS / grader + quality gate |
164
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.](0090-metic-task-authoring.md) | GDS / permissions |
165
165
  | 0091 | [Bounding the kernel and carving `db.js` per-domain (Config-B core decomposition) — amends ADR 0083 to extend the strangler from the four FEATURE modules into the CORE itself (BONGOS-V1 C7). Answers the three questions the audit opened: (1) WHAT IS THE KERNEL — replaces the 6-file machinery-only `fitness.js` `KERNEL_FILES` with an enumerated ~3.5k-LOC trust roster (auth · route-rank-check · permission-path-check · secret-box · pool · the new `db-kernel.js` identity/audit/rank/`withTx` slice · instance-config/branding/build-info · routes/_helpers · the module-system machinery + module-scope-map); everything else in `src/bongos/` is a domain destined to become a module or to stay inside the un-split lifecycle state machine. (2) HOW A DOMAIN OWNS ITS SQL — ratifies the proven `modules/discord/db.js` + `modules/game/db.js` pattern (own queries lifted verbatim + deleted from core, reached through the doorway `pool`/`withTx`; kernel helpers re-exported behind the same `db.*` namespace; boundary-forced twins allowed + documented) as THE per-domain carve mechanism; `db.js` shrinks toward a kernel slice + the lifecycle residue. (3) WHAT THE DOORWAY EXPOSES — classifies `module-api.js` re-exports into kernel (stays: identity/audit/rank/auth/seams/config + `withTx`), domain-leak-with-a-home (relocate now: `setBuilderBoxBlocked`→dev-box, gemini-key trio→art-pipeline), and domain-leak-pending-carve (`createTask`/`captureIdea`/`VOTABLE_TASK_STATUSES` stay as MARKED temporary re-exports until lifecycle/ideas are carved in tranche 2 — named, owned, tracked debt, not pretend-kernel). A lifecycle-invoked capability resolves through a kernel PORT, never a direct import (R73 replaces `routes/tasks.js`→`grader.js` with `resolve('grade')`). Deliverable = this decision + the seeded, dependency-wired R68–R74 sub-task shape; OTB byte-identical at every step. Tranche 2 (the other ~8 core domains) deferred per owner.](0091-bounding-the-kernel-and-db-carve.md) | GDS / architecture |
166
166
  | 0092 | [Hosting module + on-prem (warehouse) hosting target — scopes a portable Cloud Bongos `hosting` module (default-OFF, fail-to-vanilla) that brokers compute + storage workloads (inference / imagegen / embeddings / training / storage) to configured hosting *targets* through kernel seams, resolving each via `resolveOptional(port, cloudDefaultFn)` so a box-less / module-off instance degrades to cloud automatically. The warehouse GPU box is OTB's first ON-PREM target — one adapter backend declared in HOST-instance config (the ADR 0062 / 0081 core↔host + swappable-adapter pattern), shared across all Example instances, reached over an outbound tunnel the box initiates (no open ports / no static IP), sitting OUTSIDE the trust boundary like Discord (ADR 0033): it computes, it never grants rank or lands code (ADR 0016 / 0043). Inference REUSES the ADR 0089 `ModelProvider` openai-compatible adapter (the on-prem box = one more vLLM / Ollama endpoint) and adopts 0089's decisive finding that low-volume work is cheaper on pay-per-call APIs — so the GPU's honest justification narrows to GPU-bound batch generation, open-model fine-tuning / LoRA (the ADR 0088 deferred level-2), and always-busy bulk, plus the non-GPU wins of backups / storage + an unmetered sandbox; code that ships stays on Claude. Owner decisions 2026-06-27: serve all instances, conservative posture, proceed. Phased: scope use ✅ → build (task 1619) → hardware (task 1620) → software; the cost read-out (task 1621) is an independent input.](0092-hosting-module-and-on-prem-target.md) | Cloud Bongos / hosting |
@@ -261,7 +261,7 @@ This keeps the decision history honest and traceable.
261
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.](0175-the-board-room-full-idea-ratification.md) | government / ideas / governance |
262
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.](0176-private-repo-deploy-keys.md) | provisioning / security |
263
263
  | 0177 | [**A goal membership is a `membership_kind`, and the rename ships expand-then-contract** ([task 1003013](https://cloudbongos.com/builders#/task/1003013), goal 1000068). ADR 0174 reserved `role` for the three crafts and enforced it on the GOVERNMENT surface only — by design, since `builders.system_role` and Discord's own noun are legitimate. That left a **fourth** use uncatalogued: `goal_members.role` (`'lead' | 'member'`), the MANAGER predicate behind goal authorization. Renamed to **`membership_kind`** — values unchanged, because the noun was the problem; `rank` is wrong (not a ladder position) and `position` is worse (ADR 0174 uses it as the prose word for a rank, recreating the "rank-role" qualifier collision). A boolean `is_lead` was seriously considered and rejected: the text rename keeps the copied CHECK and stays a reversible change on a live authz column, where `ADD boolean; UPDATE; DROP text` is data-destructive. The migration is **expand-then-contract, never `RENAME COLUMN`** — auto-rollback reverts the code pin and NOT the DB, so `core_220` ADDs the column and keeps the pair in lockstep with a BEFORE trigger (direction decided by which side changed), and a later task DROPs the old column with the `-- forward-unsafe` pragma, a backup, and its own window; no `SET NOT NULL` in step 1, because a static NOT NULL on a column the previous release never heard of is what the forward-safety gate rightly flags. The API field renames with it and carries **no `role` alias** (ADR 0174's Lineage lesson: a time-boxed shim outlives its release), pinned by the test of its absence. The reservation gains a **second narrow wall** rather than a widened first one — line-scoped inside the 5k-line `db.js` to the lines that name `goal_members`, so no allowlist is needed. A **fifth** use (`project_memberships.role`) is catalogued and filed, with this as its pattern.](0177-goal-membership-kind.md) | naming / lifecycle |
264
- | 0178 | [**The copy desk: a flag carries a judgement, and no column here could ever hold copy** ([task 1003113](https://cloudbongos.com/builders#/task/1003113), R02 of goal 1000074). [ADR 0081](0081-tool-agnostic-design-layer.md) made the repo the source of truth for design TOKENS; user-facing TEXT had no equivalent. R01 built the inventory (`copy-inventory.js` → `docs/copy-registry.json`), which answers *what does the product say* but cannot answer *what is wrong with it* — a judgement a person must record. This is that record plus the artist's queue. A new default-on module keyed **`copy-desk`, not `copy`**, for a concrete reason: the branding contract already owns a `copy` block, so the bare word reds the `core carries no module code` fitness check on the English word in `src/branding.js` — and the editorial "copy desk" is the more accurate name anyway. **The reason is required in the route AND in a CHECK constraint**: the route gives a person a usable error, the constraint means no future write path can create the reasonless flag the queue exists to not contain (a pointer with no judgement is *worse* than no flag — it still costs the artist a look). Closing requires a note by the same symmetry, since an unexplained close is indistinguishable from clearing a queue to move a number. The load-bearing decision is **NO LIVE CMS enforced by the schema's SHAPE**: `copy_desk_flags` has no column that could hold replacement text and must never gain one, because that non-goal does not erode via an "add a CMS" commit but via one plausible column, then one plausible read of it — `copy_no_cms.mjs` pins the write surface at exactly two verbs so R03's proposal path changes it in a reviewable diff. Rank is asymmetric: **filing is open to every rank** (the engineer who wrote the toast and the newcomer who could not read the button are the likeliest noticers, and neither is the artist — the reason floor is the quality gate, rank is not), closing is Metic+ **or the author** (withdrawing your own flag is a correction, not triage); nothing gates on craft, since an artist is a craft and not a rank. Duplicates are the only automatic signal (the one of stale/duplicated/off-voice needing no judgement layer) and the count **subtracts settled work**, or it never moves and teaches that the queue is decorative. A vanished target is **reported, never auto-closed** — the id is a hash over the text, so an edit routinely orphans a flag, and closing a human judgement because a hash moved is the family of error ADR 0162 rules out. Deliberately reports **no `generated_at`**: R01's contract is determinism, so a clock is the one field it cannot carry.](0178-the-copy-desk-flag-and-queue.md) | design / artist loop |
264
+ | 0178 | [**The copy desk: a flag carries a judgement, and no column here could ever hold copy** ([task 1003113](https://cloudbongos.com/builders#/task/1003113), R02 of goal 1000074). [ADR 0081](0081-tool-agnostic-design-layer.md) made the repo the source of truth for design TOKENS; user-facing TEXT had no equivalent. R01 built the inventory (`copy-inventory.js` → `docs/copy-registry.json`), which answers *what does the product say* but cannot answer *what is wrong with it* — a judgement a person must record. This is that record plus the artist's queue. A new default-on module keyed **`copy-desk`, not `copy`**, for a concrete reason: the branding contract already owns a `copy` block, so the bare word reds the `core carries no module code` fitness check on the English word in `src/branding.js` — and the editorial "copy desk" is the more accurate name anyway. **The reason is required in the route AND in a CHECK constraint**: the route gives a person a usable error, the constraint means no future write path can create the reasonless flag the queue exists to not contain (a pointer with no judgement is *worse* than no flag — it still costs the artist a look). Closing requires a note by the same symmetry, since an unexplained close is indistinguishable from clearing a queue to move a number. The load-bearing decision is **NO LIVE CMS enforced by the schema's SHAPE**: `copy_desk_flags` has no column that could hold replacement text and must never gain one, because that non-goal does not erode via an "add a CMS" commit but via one plausible column, then one plausible read of it — `copy_no_cms.mjs` pins the write surface at exactly two verbs so R03's proposal path changes it in a reviewable diff. Rank is asymmetric: **filing is open to every rank** (the engineer who wrote the toast and the newcomer who could not read the button are the likeliest noticers, and neither is the artist — the reason floor is the quality gate, rank is not), closing is Metic+ **or the author** (withdrawing your own flag is a correction, not triage); nothing gates on craft, since an artist is a craft and not a rank. Duplicates are the only automatic signal (the one of stale/duplicated/off-voice needing no judgement layer) and the count **subtracts settled work**, or it never moves and teaches that the queue is decorative. A vanished target is **reported, never auto-closed** — the id is a hash over the text, so an edit routinely orphans a flag, and closing a human judgement because a hash moved is the family of error ADR 0158 §2 rules out. Deliberately reports **no `generated_at`**: R01's contract is determinism, so a clock is the one field it cannot carry.](0178-the-copy-desk-flag-and-queue.md) | design / artist loop |
265
265
  | 0179 | [**`rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic** ([task 1003094](https://cloudbongos.com/builders#/task/1003094), goal 1000069). [ADR 0175](0175-the-board-room-full-idea-ratification.md) gave the constitution ONE membership form, `rank:<key>`, resolved by an EXACT SQL match — so `rank:metic` seated metics ONLY (excluding Archons), `rank:xenos` seated NEWCOMERS ONLY rather than "everyone", and "Metic and above" was inexpressible. Everywhere else a rank is a LADDER and authority is cumulative (ADR 0034 inheritance, requireRank('metic','archon'), the floor-derived seed); board membership was the one place it meant "exactly", and nothing said so. Rehearsing R25 on a real Postgres, an Archon ratified `rank:metic` under monarchy — the apply worked perfectly — and the next vote came back 403 not_board_member: **the owner flipped the switch and fell off their own board**, with the board deadlocked at one member because an Archon can still PROPOSE but no longer VOTE. Owner decision: EXPLICIT GRAMMAR. `rank:metic+` (that rank and above), `rank:metic,archon` (union), and **a bare key still means EXACTLY that key** — rejecting the smaller diff (silently redefining the bare form) precisely because it would have changed the meaning of every already-ratified constitution, the monarchy default included, on upgrade: nothing in a government should change meaning without a vote. `+` is defined only on the STANDARD ladder — "and above" about a custom rank with no ladder position is a parse REFUSAL, not a guess, and a refusal seats nobody. The grammar had been written TWICE (a shape regex in the config sanitizer, a parse regex in the resolver, pinned by a property test) — sound for one literal form, unsound the moment a rule needs a LOOKUP, which is the same class of drift that produced the bug; both now call one pure `membership-predicate.js`. Adds the DISENFRANCHISEMENT GUARD: a proposal that would unseat its own proposer is refused before any write, naming the one-character fix, overridable with an explicit `acknowledge_self_removal` (a separate parameter, never a proposal field) because stepping back is a legitimate act. The two DB reads take a set via `= ANY($1)` + DISTINCT (a double-counted member silently raises the consent bar), the structural filters are re-proven against real Postgres (a deactivated builder and a machine principal still never govern), and the hall panel now describes every form in plain language — rendering `rank:metic+` as "the metic+ rank" would be the same bug one layer up. Unblocks task 1003095, which can now write each charter's TRUE predicate rather than wording around a limitation.](0179-board-membership-predicate-grammar.md) | government / the board |
266
266
  | 0180 | [**Dependency intake & drift policy: baseline-ratchet audit gate, install scripts off, cooldown via patrol, OIDC publish** ([task 1003127](https://cloudbongos.com/builders#/task/1003127), goal 1000076). The red-team baseline audit found 18 advisories (2 high) sitting unwatched in 9 production deps, the `undici ^6.27.0` override pinned INSIDE a vulnerable range while also breaking Node 26 CLIs, and nothing gated what an agent may install — against a 2025 npm threat wave (chalk/debug, Shai-Hulud, Nx) that ran on postinstall scripts and long-lived publish tokens. Four decisions. (1) `scripts/gds/audit-gate.js` runs in the required `unit` check: a high/critical advisory NOT in the committed `config/audit-baseline.json` fails the merge gate; the baseline is **shrink-only pre-gate debt** (10 findings at adoption) — adding an entry to silence a NEW advisory is out of policy. Fail-OPEN on registry/infra errors, fail-CLOSED on findings: the gate catches new advisories, it does not prove registry uptime. Notably a blanket `npm audit fix` cannot even run here (`workspace:` protocol, EUNSUPPORTEDPROTOCOL) — fixes are explicit tasks. (2) `.npmrc ignore-scripts=true`: the tree needs no install-lifecycle scripts — sharp installs prebuilds via optionalDependencies, proven by a script-less `npm ci` + full 430-test suite; a future dep needing one is an ADR-level exception. (3) Update cooldown is PATROL-enforced (task 1003128 flags versions younger than 7 days + stale overrides + action-pin drift) rather than `.npmrc`-enforced, because CI's npm 10 warns on the npm-11 `minimum-release-age` key for every builder; move it into `.npmrc` when the toolchain floor reaches npm ≥ 11.10. (4) Publish auth moves to npm **trusted publishing (OIDC)** — needs a one-time owner registration on npmjs.com (blocker filed); until then the granular 90-day `NPM_TOKEN` is a documented exception.](0180-dependency-intake-and-drift-policy.md) | devsecops / supply chain |
267
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 |
@@ -273,7 +273,7 @@ This keeps the decision history honest and traceable.
273
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.](0186-per-project-idea-templates.md) | ideas / templates |
274
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.](0187-collab-four-decisions.md) | collab / social hall |
275
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.](0187-oauth-handshake-cookie-lifetime.md) | auth / security posture |
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.](0188-patrol-runs-on-ci-not-the-server.md) | security / enforcement durability |
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 0158 §2), 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.](0188-patrol-runs-on-ci-not-the-server.md) | security / enforcement durability |
277
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.](0189-baseline-floors-and-the-freeze-detector.md) | quality ratchets / debt governance |
278
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.](0190-settings-apply-in-place-env-patch.md) | provisioning / settings channel |
279
279
 
@@ -328,7 +328,7 @@ This keeps the decision history honest and traceable.
328
328
  | 0237 | [**A starter bundle is a preset over the always-on core, keyed by the declared project type** ([task 1002338](https://cloudbongos.com/builders#/task/1002338) · goal 1000046, criterion `proj-module-select`). "During setup a default set of modules is pre-selected" can only mean the OPT-IN modules, since every instance already boots with the same sixteen `default: true` core-domain ones — so a bundle is a short list of opt-in module keys layered on that floor, preselected and freely toggled off by R20's picker ([task 1002339](https://cloudbongos.com/builders#/task/1002339)). **The key is the DECLARED type, and the seeded vocabulary is stale:** the task text (2026-07-17) names *Game / SaaS-Tool / Community*, but task [1002772](https://cloudbongos.com/builders#/task/1002772)'s owner decision (2026-08-13) fixed the live closed set as `game · research · business · non-profit · not-sure` — two CHECK constraints, two `PROJECT_TYPES` constants and five wizard buttons — and "community" survives only as a `team_shape`. Two constraints ride the key: the arrow runs **type → modules, never back** (1002772 rejected inferring type from module flags, which flip all through setup), and the R15 interview must **not re-ask** what `type` declares, so R19 adds no `PROJECT_DETAIL_VOCAB` key and no migration. `not-sure` and *undeclared* resolve to the same modules but are deliberately different answers — `type` is nullable precisely so "nobody said" is never back-filled with a guess, and the resolver returns `type: null` with its own copy. **The declaration is pinned by a test, not a runtime catalog read:** a module may not import `src/module-loader/catalog.js` (the one-way rule, [ADR 0083](0083-modular-architecture-strangler-build-plan.md)), so `modules/provisioning/starter-bundles.js` stays PURE like `onboard-plan.js` beside it and `tests/provisioning_starter_bundles.mjs` reads every `modules/*/module.json` off disk — failing the build if a bundle names a module that does not exist, if `ALWAYS_ON_CORE` drifts from the manifests' `default: true` set, if a bundle names an always-on module (a preselect that toggles nothing), or if it names one of the two hub-only modules. The bundles are `game → dev-box, discord`; `research → agents`; `business → agents, dev-box`; `non-profit → discord`; `not-sure → —`, and they are SHORT because today's opt-in catalog is three modules wide and a preselect an owner must audit is worse than none. Served whole at public `GET /provisioning/starter-bundles` (`{ core, bundles }`, same absent gate as `onboard-plan`) so the picker re-preselects without a refetch and R17's recommendations read the same shape. A bundle is **never a publish precondition** — [ADR 0182](0182-project-visibility-two-axes.md) D4 fixes that bar at name + description alone. Rejected: a runtime catalog read, a `bundle` key in the interview vocabulary, inferring the bundle from the description (the deferred AI layer, [task 1003500](https://cloudbongos.com/builders#/task/1003500)), and keeping the seeded Game/SaaS-Tool/Community triple.](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) | provisioning / project creation |
329
329
  | 0239 | [**A recommendation is a table that carries its own reason; governance is a preview and invites are a seam** ([task 1002336](https://cloudbongos.com/builders#/task/1002336) · goal 1000046, criteria `proj-module-select` / `proj-governance-reco` / `proj-invite-builders`). R19 ([ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md)) declared the per-type starter bundle and said outright it is "a declaration and not a rule engine — R17/R22 own the layer that reasons"; this is that layer, and the goal pins it **rule-based**, with the AI/embeddings pass deferred ([task 1003500](https://cloudbongos.com/builders#/task/1003500), idea 1000552). **Two inputs, and the free-text description is not one of them:** the DECLARED `type` (a column) and `team_shape` (the one closed-answer key the R15 interview collects, [task 1002334](https://cloudbongos.com/builders#/task/1002334)) — inferring from prose is the deferred layer ADR 0237 §7 already rejected once. The engine is three frozen tables (`TEAM_SHAPE_RULES` add/remove ONE opt-in module; `GOVERNANCE_RULES` name a preset; `INVITE_RULES` emit a seek) and **every row carries its own `reason` sentence**, shipped with the effect rather than reconstructed by whichever surface renders it — a recommendation an owner cannot be told the reason for is one they cannot disagree with. `bundle.adjustments` is a **DELTA**: a rule whose condition matched but whose module the bundle already carried records NOTHING, because "we switched discord on because you said community" over a module a game would have had anyway is a claim the owner can check and find false. **Governance is a named preview, never a setting** — the governance module is planned and unbuilt (goal 1000030), so a preset is a name plus one sentence of what it feels like (`owner-only` / `trusted-team` / `open-contribution`) and every result carries `applied: false`, one flag instead of the same fact hardcoded in R25, R18, the wizard and the project page. **Invites are a SEAM**: `suggested` is always `[]` because ranking builders means querying the leaderboard in the sibling `economy` module, which the one-way rule ([ADR 0083](0083-modular-architecture-strangler-build-plan.md)) forbids importing — the engine emits `seeking: { kind: 'top-builders', limit }` (or `null` = suggest nobody, a real answer) and R28 ([task 1002347](https://cloudbongos.com/builders#/task/1002347)) turns it into names. **Skipping is an answer:** every table states a default and each is the NARROWEST option (widening later costs one setting; a role granted to strangers is not recovered by setting it back), an off-vocabulary `team_shape` reads as no answer exactly as `normalizeProjectDetail` drops it, and the route answers 200-with-defaults rather than 400 — refusing would make the supported path ([ADR 0182](0182-project-visibility-two-axes.md) D4) an error. `tailored: boolean` is the machine-readable half of `proj-detail-interview`'s details-vs-skip split that R18 ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) proves. Served at public `GET /provisioning/recommendations?type=&team_shape=` (same absent gate as `starter-bundles`; no database, pinned by a `pool`-throws stub), returning ADR 0237 §5's bundle shape plus `adjustments` so a recommendation and R20's preselect can never name different modules. Rejected: reading the description, filling `suggested` from `GET /public/leaderboard`, a weighted scoring function, a reason per matched condition, and a 400 on an unknown shape.](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md) | provisioning / project creation |
330
330
  | 0240 | [**A skipped module picker stores nothing, and the bundle is resolved on read** ([task 1002339](https://cloudbongos.com/builders#/task/1002339) · goal 1000046, criterion `proj-module-select`). [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) declared the starter bundles and closed with "where a chosen selection lands is R20's decision". It lands in a new **nullable** `modules jsonb` column on `provisioning_instances` (`provisioning_014`), read through `starter-bundles.effectiveModules(row)` and **three-valued**: `NULL` = nobody answered (the picker was skipped, or the caller does not ask) and the declared type's bundle is resolved AT READ TIME; `[]` = the owner switched them all off, a real answer; `["dev-box",…]` = what they picked. `NOT NULL DEFAULT '[]'` was rejected because it collapses the first two forever — the `PROJECT_DETAIL_VOCAB` rule ("absent stays absent, skipped stays skipped") applied to a list. **Skipping is safe because the preselect and the fallback are the same function**, `bundleForProjectType`, called twice — not two lists kept in agreement — so `bongos onboard` and any API caller inherit the bundle by omitting the field, and the canonical sequence in `onboard-plan.js` grows **no new step** (the picker refines `type`, an answer already in it). NOT `detail` (its vocabulary is closed + scalar by construction, and [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) §2 ruled the interview must not ask this) and NOT `settings` (a setting is a policy the RUNNING instance obeys — web.env, a settings-apply intent, a restart). The picker's catalog copy (`OPTIONAL_MODULES`) is **declared beside the bundles**, because `module.json` descriptions are written for builders, `GET /modules` needs a session the signed-out wizard has not got, and `GET /public/modules` serves booleans — pinned the ADR 0237 way, so a new `default: false` module cannot ship without owner-facing copy. `GET /provisioning/starter-bundles` gains `optional` so the preselect and the catalog arrive in **one** read. The route **refuses** an unknown / always-on / hub-only key (`400 bad_modules`) rather than dropping it silently (`badProjectDetail`'s rule), and a re-request reports `modules_ignored`. The column is a DECLARATION, not yet an input to the control-plane runner. Rejected: storing the preselect on a skip (it writes down an answer nobody gave), a new `onboardPlan` step, and rendering the always-on core as sixteen disabled toggles (a disabled control reads as a choice the owner failed to make).](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md) | provisioning / project creation |
331
- | 0241 | [**The artist gate holds the DEPLOY, not the ship, and it reads a state rather than a judgement** ([task 1003116](https://cloudbongos.com/builders#/task/1003116) · goal 1000074, criterion `artist-blocking-is-per-project` — R05, the DECISION task the goal placed before R04 so no code would be wired to an unsettled gate). Goal 1000074 carries two rules on a collision course: the owner's "blocking strictness is PER PROJECT, not global", and [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md), which retired review gates on `main` after a code-owner requirement stranded two graded-PASS ships in a day. **0162 is usually cited by a paraphrase — "a verdict is advisory, never self-executing" — and the paraphrase is wrong as a blanket claim:** 0162's own table of compensating controls marks three of four as *fails closed*, the subagent grader among them. What it actually retired was a gate GitHub could only express as ACCOUNTS — identity-pinned against [ADR 0016](0016-trust-boundary-server-enforced-permissions.md), and stranding with no escape hatch. So the surviving test is: **a gate is admissible when it is evaluated automatically from readable state, consistently for every builder, with a named escape hatch; inadmissible when a particular person must act and it stalls when they do not.** Decision: (1) the gate reads a **STATE** — "is there an unresolved artist review task?" — never an opinion, so the artist's judgement is what they write INTO the task and stays advisory in exactly 0162's sense, while what the system reads is ordinary GDS status; (2) strictness is a project setting `artist_gate ∈ off | advisory | strict` and at `strict` it holds the **DEPLOY, not the ship** (owner ruling) — the builder's work is graded, merged, credited and landed on CI alone exactly as today, so 0162's strand class cannot recur, and the gate attaches to the boundary the artist's ABI mandate is actually about (users see the deployed instance, not `main`; [ADR 0137](0137-upstream-publish-policy-manual-owner-gated.md) already splits land/release/deploy); (3) the escape hatch is the **project OWNER only** — not the builder, not any Metic+, not the artist — via the existing `POST /tasks/:id/override-request`, because a person can be both artist and builder and could pass their own work; a self-release is permitted but never quiet (required rationale, audit-logged as a self-override) so a solo instance cannot deadlock; (4) the default is **`strict`** (owner ruling, against the author's `advisory` recommendation), which is safe only because §2 holds the deploy — the worst case is a pin that waits, and this instance is included once R04 ships; a project with no artist must still be able to deploy, and the refusal must name what holds it and which control releases it; (5) tightening is **not retroactive** — re-gating history would strand the next deploy behind reviews for work nobody remembers and make the setting frightening to raise; R02's flag queue is the right tool for already-shipped copy. Rejected: blocking the ship (recreates 0162's incident exactly, with credits unpaid — [ADR 0120](0120-pay-on-land-and-builder-owned-rebase-gate.md)); advisory at every level (configures nothing); any Metic+ as the hatch (makes `strict` indistinguishable from `advisory`); making the artist's VERDICT the gate (the one thing 0162 genuinely forbids); retroactive tightening; a global constant (ruled out by the owner's constraint by name).](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md) | goal 1000074 / the artist's loop |
331
+ | 0241 | [**The artist gate holds the DEPLOY, not the ship, and it reads a state rather than a judgement** ([task 1003116](https://cloudbongos.com/builders#/task/1003116) · goal 1000074, criterion `artist-blocking-is-per-project` — R05, the DECISION task the goal placed before R04 so no code would be wired to an unsettled gate). Goal 1000074 carries two rules on a collision course: the owner's "blocking strictness is PER PROJECT, not global", and [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md), which retired review gates on `main` after a code-owner requirement stranded two graded-PASS ships in a day. **0162 is usually cited by a paraphrase — "a verdict is advisory, never self-executing" — and the paraphrase is wrong as a blanket claim:** the phrase is ADR 0158 §2's heading, not a sentence of 0162, and 0162's own table of compensating controls marks three of four as *fails closed*, the subagent grader among them. What it actually retired was a gate GitHub could only express as ACCOUNTS — identity-pinned against [ADR 0016](0016-trust-boundary-server-enforced-permissions.md), and stranding with no escape hatch. So the surviving test is: **a gate is admissible when it is evaluated automatically from readable state, consistently for every builder, with a named escape hatch; inadmissible when a particular person must act and it stalls when they do not.** Decision: (1) the gate reads a **STATE** — "is there an unresolved artist review task?" — never an opinion, so the artist's judgement is what they write INTO the task and stays advisory in exactly ADR 0158 §2's sense, while what the system reads is ordinary GDS status; (2) strictness is a project setting `artist_gate ∈ off | advisory | strict` and at `strict` it holds the **DEPLOY, not the ship** (owner ruling) — the builder's work is graded, merged, credited and landed on CI alone exactly as today, so 0162's strand class cannot recur, and the gate attaches to the boundary the artist's ABI mandate is actually about (users see the deployed instance, not `main`; [ADR 0137](0137-upstream-publish-policy-manual-owner-gated.md) already splits land/release/deploy); (3) the escape hatch is the **project OWNER only** — not the builder, not any Metic+, not the artist — via the existing `POST /tasks/:id/override-request`, because a person can be both artist and builder and could pass their own work; a self-release is permitted but never quiet (required rationale, audit-logged as a self-override) so a solo instance cannot deadlock; (4) the default is **`strict`** (owner ruling, against the author's `advisory` recommendation), which is safe only because §2 holds the deploy — the worst case is a pin that waits, and this instance is included once R04 ships; a project with no artist must still be able to deploy, and the refusal must name what holds it and which control releases it; (5) tightening is **not retroactive** — re-gating history would strand the next deploy behind reviews for work nobody remembers and make the setting frightening to raise; R02's flag queue is the right tool for already-shipped copy. Rejected: blocking the ship (recreates 0162's incident exactly, with credits unpaid — [ADR 0120](0120-pay-on-land-and-builder-owned-rebase-gate.md)); advisory at every level (configures nothing); any Metic+ as the hatch (makes `strict` indistinguishable from `advisory`); making the artist's VERDICT the gate (the one thing 0162 genuinely forbids); retroactive tightening; a global constant (ruled out by the owner's constraint by name).](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md) | goal 1000074 / the artist's loop |
332
332
  | 0242 | [**A cross-role cascade is a declaration table on the event that already exists** ([task 1003549](https://cloudbongos.com/builders#/task/1003549) · goal 1000074, criterion `artist-review-task-on-ship` — the split R04 invited in its own brief and [ADR 0241](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md)'s dependency). R04 needs "every ship with copy or visuals GENERATES one", and its brief said the infrastructure did not exist, naming the spark queue as the prototype. Reading both surfaces shows the prototype is the wrong half: the spark queue ([task 1002921](https://cloudbongos.com/builders#/task/1002921)) and the copy-desk flag queue ([task 1003113](https://cloudbongos.com/builders#/task/1003113)) are both **PULL** — filtered listings of rows another role already created — and nothing anywhere generated work on a state transition, which is the **PUSH** R04 asks for. (`cascade-dispatch.js` is model-cost routing; named only because its name has misled more than one reader.) **Decision: no new bus.** The kernel seam registry's own header uses this exact scenario as its worked example, `task.shipped` is already emitted post-commit by both ship paths and already has two consumers (discord's broadcast, sessions' deferred reward) — so a cascade is ONE MORE LISTENER on it, registered in `routes/lifecycle.js` beside the port because that mount is the one boot hook tests also run. A cascade is a frozen entry `{ key, discipline, when(task), title(task), body(task) }` — **a declaration table, not a rules engine**: no condition language, no stored rules, no per-project config, no priority ordering; adding one appends an entry and the runner does not change (the [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md)/[0239](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md) shape, for their reason — the generalisations worth building are the ones two or three real entries have shown they share). **`CASCADES` ships EMPTY and R04 writes the first rule**, asserted so the emptiness is a decision on the record; the mechanism is proved against the tests' own declarations rather than against its first consumer's shape. Four load-bearing properties, all pinned: (1) **it can never fail the ship it reacts to** — post-commit event, isolated listeners, and a runner that catches everything and returns a report, because ADR 0241 settled that an artist may hold a DEPLOY and never a builder's SHIP and a generator that could fail a ship would smuggle that gate back in; verified against a real failure (no database reachable → one log line, resolves); (2) **idempotent on machinery that already exists** — `source: 'cascade'` + `source_ref: cascade-<key>-<taskId>` through `createTask`'s `(source, source_ref)` read, the same one a copy proposal uses ([ADR 0233](0233-a-copy-proposal-is-a-task-carrying-a-patch.md)), so a reconciler re-sweep or a double emit is a no-op; (3) **it says where it came from** — inherits `goal_id`/`version_id`, carries the origin id in the ref and the prose, but NOT `parent_task_id`, which models a SUBTASK, and cascaded work is not part of the ship that triggered it; (4) **files at `backlog`, never `ready`** — generated work is a request, and auto-filing claimable work would let a rule nobody re-read mint claims. A rule key is PERMANENT (half the source_ref; renaming re-files everything it ever generated), pinned by a duplicate-key check. The mechanism is discipline-agnostic — an ideator or engineer cascade is the same entry with a different `discipline`, which is the generalisation that made this its own task. Accepted limit: cascades are declared in code, not configured per project. Rejected: extending the spark queue (a read-side filter that generates nothing), a new event or poller, a rules engine or per-project config, filing at `ready`, `parent_task_id` as the link, and running the cascade inside the ship transaction (which would make a generation failure a ship failure — the exact coupling ADR 0241 rules out).](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md) | goal 1000074 / the artist's loop |
333
333
  | 0243 | [**A bundle adjustment belongs to the bundle, not to the advice about it** ([task 1002341](https://cloudbongos.com/builders#/task/1002341) · goal 1000046, criterion `proj-module-select`). R17 ([ADR 0239](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md)) built a rule engine and R20 ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md)) built a picker, and nothing connected them: the picker preselected the type's RAW preset while `GET /provisioning/recommendations` separately applied `TEAM_SHAPE_RULES` to that same preset and returned a better answer no surface rendered. Joining them the obvious way — let the picker apply the rules — breaks the property R20 was built on ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md) §2: the preselect and the fallback are "one function, called twice"), because an owner who answers the interview, watches the panel preselect `discord`, and then SKIPS the picker would get a project without it. **Decision: `TEAM_SHAPE_RULES` moves from `recommendations.js` down to `starter-bundles.js`** (re-exported, so the public surface is unchanged), `effectiveModules` resolves through the new `bundleForAnswers(type, teamShape)`, and the picker reads the same table the fallback does. The distinction is not a preference: a bundle adjustment is not advice ABOUT the bundle — it IS the bundle once the owner has answered, and it is the only one of R17's three tables that is ever APPLIED (governance is a preview carrying `applied: false`, invites are a seam for R28), so it is the only one that has to live where the resolution happens. `effectiveModules` reads `detail.team_shape` straight off the row rather than through `normalizeProjectDetail` — importing it would point the bundle layer UP at the module that requires it ([ADR 0083](0083-modular-architecture-strangler-build-plan.md)) to buy an answer it already has, since an off-vocabulary shape fires no rule and adjusts nothing. `GET /provisioning/starter-bundles` gains `byTeamShape`: every bundle re-resolved for every ruled answer, with the `adjustments` that got it there — twenty small rows, the whole preselect table in ONE read, for the reason `optional` rides the same payload (the owner flips type and team-shape on two EARLIER panels, and a refetch could land after they moved on or paint a key the panel has no row for). `PATCH /provisioning/instances/:id/detail` returns `modules: effectiveModules(updated)` beside its `publish` verdict and for the same reason — changing `team_shape` MOVES the modules of a `source: 'bundle'` project, so the effect comes back with the cause. The panel renders one reason line per adjustment that ACTUALLY fired, and only while the owner has not answered for themselves (a reason handed back for a toggle they flipped reads as the panel arguing with them). An adjusted bundle returns in CATALOG order, never append order, or the same set of modules would read as a different list depending on whether the picker was answered or skipped; a payload without `byTeamShape` degrades to the R20 answer, which is never wrong, only untailored. No new storage — the adjustment is resolved, never written. Rejected: leaving the rules up in `recommendations.js` and having the picker call `/recommendations` (two lists kept in agreement by hand, plus a round trip per keystroke); importing `normalizeProjectDetail`; storing the adjusted bundle on a skip (it writes down an answer nobody gave); moving governance and invites down too (they are never applied, so there is nothing for them to stay in agreement with); and a reason line per matched condition rather than per actual change.](0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md) | provisioning / project creation |
334
334
  | 0245 | [**The way back into an optional step is that step’s own answer route** ([task 1002342](https://cloudbongos.com/builders#/task/1002342) · goal 1000046, criterion `proj-module-select`). R20 ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md)) made the create wizard’s module picker skippable and left a project’s module set write-once — the route file said so out loud (“there is no mutation route yet”). R23 closes it WITHOUT a new endpoint: `modules` joins `description` and `detail` on `PATCH /provisioning/instances/:id/detail`, because the wizard’s two optional steps are the same kind of thing — asked optionally at creation, stored on the platform row, reaching no running instance — and that route already carried the ownership gate, the `api:self`/`api:archon:<id>` actor rule, the 404-no-existence-leak shape, and (since [ADR 0243](0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md)) an `effectiveModules` answer it could report but not set. Deliberately NOT the settings channel: a setting is a policy the running instance obeys and would queue a restart to deliver a value the instance does not read. Only an ARRAY is a change — an omitted or null key leaves the column alone — so the three-valued contract’s `NULL` (“never asked”) stays unreachable by a later edit, and `modules` REPLACES where `detail` merges, because a merging write would make removal impossible. The surface is the manage view’s extras band (`/projects?view=manage&id=N`), where a toggle IS the save and the band repaints from the platform’s answer rather than from what was sent.](0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md) | provisioning / project creation |
@@ -19391,7 +19391,7 @@
19391
19391
  "tasks"
19392
19392
  ],
19393
19393
  "summary": "GET /tasks/:id/publish-status",
19394
- "description": "GET /tasks/:id/publish-status?branch=<branch> — poll the PR + prod-deploy state for a server-mediated publish so ship.js can confirm the land WITHOUT gh on the box. Ownership-gated like the POST. Derives state live from GitHub.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
19394
+ "description": "GET /tasks/:id/publish-status?branch=<branch> — poll the PR + prod-deploy state for a server-mediated publish so ship.js can confirm the land WITHOUT gh on the box. Ownership-gated like the POST. Derives state live from GitHub. `branch` is optional (task 1003764): without it the server finds the task's own PR (both publish paths title it \"task <id>: …\") and the answer names the `branch` it used and its `branch_source`, so the check agents are sent to instead of git probes never refuses a live publish for want of a branch name.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
19395
19395
  "x-rank": "any-builder",
19396
19396
  "x-source": "modules/lifecycle/routes/tasks.js",
19397
19397
  "parameters": [
@@ -14,6 +14,7 @@
14
14
  - **Cloud Bongos control-plane droplet (NEW 2026-07-05, [ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md), task [#2063](https://example.com/builders#/task/2063)):** DigitalOcean, hostname `cloudbongos`, IPv4 `REDACTED_IP`, NYC3, `s-1vcpu-2gb` (2 GB / 1 vCPU), Ubuntu 24.04, ~$12/mo, tag `cloudbongos-control`. Runs **cloudbongos.com + `builders.` + `status.`** (`src/platform-server.js`, Cloud Bongos brand, port 3002, own `cloudbongos` DB, checkout `/home/lars/cloudbongos-instance`, the root subscribed to the nightly core-patch sweep) and is the **sole token-holding control plane** — a SECOND root, `/home/lars/cloudbongos`, survives only to vendor `infra/` (the box cloud-init the core package does not ship, ADR 0150); it is not subscribed, so nothing may resolve core code from it (task 1002738, [ADR 0144](adr/0144-devbox-rehome-onto-cloudbongos-plane.md) follow-up 5) — `/etc/cloudbongos/box.env` holds the account-wide Cloudflare token (IP-locked to this box) + the DigitalOcean token; this is where `provision.js` provisions instance subdomains. **Structurally separate SSH** (own `cloudbongos_ed25519` key). IPv6 egress disabled so the IPv4-locked CF token can't be spuriously rejected. cloudbongos.com was previously a co-tenant on the co-hosting box; that unit was decommissioned at cutover. **Follow-ups:** deploy automation (task [#2064](https://example.com/builders#/task/2064)) + fully stripping cloud tokens off the co-hosting box (task [#2065](https://example.com/builders#/task/2065)).
15
15
  - **Server stack:** Node.js 22 LTS · PostgreSQL 16 · Caddy 2.11.2. `example.service` (systemd) runs `node server.js` as user `lars` on `127.0.0.1:3000`; restart-on-failure.
16
16
  - **Hardening:** `lars` user with passwordless sudo; SSH key-only (root login disabled, password auth disabled); ufw allows 22/80/443; fail2ban active; unattended-upgrades enabled; timezone `America/New_York`.
17
+ - **Credentials:** every credential the instance needs — what it is for, which env file holds it (`web.env` · `box.env` · `spaces.env`), GitHub Actions secrets and builder-machine logins — is listed in [`docs/credentials.md`](credentials.md), names only. Whether each is SET on a host is one command run there: `node scripts/gds/credential-presence.js` (task 1002880). Check both before asking the owner for a credential.
17
18
  - **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`); the provisioning runner verifies the box's resolved Caddy config carries that import and the `on_demand_tls` ask before it reports a domain wired, and refuses with the line to add rather than editing the base file ([ADR 0303](adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
18
19
  - **Platform observability — REMOVED 2026-09-15 ([ADR 0284](adr/0284-the-control-plane-carries-no-metrics-stack.md), task 1003876):** the control plane runs **no Grafana and no Prometheus**, and `metrics.cloudbongos.com` has no Caddy site. They cost ~300 MB RSS + 2.5 GB of data on the box every provisioned project shares, and scraped none of those projects. Every instance still exports `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js), loopback or `METRICS_TOKEN`), so a scraper can return from a box that is *not* the control plane. The rest of this bullet is the **historical** ADR 0127 layout, kept so its leftovers (co-hosting-box forwarders, instance `infra/` copies) can be found: this watched the **Bongos API** (`gds_http`/`tasks`/`claims`/`builders`/`auth`), not any one game, so it lived **on the control-plane box** (REDACTED_IP) at **`https://metrics.cloudbongos.com`** and observed the **whole fleet**. **Prometheus** (`127.0.0.1:9090`, loopback-only) scrapes every instance's `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js)), each tagged with a `project` label (`infra/prometheus/prometheus.yml` (instance-side)): **cloudbongos** locally (`:3002`); **example + emersonian-circles** on the co-hosting box, reached over the **private VPC** via `socat` forwarders on `10.108.0.2:9101/9102` (→ their loopback `:3000`/`:3004`), ufw-locked to the control-plane box's private IP — raw TCP relays, so the exporter still sees a loopback request and needs no token; nothing is public. **Grafana** (`127.0.0.1:3003`, loopback-only) renders the 4 provisioned-as-code dashboards (folder **Cloud Bongos**: system-overview, error-watch, builder-activity, auth-security), each with a **`project`** template variable for per-instance / fleet views. Reverse-proxied on the `*.cloudbongos.com` origin cert — Archon-only, gated by Caddy `basic_auth` (user `archon`) **and** Grafana's own admin login (both secrets owner-held, never in the repo). Grafana config is a systemd `GF_*` env drop-in (`grafana-server.service.d/override.conf`, `chmod 600`). ~256 MB RAM. A future dedicated-droplet instance is added as a `METRICS_TOKEN` Bearer target instead of a VPC forwarder.
19
20
 
@@ -41,7 +42,7 @@ The product is organized as a small kernel (`src/`) + **19 modules** under `modu
41
42
  - **Core-domain modules** (`default: true` — always on unless disabled): `memory`, `grading`, `economy`, `ideas`, `security`, `builder-settings`, `onboarding`, `sessions`, plus `lifecycle` (the task/claim/version state machine), `autonomy` (the MAS), the `hall-ui` + `status-ui` internal surfaces, `public-landing` (the apex front door — [ADR 0218](adr/0218-the-apex-root-web-surface.md)), `ui-design` (the design contract, its adapters and the `/design` playbook as a platform capability — [ADR 0197](adr/0197-the-ui-design-module.md)), and the `demo` acceptance-proof module. See [docs/modules-contract.md](modules-contract.md) for the live roster and which default on.
42
43
  - **Module-owned migrations** — `modules/<key>/migrations/*.sql` applied only when the module is enabled. Stem-keyed so moving a migration never re-runs it.
43
44
  - **`bongos module new <key>`** — scaffolds a new module directory in one command.
44
- - **`bongos upgrade`** — the update-consumption channel a two-repo consumer uses to pull the core from version N→N+1 (task 1690, ADR 0100 §2 / ADR 0103 §4): pre-flight (module-compat + clean tree) → bump the vendored-tarball pin → `npm install` → `npm run migrate` (additive `core_*` migrations) → re-materialize `.claude/` → restart → verify. Run bare (no `--to`) it is still just the module-compat pre-check that exits 1 on mismatch; `--to <version>` runs the real bump. (`scripts/gds/upgrade.js`.)
45
+ - **`bongos upgrade`** — the update-consumption channel a two-repo consumer uses to pull the core from version N→N+1 (task 1690, ADR 0100 §2 / ADR 0103 §4): pre-flight (module-compat + clean tree) → bump the vendored-tarball pin → `npm install` → `npm run migrate` (additive `core_*` migrations; the core's own `migrate.sh` when an adopted instance declares no such script — task 1004274) → re-materialize `.claude/` → restart → verify. Run bare (no `--to`) it is still just the module-compat pre-check that exits 1 on mismatch; `--to <version>` runs the real bump. (`scripts/gds/upgrade.js`.)
45
46
 
46
47
  **V1 networking design:**
47
48
 
@@ -65,7 +66,7 @@ The product is organized as a small kernel (`src/`) + **19 modules** under `modu
65
66
  **Code & deploy:**
66
67
 
67
68
  - **Code repo:** [github.com/example-owner/example](https://github.com/example-owner/example) — **private**. Clone URL: `git@github.com:example-owner/example.git`. Default branch: `main`. GitHub user: `example-owner`.
68
- - **Deploy:** two modes, selected by `config/deploy.json` `mode` (env `OTB_DEPLOY_MODE` overrides), currently **`ci`** (flipped from `laptop` 2026-06-13, [#859](https://example.com/builders#/task/859)) — see [ADR 0042](adr/0042-builder-self-deploy-ci-auto-merge.md). **`laptop`** (legacy): `git push origin main` from the Mac → `ssh lars@REDACTED_IP ~/deploy.sh`. The droplet has a read-only deploy key at `~/.ssh/github_deploy` registered on the repo. `~/deploy.sh` does: `git fetch + reset --hard origin/main` → `npm ci --omit=dev` if `package-lock.json` changed → `./scripts/migrate.sh` → `sudo systemctl restart example` → `curl /healthz` smoke (**retried up to 15× @ 1s** — `systemctl` reports "active" before the Node process binds the port, so a single curl raced the bind and false-failed healthy deploys: [ADR 0056](adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md) / [#1026](https://example.com/builders#/task/1026)). Droplet checkout: `/home/lars/example/`. `~/deploy.sh` is hand-maintained on the droplet but now has a byte-faithful, reviewable repo mirror at `scripts/deploy/deploy-prod.sh` (instance-side) — **not** auto-installed (the ci deploy key is forced-command-locked, ADR 0042/0043; reinstall by hand on change: `scp scripts/deploy/deploy-prod.sh lars@…:deploy.sh`). **`ci`** (current, armed via [#679](https://example.com/builders#/task/679)): no builder box holds the droplet key — `ship.js` opens a PR + enables GitHub auto-merge, and `.github/workflows/deploy-prod.yml` (instance-side) runs the same `~/deploy.sh` from Actions on merge to `main`. Cutover preconditions (incl. a server-recorded **CI grade-gate**) are in ADR 0042.
69
+ - **Deploy:** two modes, selected by `config/deploy.json` `mode` (env `<PREFIX>_DEPLOY_MODE` overrides — `BONGOS_DEPLOY_MODE` on a vanilla instance; the legacy `OTB_DEPLOY_MODE` is still read), currently **`ci`** (flipped from `laptop` 2026-06-13, [#859](https://example.com/builders#/task/859)) — see [ADR 0042](adr/0042-builder-self-deploy-ci-auto-merge.md). **`laptop`** (legacy): `git push origin main` from the Mac → `ssh lars@REDACTED_IP ~/deploy.sh`. The droplet has a read-only deploy key at `~/.ssh/github_deploy` registered on the repo. `~/deploy.sh` does: `git fetch + reset --hard origin/main` → `npm ci --omit=dev` if `package-lock.json` changed → `./scripts/migrate.sh` → `sudo systemctl restart example` → `curl /healthz` smoke (**retried up to 15× @ 1s** — `systemctl` reports "active" before the Node process binds the port, so a single curl raced the bind and false-failed healthy deploys: [ADR 0056](adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md) / [#1026](https://example.com/builders#/task/1026)). Droplet checkout: `/home/lars/example/`. `~/deploy.sh` is hand-maintained on the droplet but now has a byte-faithful, reviewable repo mirror at `scripts/deploy/deploy-prod.sh` (instance-side) — **not** auto-installed (the ci deploy key is forced-command-locked, ADR 0042/0043; reinstall by hand on change: `scp scripts/deploy/deploy-prod.sh lars@…:deploy.sh`). **`ci`** (current, armed via [#679](https://example.com/builders#/task/679)): no builder box holds the droplet key — `ship.js` opens a PR + enables GitHub auto-merge, and `.github/workflows/deploy-prod.yml` (instance-side) runs the same `~/deploy.sh` from Actions on merge to `main`. Cutover preconditions (incl. a server-recorded **CI grade-gate**) are in ADR 0042.
69
70
  - **Post-merge branch cleanup (standard practice):** Every feature branch that lands on main via `--no-ff` merge gets deleted on origin once the deploy succeeds. The merge commit itself preserves the branch's history in main, so the named ref isn't carrying any signal — keeping it just clutters GitHub's branch list and tricks the UI into offering empty PRs against zero-diff branches. The auto-merge in [`scripts/gds/ship.js`](../scripts/gds/ship.js) does this automatically after `~/deploy.sh` returns 0; the manual [`/merge-mode`](../.claude/skills/merge-mode/SKILL.md) flow follows the same step 5b. Local refs and worktree directories stay — only the origin branch and the local tracking ref (cleaned on next `git fetch --prune`) go.
70
71
  - **Local dev machine:** Lars's MacBook Air (Apple Silicon, macOS Sequoia). Tooling installed via Homebrew at `/opt/homebrew/`. Node v26.0.0 locally; `package.json` declares `"engines": { "node": ">=22" }`. Single ed25519 SSH key (`~/.ssh/id_ed25519`) authenticates to GitHub and droplet.
71
72
  - **Smoke tests:** lightweight scripts in `/tmp/colyseus-*-smoketest.js` verify joinOrCreate, move protocol, interact, identity, queue. Run any after deploy with `node /tmp/colyseus-<name>-smoketest.js`.
@@ -229,7 +230,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
229
230
  - Own-scoped session self-service ([ADR 0206](adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md), task 1003341), in [`src/bongos/routes/my-sessions.js`](../src/bongos/routes/my-sessions.js): `GET /me/sessions` (every live session on the caller's own account — source, user agent, created/last-used/expires, a `current` flag; the token value is never selected), `POST /me/sessions/revoke` (`{session_id}` — one of your own; a miss answers `revoked: 0` so a stranger's id cannot be probed) and `POST /me/sessions/revoke-all` (sign out everywhere, the requesting session included). All three gate on `requireBuilder` alone and key every statement on `req.builder.id` — an account is never named in the query, body or path. They exist because a bearer refreshes to `now()+24h` on every call, so a leaked one never expires, and the cross-builder pair (`POST /auth/revoke`, `/auth/revoke-all`, both still gated on the admin `auth.session.revoke` atom) needed an Archon the victim might not have. Surfaced in the hall at Settings → Active sessions.
230
231
  - Authenticated CLI/web: `GET /me` (returns `rank`), `GET /versions[/progress]`, `GET /tasks[?version=&status=&kind=&discipline=]`, `GET /tasks/claimable[?version=&discipline=]`, `GET /tasks/:id`, `POST /tasks` (accepts `discipline` + `criterion_ids`; **metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)), `PATCH /tasks/:id` (supports `parent_task_id`, `kind`, `discipline`, `goal_id` — (re)assign the task's goal; must be a goal in the task's own version, task 1763; **metic+**), `POST /tasks/:id/promote` (**metic+**), `POST /claims`, `POST /claims/:id/resolve`, `POST /cost`.
231
232
  - Criterion rollup + task↔criterion links ([ADR 0025](adr/0025-structured-criterion-task-link.md), [#435](https://example.com/builders#/task/435)/[#438](https://example.com/builders#/task/438)): `GET /versions/:id/progress` rolls up each done-when criterion → its gating tasks (via `task_criteria`) → live status counts + the not-yet-shipped `remaining[]` + `unattributed_tasks` (the read behind the `/status` skill, in `src/bongos/done-when.js criterionProgress`). The link CRUD mirrors the dependency endpoints: `GET /tasks/:id/criteria`, `POST /tasks/:id/criteria` (**metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)) and `DELETE /tasks/:id/criteria/:criterionId` (**metic+**). A criterion ref is a numeric `done_when_criteria.id`, a positional `"Cn"` token, or a `criterion_id` slug, resolved against the task's version (`POST /tasks` links at create time so `/status` counts the task with no backfill).
232
- - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). Because the server's credential is the PR's AUTHOR, publish also **assigns the PR to the task's claim holder** and names them in the body (`modules/lifecycle/pr-assign.js`, task 1003991) — best-effort: the 201 carries `pr_assignee` (the login, or `null` when GitHub would not assign it) and a failed assignment never fails the publish. `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
233
+ - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). `branch` is optional: without it the server finds the task's own PR by its `task <id>: …` title, and the answer names the `branch` it used and its `branch_source` (`query` / `task_pr`, or `null` with a `branch_hint` when there is no such PR) — never a `400` (task 1003764). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). Because the server's credential is the PR's AUTHOR, publish also **assigns the PR to the task's claim holder** and names them in the body (`modules/lifecycle/pr-assign.js`, task 1003991) — best-effort: the 201 carries `pr_assignee` (the login, or `null` when GitHub would not assign it) and a failed assignment never fails the publish. `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `<PREFIX>_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
233
234
  - Archon monitoring reads backing the `/watch` page (ADR 0036): `GET /builders/roster`, `GET /grades/by-builder[?days=N]` (**archon-only**, [#726](https://example.com/builders#/task/726) — per-builder grade breakdown for the MARKS section; the project-wide aggregate stays public at `/public/grades`), `GET /audit-log`, `GET /override-requests`, `GET /access-requests`, `GET /security/reports`. Personal-prefs writes backing `/settings`: `GET/PATCH /me/skill-prefs`, `PATCH /me/disciplines`.
234
235
  - Per-builder "needs" + own Gemini key ([ADR 0073](adr/0073-builder-needs-signal-and-byok-gemini-key.md), [#1013](https://example.com/builders#/task/1013)): `GET /me` now also carries `needs` ({items, action_needed_count} — the consistent "the system needs an input from you" signal; `modules/builder-settings/builder-needs.js` is the SSOT — carved out in BV1.R80, resolved by `GET /me` via the `builder-settings` kernel port — rendered by the hall Standing card + the CLI `printNeedsNudge`). The own-key (pragmatic BYOK) endpoints, all own-scoped: `GET /me/art-key/own` (masked meta — last4 only), `PUT /me/art-key/own` (validate-on-save via a Google list-models call → encrypt with `src/bongos/secret-box.js` → store; `503 storage_not_configured` until `BUILDER_SECRET_KEY` is set, `422 invalid_key` on a bad key), `DELETE /me/art-key/own`. The existing `GET /me/art-key` (shared-key box-sync delivery) is extended to also deliver the decrypted own key, which `scripts/gds/fetch-art-key.js` syncs into the local `gemini_api_key` slot (own key wins in the `gen_api.py` cascade).
235
236
  - Builder memory (ADR 0024 cloneable memory + ADR 0026 BFG). Self-scoped (owner is always `req.builder.id`, never input): `POST /memory/sync` (push own memory; own 64 MB parser, vs the global 64 KB), `GET /memory/files`, `GET /memory/file`, `DELETE /memory/file` (the BFG delete affordance, 6C.2). Cross-builder (input-owner) endpoints — all privileged + audit-logged fail-closed: `GET /memory/builders/:id/files`+`/file` (**archon forensic** read), `GET /memory/bfg/builders/:id/files`+`/file` (**BFG-principal only**, 6B.1 read), and the one cross-builder WRITE `POST /memory/builders/:id/bfg-write` (**BFG-principal only**, kill-switched `BFG_WRITE_ENABLED` default-OFF, namespaced `bfg/`, attributed `author: BFG`, write+audit in one tx — dreams + transparent corrections, 6C.1/6C.2).
@@ -315,8 +315,8 @@ pre-merge gate misses, independent of the builder's cooperation:
315
315
  activated it; ADR 0043 §"G-2/G-17 resolution") blocks it. This hook is a CORE-repo control:
316
316
  scaffolded standalone instances are hookless by default — deliberate, not a gap
317
317
  ([ADR 0165](adr/0165-instances-hookless-by-default.md)); the graded
318
- pipeline pushes only via `scripts/gds/push-main.js` (which carries the `OTB_ALLOW_MAIN_PUSH`
319
- override). A raw push is caught and reverted by the **post-push main audit**
318
+ pipeline pushes only via `scripts/gds/push-main.js` (which carries the `<PREFIX>_ALLOW_MAIN_PUSH`
319
+ override — `BONGOS_ALLOW_MAIN_PUSH` on a vanilla instance). A raw push is caught and reverted by the **post-push main audit**
320
320
  (`scripts/gds/main-audit.js`) — nightly and as the `~/deploy.sh` deploy gate — which is the
321
321
  authoritative layer (it reads the real git history + real ranks, independent of the builder).
322
322
  - **The pre-push hook fails OPEN by contract, and the wrapper is what enforces that** (task 1002725).