@bongos/core 1.19.1032 → 1.19.1033
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +159 -54
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grader-health/SKILL.md +1 -1
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +3 -3
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +57 -9
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7411 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +81 -1
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/provision-core-upgrade.js +128 -77
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-land.js +86 -7
- package/scripts/gds/ship-preflight-steps.js +56 -3
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +6 -4
- package/src/module-api.js +1 -1
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/core_upgrade_runner.mjs +89 -1
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_preflight.mjs +160 -3
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -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
|
|
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
|
|
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
|
|
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),
|
|
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
|
|
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
|
|
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.
|
package/docs/adr/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 |
|
package/docs/architecture.md
CHANGED
|
@@ -41,7 +41,7 @@ The product is organized as a small kernel (`src/`) + **19 modules** under `modu
|
|
|
41
41
|
- **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
42
|
- **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
43
|
- **`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`.)
|
|
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; 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
45
|
|
|
46
46
|
**V1 networking design:**
|
|
47
47
|
|
package/docs/file-map.md
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
│ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
|
|
135
135
|
│ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
|
|
136
136
|
│ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
|
|
137
|
-
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,gate,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, gate, task 1003469 (harbor was its fourth until task 1003890 removed the dev box): the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, task 1003308 (pair was its third until task 1003890 removed the dev box): the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`), `thinking.css` THE SKY's one sheet (task 1004231 / sky part 3 — `thinking.html` at `/thinking` is the prototype the owner adopted as the design of record, `docs/design/mocks/sky/`, built over `GET /sky` + `GET /sky/mine`; its script is the seven `sky-*.js` files on one shared `window.OTBSky`; it replaced THE ROOM of task 1004098, which survives as the list drawer, and the old `/sky` page, whose URL 301s here; `idea-objects.js` beside it is the shared five-object vocabulary).
|
|
137
|
+
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, records/ (one file per page family — what each family's redesign settled; CLAUDE.md points at them, and a new record is a new file there so concurrent hall tasks stop colliding on one insertion point, task 1003499), public/{index.html,builders.js,style.css,work,watch,gate,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, gate, task 1003469 (harbor was its fourth until task 1003890 removed the dev box): the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, task 1003308 (pair was its third until task 1003890 removed the dev box): the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`), `thinking.css` THE SKY's one sheet (task 1004231 / sky part 3 — `thinking.html` at `/thinking` is the prototype the owner adopted as the design of record, `docs/design/mocks/sky/`, built over `GET /sky` + `GET /sky/mine`; its script is the seven `sky-*.js` files on one shared `window.OTBSky`; it replaced THE ROOM of task 1004098, which survives as the list drawer, and the old `/sky` page, whose URL 301s here; `idea-objects.js` beside it is the shared five-object vocabulary).
|
|
138
138
|
│ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
|
|
139
139
|
│ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. R03 (task 1003114, ADR 0233) added the PROPOSAL: an artist writes the replacement wording and it becomes a Bongos task carrying a fenced copy-proposal patch — no row, NO second migration (a proposals table is the forbidden column with extra steps), and scripts/gds/copy-apply.js lands it as a real diff under a claim. CLAUDE.md, module.json, registry.js, flags.js, proposals.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms,copy_proposals}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
|
|
140
140
|
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. minimalist-ui/, industrial-brutalist-ui/, gpt-taste/, stitch-design-taste/ (task 1003327 / ADR 0230) are the four looks, composition grammars with no colours of their own: the palette is the instance's pack and the texture is a named entry in styles/; a substrate is an archetype that still ships both modes, gpt-taste's selection is derived from the route rather than rolled, and stitch-design-taste writes a DESIGN.md-shaped contract document whose thirteen are transcribed from the pack. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. style/ (task 1003475 / ADR 0231) is the module's first FIRST-PARTY skill (no upstream, so a `## First party` section instead of `## Rebuilt from`): the /style session in which an owner authors their own look in four bounded steps, rendering the nearest library entries before asking, drafting inside the contract, retuning by measurement, and handing over the one-step adoption without ever writing config/branding.json. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove, blueprint (the first cool look, authored through /style at task 1003475); STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
|
|
@@ -174,6 +174,7 @@
|
|
|
174
174
|
│ ├── ship.js ← resolve claim as shipped, award credits (`/builder-ship`); also uploads a secret-scrubbed session digest to the corpus (6D.1, ADR 0027); on a dev box runs the sandbox-first review gate before resolving (#927, ADR 0046)
|
|
175
175
|
│ ├── ship-visual.js ← the OPTIONAL `--visual <image> [--visual-alt "…"]` leg of a ship (task 1003109): screens the file BEFORE the claim resolves (bad input costs no claim), uploads it AFTER (a failed picture never fails a ship)
|
|
176
176
|
│ ├── package-core.js ← package the core as a versioned, installable artifact + pin manifest (`bongos package-core`; R84, ADR 0100 §1, [#1688](https://example.com/builders#/task/1688)): isPublishable() selection + mirror redaction + fail-closed no-leak gate → dist/bongos-core-<version>.{tgz,manifest.json}; version = src/module-api CORE_VERSION. Recipe: docs/recipes/packaging-the-core.md
|
|
177
|
+
│ ├── release-notes.js ← what each core release brought in (task 1004218): generated at pack time from the ref's HISTORY into the package's release-notes.json (exact per-release ranges, 1 MiB bound; package-core redacts its prose like a doc), read back by the upgrade runner for a from→to range; home of the one merge-summary parser
|
|
177
178
|
│ ├── surface.js ← who the CLI output is FOR: agent (default) or terminal (task 1003680). The `bongos` dispatcher sets BONGOS_SURFACE=terminal when a person types a verb; the skills and the card hook run `node scripts/gds/<verb>.js` directly and keep the agent card byte-identical. Never inferred from isTTY — a session's stdout is not a TTY either, so sniffing would invert it. Helpers: action() (/builder-ship N vs bongos ship N), taskRef(), strong()/em()/code(), table() (markdown vs aligned columns). Guarded by tests/cli_surface.mjs
|
|
178
179
|
│ ├── sandbox-stage.js ← stage the working tree on the live game preview (box OR local) for browser review before ship; resolvePreviewContext picks the context (`/builder-stage`; #927, ADR 0046; local: task 1056)
|
|
179
180
|
│ ├── local-preview.js ← builder CLI for the LOCAL sandbox: start/stop/status/restart/logs/url of the game-only preview at http://localhost:3100 (`npm run preview`; task 1056)
|
|
@@ -415,7 +416,7 @@ tests/
|
|
|
415
416
|
├── fix-task/SKILL.md ← /fix-task N — claim, fix (test-first), verify, and ship a single Bongos task by id end to end; defers to /builder-claim + /builder-ship for their own mechanics
|
|
416
417
|
├── goal-create/SKILL.md ← /goal-create — plan and create ONE goal (the module-scoped workspace between version and criteria, ADR 0086) in one session: <redacted>, scope wall, 2-4 criteria, seed tasks; also the auto-pickup when a task is created with no goal to live in (goal_advisory); Metic+ (Archon for a protected-module scope)
|
|
417
418
|
├── goal-review/SKILL.md ← /goal-review — walk the criterion review queue: each auto-flagged 'met — pending review' criterion gets confirm / reject / defer; confirming a goal's last criterion auto-achieves it (ADR 0086 §6); Metic+
|
|
418
|
-
├── grade-audit/SKILL.md ← /grade-audit — manual-cadence accountability sweep: override ledger (shipped-despite-fail, infra vs real, un-requested confirms named), dropped blocker/major findings on passes queued via capture.js with the advisory-Narc filter, runtime-blindness false-pass spot-check, outage roll-up → /grader-health; describes and queues only, never confirms (ADR
|
|
419
|
+
├── grade-audit/SKILL.md ← /grade-audit — manual-cadence accountability sweep: override ledger (shipped-despite-fail, infra vs real, un-requested confirms named), dropped blocker/major findings on passes queued via capture.js with the advisory-Narc filter, runtime-blindness false-pass spot-check, outage roll-up → /grader-health; describes and queues only, never confirms (ADR 0158 §2, task 1002657)
|
|
419
420
|
├── grade-recover/SKILL.md ← /grade-recover N — the 'my grade failed, now what' loop: diagnose outage vs fabricated fail vs genuine fail from the persisted grade, then run the right recovery (--regrade or the auditable override-request); adds no ship-path gate (ADR 0162, task 1002655)
|
|
420
421
|
├── grader-health/SKILL.md ← /grader-health — read-only grader health check: outage runs (n_unavailable streaks, outage-vs-rejection confirmation), pass-rate drift vs baseline, per-grade cost anomalies, stale model pins; emits exact --regrade remediation lines; optional --probe live calibration (Metic+, paid) (task 1002656)
|
|
421
422
|
├── idea-triage/SKILL.md ← /idea-triage — daily walk through the Bongos idea_inbox (promote / discard / merge); Metic+
|
|
@@ -462,7 +463,7 @@ tests/
|
|
|
462
463
|
├── peer-vote-tally/SKILL.md ← deterministic tally (`tally.js`) folding builder-on-builder peer votes at least 7 days old into karma_log rows (criterion C2) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
|
|
463
464
|
├── publish-watch/SKILL.md ← daily publish-lane check (task 1003916): compares every version the release lane ANNOUNCED in its own release commits against what the registry actually holds, and posts anything announced-but-never-delivered to Discord #ship-news. Exists because the 2026-09-13 outage (8 failed runs, 3 hours) was found by a person reading the npm version list — a failed run is one red row nobody watches, and the next merge erases the evidence. Silent on a healthy day by design; exit 2 (could not read) is distinct from exit 1 (found one).
|
|
464
465
|
├── rank-roles-audit/SKILL.md ← nightly deterministic check that every builder holds one of the four live ranks, so none silently resolves to zero permissions under the governance_002 backfill's INNER JOIN; detect-only, files one idea_inbox row and never repairs, and never fatal at boot (task 1002873; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
|
|
465
|
-
├── redteam-weekly/SKILL.md ← weekly deterministic red-team patrol (task 1003128): full-history gitleaks scan + dependency drift review (audit delta, cooldown, young pkgs, stale overrides) + action-pin check → files security reports (advisory, ADR
|
|
466
|
+
├── redteam-weekly/SKILL.md ← weekly deterministic red-team patrol (task 1003128): full-history gitleaks scan + dependency drift review (audit delta, cooldown, young pkgs, stale overrides) + action-pin check → files security reports (advisory, ADR 0158 §2). FIRED BY .github/workflows/redteam-patrol.yml, Mondays 06:17 UTC — `cadence: weekly` here and that cron must agree or fitness Check 25 (scripts/gds/routine-schedule.js) fails the gate (task 1003209, ADR 0188)
|
|
466
467
|
├── repo-simplify-sweep/SKILL.md ← weekly repo-cleanliness janitor — runs repo-metrics, files dead-code findings to idea_inbox, and auto-fixes only the narrow safe class (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
|
|
467
468
|
├── security-review-weekly/SKILL.md ← weekly security-state review — stale vulnerability_reports, ageing security-tagged tasks, recent high/critical dependency alerts; one Archon broadcast when anything is stale, silent otherwise (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
|
|
468
469
|
├── skill-usage-review/SKILL.md ← nightly skill-usage feedback loop — aggregates 24h of skill invocations into friction findings filed as `[skill-friction]` idea_inbox rows (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
|
|
@@ -2543,5 +2543,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2543
2543
|
landed since 1.19.1030 with no explicit bump. run 36192093201. (task 1002620)
|
|
2544
2544
|
1.19.1032 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2545
2545
|
landed since 1.19.1031 with no explicit bump. run 36192968150. (task 1002620)
|
|
2546
|
+
1.19.1033 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2547
|
+
landed since 1.19.1032 with no explicit bump. run 36194601014. (task 1002620)
|
|
2546
2548
|
---------------------------------------------------------------------------
|
|
2547
2549
|
```
|
|
@@ -152,7 +152,9 @@ It is the machine's working copy, not a place to edit.
|
|
|
152
152
|
| Symptom | Where to look |
|
|
153
153
|
|---|---|
|
|
154
154
|
| Runner line says **Not checking in** | is the PC on? `...\autobongos-service.ps1 status` on the box |
|
|
155
|
-
| **Checking in** but nothing ever ships | the Gate page — is a goal allowlisted, and is the switch on? |
|
|
155
|
+
| **Checking in** but nothing ever ships | first the Gate page — is a goal allowlisted, and is the switch on? If both are on, the **gauge**: `autobongos-service.ps1 digest`, or `node scripts/gds/autonomy-gauge.js`. A `hold` with `unknown: true` means it cannot read your remaining capacity, and it will wait forever rather than guess |
|
|
156
|
+
| The log repeats `hold ... unknown: true` | the gauge has no reading it can vouch for. Open a **terminal** `claude` session on the box once — that writes it. A desktop-app session does not: the writer is a statusLine command, and only a terminal renders one |
|
|
157
|
+
| `hold` naming a **derived** gauge and a cap | working as designed. After the last real reading's window resets, the runner may do `AUTOBONGOS_DERIVED_TASK_CAP` tasks (default 2) before it wants a fresh reading. A terminal session lifts it |
|
|
156
158
|
| Shipping stopped after a while | the log: the runner backs off after three failures and probes; the reason is in the log |
|
|
157
159
|
| It keeps restarting | `status` shows the last result; the `====` banners in the log mark each start |
|
|
158
160
|
|
|
@@ -40,6 +40,8 @@ If the no-leak gate fails, it names the file + the residual literal. Fix the red
|
|
|
40
40
|
|
|
41
41
|
**The neutral floor** (task [#1002217](https://example.com/builders#/task/1002217), audit H4). Because the trunk carries no `config/branding.json`, the *branding-derived* redaction would strip nothing — so `package-core.js` always injects a fixed **`PRIOR_INSTANCE_FLOOR`** (prior-instance tokens/owners) into the doc-redaction + no-leak gate. Published **docs** are de-branded against it and the gate **fails closed** if a floor literal survives, even with no branding.json. **Functional** files still ship verbatim (the R88 fix) and are instead surfaced by the non-fatal `[leak-scan]` (task 1002220). To retire a new prior-instance token, add it to `PRIOR_INSTANCE_FLOOR` (kept in `package-core.js`, never in `mirror-redact.js` — that file's self-leak invariant forbids hardcoded literals).
|
|
42
42
|
|
|
43
|
+
**The release notes** (task 1004218). The tarball also carries `release-notes.json` at its root — what each release brought in, as the merge summaries its ships wrote — so the Deploy page on any project can say what an upgrade brings in without a core git checkout (no tenant box has one). It is the one file derived from the ref's **history** rather than from a committed file, built by [`scripts/gds/release-notes.js`](../../scripts/gds/release-notes.js): pinned in the manifest like every other file (so `--self-verify` reproduces it), its prose **redacted exactly like a doc** (the neutral floor applies — real summaries in the history name prior instances, and shipping them verbatim would fail the leak scan on every pack), and bounded at 1 MiB by dropping the oldest releases. **A shallow clone or a `--from-worktree` build ships without it**, printing `! release-notes.json not generated — <why>`; an upgrade onto such a version then says its notes are unavailable. CI packs at `fetch-depth: 0`, so every release it publishes carries them.
|
|
44
|
+
|
|
43
45
|
## Prove it boots (the standing PROVE-PUBLISH gates)
|
|
44
46
|
|
|
45
47
|
Two standing gates keep "a published core boots clean from a bare install" honest (task [#1002221](https://example.com/builders#/task/1002221), audit H1/H2):
|
|
@@ -55,7 +55,7 @@ files normally. This is opt-in on purpose — the patrol must not *require* a
|
|
|
55
55
|
long-lived token to be useful.
|
|
56
56
|
|
|
57
57
|
**Red means a finding was LOST, not that a finding exists.** Findings are
|
|
58
|
-
advisory (ADR
|
|
58
|
+
advisory (ADR 0158 §2), so a run that files three of them is a success:
|
|
59
59
|
|
|
60
60
|
| the run… | exit |
|
|
61
61
|
|---|---|
|
|
@@ -88,7 +88,7 @@ Reports land at `target: redteam-patrol/<class>` for Archon adjudication in the
|
|
|
88
88
|
red-team dashboard — confirm / dispute / link-fix, exactly like a
|
|
89
89
|
`/builder-redteam` filing (they share the pipeline and the bounty table; patrol
|
|
90
90
|
reports are machine-filed, so confirming one pays nobody). The fix is always an
|
|
91
|
-
ordinary task; the patrol never gates and never self-executes (ADR
|
|
91
|
+
ordinary task; the patrol never gates and never self-executes (ADR 0158 §2).
|
|
92
92
|
|
|
93
93
|
## Design boundaries
|
|
94
94
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
2. **bump the pin** — vendor the new `bongos-core-<version>.tgz` and rewrite the `"@bongos/core": "file:vendor/…"` dependency.
|
|
15
15
|
3. **`npm install`** — `node_modules/@bongos/core` becomes the new version.
|
|
16
16
|
3b. **verify the integrity pin** (task [#1002216](https://example.com/builders#/task/1002216)) — recompute `tree_sha256` over the installed core and check the vendored `.tgz` bytes against `tarball.sha256`, comparing to the trusted reference manifest (the committed `vendor/…manifest.json`; the installed inner manifest for `--registry`). Runs **before** migrate so a corrupted core never touches the DB. A **mismatch is fatal → rollback**; a missing reference warns (fatal with `--require-pin`); opt out with `--skip-pin-verify`.
|
|
17
|
-
4. **`npm run migrate`** — the core's `migrate.sh` applies additive `core_*` migrations (merged three-root: core + instance + modules).
|
|
17
|
+
4. **`npm run migrate`** — the core's `migrate.sh` applies additive `core_*` migrations (merged three-root: core + instance + modules). An instance whose `package.json` declares **no** `migrate` script — an adopted repo, since the adopt layer adds only the core dependency — runs the installed core's `scripts/migrate.sh` directly from the instance root instead, which is exactly what a scaffold's script runs; the pre-flight names it, so the dry run shows it (task [#1004274](https://example.com/builders#/task/1004274)). Before that fallback, such an instance failed here after the pin had moved and rolled back every time.
|
|
18
18
|
5. **materialize `.claude/`** — refresh skills/settings/hooks from the new core (`materializeClaude`), rewriting invocations to the `bongos` bin.
|
|
19
19
|
6. **restart** — `--service <unit>` or `--restart-cmd "<cmd>"`.
|
|
20
20
|
7. **verify** — installed version == target + the **mandatory post-restart health check** (task [#1002222](https://example.com/builders#/task/1002222)): a bump that restarts the service **requires `--health-url <url>`** (it can't report success without confirming the new core serves). Opt out explicitly with `--no-health-check`; a bump with no managed restart needs no URL (it reports the new core installed-but-unconfirmed). A still-failing health poll fails the bump (auto-rollback).
|
|
@@ -114,11 +114,14 @@ function readGauge(raw, nowEpochS, config = {}) {
|
|
|
114
114
|
|
|
115
115
|
const writtenAt = Number(raw.written_at);
|
|
116
116
|
if (!Number.isFinite(writtenAt)) return { known: false, reason: 'gauge file has no readable written_at' };
|
|
117
|
-
|
|
118
117
|
const ageS = nowEpochS - writtenAt;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// NO FILE-LEVEL STALENESS BAIL (task 1004178). There used to be one here, and
|
|
119
|
+
// it short-circuited before the per-window logic below could run — which made
|
|
120
|
+
// the "staleness is per window, not per file" rule directly underneath it a
|
|
121
|
+
// dead letter for exactly the files that needed it. An old file whose windows
|
|
122
|
+
// have all turned over is not unknown; it is a file describing windows that
|
|
123
|
+
// have ended. The windows decide, and when none of them can, the return at the
|
|
124
|
+
// bottom says so with the file's age.
|
|
122
125
|
// A clock that ran backwards is not evidence either.
|
|
123
126
|
if (ageS < -60) return { known: false, ageS, reason: 'gauge is dated in the future — clock skew, treating as unknown' };
|
|
124
127
|
|
|
@@ -129,6 +132,26 @@ function readGauge(raw, nowEpochS, config = {}) {
|
|
|
129
132
|
// on a fresh written_at forever — "unknown" quietly becoming "headroom".
|
|
130
133
|
// Files written before observed_at existed fall back to written_at, which is
|
|
131
134
|
// exactly what they meant.
|
|
135
|
+
// AN AGED-OUT READING WHOSE WINDOW HAS SINCE RESET IS NOT STALE — IT IS
|
|
136
|
+
// SUPERSEDED (task 1004178, found by the 1003907 proof run).
|
|
137
|
+
//
|
|
138
|
+
// WHY: an unattended host cannot refresh this file at all — the writer is a
|
|
139
|
+
// statusLine, which only renders in a terminal session. The evidence and the
|
|
140
|
+
// full account are in modules/autonomy/CLAUDE.md; what matters here is the
|
|
141
|
+
// consequence: without this, the runner holds 30 minutes in and never resumes.
|
|
142
|
+
//
|
|
143
|
+
// The narrow fact that rescues it: when `now` is past a window's own
|
|
144
|
+
// `resets_at`, the window that reading described has ENDED, and the current
|
|
145
|
+
// one began at that reset with zero used. That is what a reset IS, not a
|
|
146
|
+
// guess — which is why this does not breach "unknown is never headroom":
|
|
147
|
+
// nothing is being assumed about the observation, it is being RETIRED.
|
|
148
|
+
//
|
|
149
|
+
// What IS assumed is that nobody else spent the new window. That is why a
|
|
150
|
+
// derived window is MARKED, and why the caller must bound how much work it
|
|
151
|
+
// does on one. The true boundary is also unknowable — these windows roll from
|
|
152
|
+
// first use, not from a fixed clock — so `resetsAt` on a derived window is a
|
|
153
|
+
// lower bound, and the seven-day PACE check is skipped rather than run
|
|
154
|
+
// against a boundary we invented.
|
|
132
155
|
const win = (w) => {
|
|
133
156
|
const o = raw[w];
|
|
134
157
|
if (!o || typeof o !== 'object') return null;
|
|
@@ -136,8 +159,14 @@ function readGauge(raw, nowEpochS, config = {}) {
|
|
|
136
159
|
const resets = Number(o.resets_at);
|
|
137
160
|
if (!Number.isFinite(used) || !Number.isFinite(resets)) return null;
|
|
138
161
|
const observedAt = Number.isFinite(Number(o.observed_at)) ? Number(o.observed_at) : writtenAt;
|
|
139
|
-
if (nowEpochS - observedAt
|
|
140
|
-
|
|
162
|
+
if (nowEpochS - observedAt <= c.staleAfterS) {
|
|
163
|
+
return { used, resetsAt: resets, observedAt, ageS: nowEpochS - observedAt, derived: false };
|
|
164
|
+
}
|
|
165
|
+
// Aged out. Superseded ONLY if its window has actually turned over.
|
|
166
|
+
if (nowEpochS >= resets) {
|
|
167
|
+
return { used: 0, resetsAt: resets, observedAt: resets, ageS: nowEpochS - resets, derived: true };
|
|
168
|
+
}
|
|
169
|
+
return null; // old, and its window is still running — no longer evidence
|
|
141
170
|
};
|
|
142
171
|
const fiveHour = win('five_hour');
|
|
143
172
|
const sevenDay = win('seven_day');
|
|
@@ -145,7 +174,7 @@ function readGauge(raw, nowEpochS, config = {}) {
|
|
|
145
174
|
// Both windows are optional in the source ("may be absent"), but with neither
|
|
146
175
|
// there is nothing to decide on.
|
|
147
176
|
if (!fiveHour && !sevenDay) {
|
|
148
|
-
return { known: false, ageS, reason:
|
|
177
|
+
return { known: false, ageS, reason: `gauge is ${Math.round(ageS / 60)}m old and no window has turned over since — absent or aged out, which is unknown, not headroom` };
|
|
149
178
|
}
|
|
150
179
|
return { known: true, ageS, fiveHour, sevenDay };
|
|
151
180
|
}
|
|
@@ -188,7 +217,9 @@ function decideCapacity({ gauge, now, config = {} } = {}) {
|
|
|
188
217
|
return { go: false, unknown: false, sleepUntil: g.sevenDay.resetsAt, window: 'seven_day',
|
|
189
218
|
reason: `seven-day window spent (${g.sevenDay.used.toFixed(1)}%) — sleeping to its reset` };
|
|
190
219
|
}
|
|
191
|
-
|
|
220
|
+
// A DERIVED seven-day window carries a boundary we inferred, so pacing it
|
|
221
|
+
// would measure progress through a week we guessed at. Skipped, not faked.
|
|
222
|
+
if (!g.sevenDay.derived && g.sevenDay.used > pace.allowedPct) {
|
|
192
223
|
const until = whenPaceAllows(g.sevenDay, c);
|
|
193
224
|
// Never propose a wake in the past, and never past the reset itself.
|
|
194
225
|
const sleepUntil = Math.min(Math.max(until, now + 1), g.sevenDay.resetsAt);
|
|
@@ -209,7 +240,24 @@ function decideCapacity({ gauge, now, config = {} } = {}) {
|
|
|
209
240
|
const bits = [];
|
|
210
241
|
if (g.fiveHour) bits.push(`5h ${g.fiveHour.used.toFixed(1)}%`);
|
|
211
242
|
if (g.sevenDay) bits.push(`7d ${g.sevenDay.used.toFixed(1)}%`);
|
|
212
|
-
|
|
243
|
+
// A derived window is reported as such all the way out, so a caller can bound
|
|
244
|
+
// the work it does on one and a reader can see which kind of "go" this is.
|
|
245
|
+
const derived = !!((g.fiveHour && g.fiveHour.derived) || (g.sevenDay && g.sevenDay.derived));
|
|
246
|
+
const detail = derived
|
|
247
|
+
? "the last reading's window has since reset, so it was retired rather than trusted"
|
|
248
|
+
: `gauge ${Math.round(g.ageS)}s old`;
|
|
249
|
+
// `derivedSince` is the reset the derived baseline stands on. The caller needs
|
|
250
|
+
// it because a derived window cannot be paced: a SEVEN-day window will almost
|
|
251
|
+
// never turn over during a night, so its stale reading is dropped and the
|
|
252
|
+
// weekly pace check — the one the module's own notes call the thing that
|
|
253
|
+
// actually binds a heavy week — does not run at all. A stale weekly figure
|
|
254
|
+
// cannot stand in for it either: usage only grows, so an under-reported value
|
|
255
|
+
// makes the check MORE permissive, which is the wrong direction to be wrong in.
|
|
256
|
+
// Bounding the work done since `derivedSince` is what replaces that pacing.
|
|
257
|
+
const derivedSince = derived
|
|
258
|
+
? Math.min(...[g.fiveHour, g.sevenDay].filter((w) => w && w.derived).map((w) => w.observedAt))
|
|
259
|
+
: null;
|
|
260
|
+
return { go: true, unknown: false, sleepUntil: null, derived, derivedSince, reason: `headroom (${bits.join(', ')}; ${detail})` };
|
|
213
261
|
}
|
|
214
262
|
|
|
215
263
|
module.exports = {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The builders page — /roster, the in-project roster (task 1003441)
|
|
2
|
+
|
|
3
|
+
**The BUILDERS page landed next (task 1003441, goal 1000075)** — the in-project roster, at `/roster`. It is not a redesign but a MOVE: the Roll was a `#leaderboard` HASH VIEW on `index.html`, which is why the Community group read as one real page plus a link into the home page. Four things it settled:
|
|
4
|
+
|
|
5
|
+
- **The load-bearing part was not the markup.** `shell.js` `VIEWS`, `dom-utils.js` `HALL_VIEWS` and `hall-widgets.js` all declared `leaderboard`; the table, its pager, its sort lenses (`LB_SORT_HEADERS`, `sortedLeaderboardRows`) and the achievement laurels (`ACH_EMOJI`) lived in `hall-render.js`. All of it MOVED to `roster.js` rather than being copied, so Home carries no dead renderer. `roster.html` is the board archetype at people.html’s shape and reuses `people.css` for the identity cell — one definition, both rosters.
|
|
6
|
+
- **`lbState` and `leaderboardPlace()` STAYED on Home**, shrunk to `{ rows: [] }`. The Standing card still says "#N on the leaderboard", and it needs the same rows; the fetch stays too. Verified live: the card reads `#4 on the leaderboard` with the table gone.
|
|
7
|
+
- **The hash REDIRECTS, it does not alias.** `#achievements` could fold into Standing because Standing exists on that page; there is no host view for a Roll that left. So `VIEW_REDIRECTS` in `shell.js` sends `#leaderboard` to `/roster` via `location.replace`, guarded to the index — without it `viewFromHash` returns nothing and `route()` silently leaves the reader on Home, a dead link that looks like it worked.
|
|
8
|
+
- **It is deliberately UNGATED,** and `tests/hall_page_gate_map.mjs` spells that out rather than defaulting. Its one data source, `GET /leaderboard`, is deliberately unauthenticated and mirrors the public status surface; a page must not be gated tighter than the API it fronts, and it was already reachable to a stranger as a view on the ungated index. No visibility predicate is applied and that is correct — privacy suppresses cross-project surfaces only, so a `private` builder still appears on the roster of a project they build on (ADR 0225).
|
|
9
|
+
|
|
10
|
+
Pinned by `tests/hall_nav.mjs` (the Community group's middle id is `roster`) and `tests/hall_page_gate_map.mjs`. Measured through `roster.states.json` — whose `out` state renders the REAL page, since the page is ungated.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The catalog pages — modules and the repo atlas (task 1003472)
|
|
2
|
+
|
|
3
|
+
**The CATALOG pages landed next (task 1003472)** — modules and the repo atlas. Modules rides the oversight family's row (`oversight.css` before `modules.css`); the atlas is NOT a list page, so its scene stays and only the chrome around it moved. Four things it settled:
|
|
4
|
+
|
|
5
|
+
- **Modules are ROWS, and the description is why.** The task asked for rows to be tested against the cards rather than assumed, since modules was the one hall page already rendering its record's description. Against the real catalog **the description is what breaks the card**: it runs 120–1,200 characters, so the 280px auto-fill grid stood a 500px card beside a 120px one and the toggle — the row's one control — sat at a different height in every column. The row clamps the description to two lines (`.mod-row__desc`, lifted while the row's `<details class="mod-more">` is open) and the disclosure opens the facts the card never rendered. The page is ONE panel; the old "Contribute a module upstream" panel is gone — the submit door is a `.btn-ghost` on the row of the module it submits.
|
|
6
|
+
- **The switch is the family's `.ov-toggle`, and the state is never an accent fill.** Fourteen `button.mod-toggle[data-on="1"]` pills painted salmon were the loudest thing on the page and none of them was live. Enabled reads in ink, disabled in the faint ink, the accent on hover; a viewer below the floor (`rankLabel('metic')`) sees a `.fact-pill` state, not a dead control. Colour marks the exception: `upkeep undeclared` is the one warn pill; a maintained module's upkeep is a plain `.ov-fact`. The `.mod-chip` alias left the shell's pill list — modules takes `.fact-pill` by name.
|
|
7
|
+
- **The atlas keeps its nine colours and joins the shell everywhere else.** `--ab-sun … --ab-danger` stay fixed literals (ADR 0094; `tests/hall_catalog_world.mjs` pins them AND that nothing else in the sheet is a literal). The panel is the shell's `.scroll` — its private hairline and 4% accent wash are gone, which answers the "pane" question the task raised by the owner's standing `.scroll` decision (task 1003470) rather than reopening it. `.ab-modes` (a segmented box, the third toggle grammar) is the shell's `.kit-tabs` driven by `aria-selected`. The hub pulse went from 5.5s to **24s, the world's ambient floor**; the accent "live" chip, the glass hint pill and the two `--shadow-pop`s went with it.
|
|
8
|
+
- **The island index is the map's keyboard and phone path.** The scene is a hover surface: SVG `<g>`s with mouse listeners, **no focusable element**, and at 390px thirty islands in 360px with nothing under them. `drawIndex` renders every node of the current view as a dense `.ov-row` under a `kit-collapse` (`#ab-index`) whose title is a disclosure button (`aria-expanded`/`aria-controls`) laying the SAME `detailHTML` inline; hovering a row lights the island, and **the index opens itself under 640px (`matchMedia`), where the list is the read.**
|
|
9
|
+
|
|
10
|
+
Also gone, and the one rule in that list: `.ab-caption` takes the lede's `--measure`, never a fresh `90ch`. **Gotcha:** `gen-atlas.js` run from the MAIN checkout walks `.claude/worktrees/*` and emits thousands of context nodes — run it from a worktree, or copy a worktree's `atlas.json` over, before previewing the atlas there.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The collaboration pages — collab and the copy desk (tasks 1003471, 1003942, 1004067)
|
|
2
|
+
|
|
3
|
+
**The COLLABORATION pages landed next (task 1003471)** — collab and the copy desk. They ride the oversight family's row (`oversight.css`, linked **before** each page's own sheet) rather than a fourth archetype: **a page whose repeated thing is a titled row with facts and actions IS that archetype at its own density.** Four things it settled:
|
|
4
|
+
|
|
5
|
+
- **A help request has no title, so the row DECIDES one.** `collab-lib.js` `titleOf` makes the ask's first sentence the 15px/500 title and the rest the sub line — the asker's own words, nothing clipped; a first sentence too long to be a line keeps the whole ask together. The row id stays unshown on purpose: an id with no door is noise (the archetype's mono id is a goal's, which has a page).
|
|
6
|
+
- **"Who is asking" is a server join, not a client guess.** `lifecycle/help-requests.js` reads its two lists through `LIST_COLS`/`LIST_FROM` — two `LEFT JOIN builders` for the asker and the by-name addressee — while `COLS` stays bare for the UPDATE's `RETURNING`, which cannot reach a joined alias. The row renders the name as a door to `/builders/profile?id=N`, with the login and then the bare id as fallbacks, so an older server still names someone. **The row's fact says DIRECTION, not just the name (task 1003274, owner report):** `collab-lib.directionLine(req, dir)` writes "You asked Lars by name" on your own asks and "Lars asked you by name" on what was asked of you, and `dir` comes from WHICH SERVER READ produced the row (`/help-requests/mine` vs `/for-me`) rather than from comparing `requested_by` to a viewer id. That matters: the old `addressedTo(req, viewerId)` named the addressee only when the viewer WAS the addressee, so the asker reading their own list — the commonest case for that branch — got "asked of one person", and the owner reasonably concluded their ask to Lars had been misrouted. `addressedTo` is deleted, not deprecated; `directionLine` takes no viewer id, and a test pins its arity so the comparison cannot come back. The craft/by-name tonal split (ADR 0187 §3) travelled with it — "Lars asked artists" stays a call anyone of that craft may take. `directionFact` in `collab.js` splices the profile link over the person's name **with a replacer FUNCTION**, because a builder-controlled display name containing `$&` would otherwise be substituted by `String.prototype.replace`.
|
|
7
|
+
- **The age is a pill in the right column, and the test moved with it.** `tests/collab_page.mjs` pinned a 3px coloured `border-left` for the stale band; this world marks state with a pill, never a bar, so the pin now reads the four `.cb-age--*` colour rules (faint, soft, warn, danger). "Asked of you" reads in runs under group heads — by name first, then each craft — because a personal ask is the one you owe an answer.
|
|
8
|
+
- **The copy desk's state was OPACITY, and opacity dims the panel too.** `.cd-tag--flagged/--settled/--moved/--gone` differed by .6/.75/.85/1 and the same ran through nine more rules, which is why the page read muddy rather than quiet. State is the `.fact-pill` vocabulary now (target gone = warn; already flagged = open; looked at = ok; **untouched = the plain pill, because colour marks the EXCEPTION** in a list that is almost entirely work). The duplicates page on a ROW list, not a ledger, because each row carries its places; the two dialogs are the family `.ov-modal` on a native `<dialog>` (its own focus trap and Escape; **the `::backdrop` scrim is the sheet's one sanctioned literal**), with the hints moved OUT of the labels — **the modal's label voice uppercases whatever sits inside a `<label>`.**
|
|
9
|
+
|
|
10
|
+
Also gone, and the one token rule in that list: `--accent` is the FILL token and must not be spent on a link (`.cb-ctx` was).
|
|
11
|
+
|
|
12
|
+
**Collab became a THREE-BLOCK room of PEOPLE (task 1003942, owner direction: "way too text heavy, doesn't feel collaborative" — asked of you left, your asks right, every open ask below).** The top two ride the shell's `.view-zone--band`; the third is the wide `.cb-block--all`, fed by a new read, `GET /help-requests` (`help.listOpenAll`: every open ask in the project, newest first, any signed-in builder, **takes nothing from the caller** so C01's `?craft=` hole cannot reopen). Each block is condensed to the **newest three** with a "Show all N" `.btn-ghost` that flips `[data-extra]` rows (`collab-lib.splitShown` — the button's number and the rows it reveals come from one split; no re-render, no motion), and each head carries its count and the faces of who is asking. Every row is the family's `.ov-row--lead` with a face in the lead column. Four rules travel with it: **the face is decided by the lib** (`collab-lib.faceOf(req, dir)`: the asker incoming and on the board, the addressee outgoing, a lettered dashed disc for a craft ask so it never passes for a person; `httpUrl` is roster.js's allowlist, so a src the page did not vouch for renders as the initial); **the board speaks in the third person** (`boardLine`: "Alpha asked Beta by name", never "you"); **a by-name ask is the one lit thing** (`.cb-row--live` puts the accent INK on its direction line — the personal call, and nothing in the sheet spends the accent fill, test-pinned); and **the prose budget is one lede on the whole page** (the social panel's), with the "declining costs nothing" sentence said once at the foot. Inside the two half-width blocks the row's right column drops under the words and the date yields to the age pill; the band goes to one column under 1100px. The avatar rides the existing builders JOIN (`requested_by_avatar_url` / `needs_builder_avatar_url` in `LIST_COLS`). The sidebar Collab item took the count badge the same day — `count: 'help_requests'` on the nav item, `count: n` on `helpRequestsNeed` — exactly the adoption task 1003737's mechanism promised; the three collab needs' stale `/#/collab` hrefs became `/collab` with it. **The harness needs `help-requests.json`** for the board block, beside the two it had.
|
|
13
|
+
|
|
14
|
+
**A COLLAB PILL MARKS A ROW WORSE THAN ITS ROOM, not every row (task 1004067, from idea 1001065).** The report was fourteen "waiting over a week" pills on fourteen rows. Task 1004041 fixed the WORDS (`collab-lib.ageLabel` gives each row its real age) and dropped the ring from the two calm bands; what it could not fix from inside a single row is a block where every row is genuinely past a week — there the condition is correct on all fourteen, and fourteen correct alarms are still not a signal. **The instrument was wrong, not the threshold.** So `paintBlock` computes `collab-lib.roomBaseline(list, nowMs)` — the severest band wearing a pill (severity >= 2) that covers >= `BASELINE_SHARE` (0.7) of at least `BASELINE_MIN_ROWS` (4) rows — writes `roomAgeLine` into the head's `#cb-<key>-age`, and `ageChip` adds `.cb-age--quiet` to every row at or below it. Four things travel with it: **the baseline is over the WHOLE list, not the shown three**, or pressing "Show all" would re-alarm the rows it had just quieted; **a room with no dominant band is untouched**, which is what keeps this inert on the board as it usually looks; **a calm room announces nothing** (an all-`fresh` block has no alarm to move to the head, and putting one there would invent the noise this removes); and **the per-row words are never quieted**, only the treatment — an old ask stays visibly aging rather than silently dead, which is the criterion the bands exist to satisfy. Run, not grepped, in `tests/collab_page.mjs`.
|