@bongos/core 1.19.616 → 1.19.618
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 +51 -21
- package/.claude/skills/backlog-review/SKILL.md +175 -0
- package/docs/adr/0268-the-constitution-comes-from-two-roots.md +132 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +36 -33
- package/docs/copy-registry.json +61 -34
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/government/board.js +58 -0
- package/modules/government/config.js +34 -9
- package/modules/hall-ui/public/government.js +54 -0
- package/modules/hall-ui/public/oversight.css +25 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/backlog-review.js +399 -0
- package/src/module-api.js +27 -1
- package/tests/backlog_review.mjs +183 -0
- package/tests/government_config_roots.mjs +159 -0
- package/tests/government_constitution_divergence.mjs +123 -0
- package/tests/module_api.mjs +1 -0
package/.bongos-core.json
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.618",
|
|
6
|
+
"core_contract": "1.19.618",
|
|
7
|
+
"source_commit": "1e344743e60d4921a3e5d73b32817388928e62a0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-09T06:20:10.684Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 462,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2085,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2571,
|
|
20
|
+
"tree_sha256": "725579642848cf49184292a5271ac418e2ea288cac2beaaa80b49468625abc14",
|
|
21
21
|
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
24
|
+
"mode": "0000644",
|
|
25
|
+
"sha256": "ce45a242487fa6d10794280958fea47f8a23e2618551f629abe38054d467a59c"
|
|
26
|
+
},
|
|
22
27
|
{
|
|
23
28
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
24
29
|
"mode": "0000644",
|
|
@@ -1854,10 +1859,15 @@
|
|
|
1854
1859
|
"mode": "0000644",
|
|
1855
1860
|
"sha256": "32a1e0c4fe9423008caf491e5d3594aa4a07874a46a047a86e5eb321baef773a"
|
|
1856
1861
|
},
|
|
1862
|
+
{
|
|
1863
|
+
"path": "docs/adr/0268-the-constitution-comes-from-two-roots.md",
|
|
1864
|
+
"mode": "0000644",
|
|
1865
|
+
"sha256": "cec61252a02d5da416c77088dbf2adf99c138cb10b654fa9fef949a0848a7964"
|
|
1866
|
+
},
|
|
1857
1867
|
{
|
|
1858
1868
|
"path": "docs/adr/README.md",
|
|
1859
1869
|
"mode": "0000644",
|
|
1860
|
-
"sha256": "
|
|
1870
|
+
"sha256": "7ab74c04d47b89328aedfe3fca3f2fd18c049c2fe5aa6c98bc1072d26bc12c68"
|
|
1861
1871
|
},
|
|
1862
1872
|
{
|
|
1863
1873
|
"path": "docs/api-reference.md",
|
|
@@ -1892,12 +1902,12 @@
|
|
|
1892
1902
|
{
|
|
1893
1903
|
"path": "docs/copy-inventory.md",
|
|
1894
1904
|
"mode": "0000644",
|
|
1895
|
-
"sha256": "
|
|
1905
|
+
"sha256": "0e4c54e6bd6d76fdd7853b19c343b747fabfc2f59ee8baab74cbaebc6064aa1e"
|
|
1896
1906
|
},
|
|
1897
1907
|
{
|
|
1898
1908
|
"path": "docs/copy-registry.json",
|
|
1899
1909
|
"mode": "0000644",
|
|
1900
|
-
"sha256": "
|
|
1910
|
+
"sha256": "4357446e8f95c430791743a43f893140d862ab7dd5453f6a42791b194956d61c"
|
|
1901
1911
|
},
|
|
1902
1912
|
{
|
|
1903
1913
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2732,7 +2742,7 @@
|
|
|
2732
2742
|
{
|
|
2733
2743
|
"path": "docs/file-map.md",
|
|
2734
2744
|
"mode": "0000644",
|
|
2735
|
-
"sha256": "
|
|
2745
|
+
"sha256": "0cec3b144d8cca893b49c1d08bdacf37220dc1b670fd771656dda659bae16edc"
|
|
2736
2746
|
},
|
|
2737
2747
|
{
|
|
2738
2748
|
"path": "docs/handoff-template.md",
|
|
@@ -2742,7 +2752,7 @@
|
|
|
2742
2752
|
{
|
|
2743
2753
|
"path": "docs/module-api-changelog.md",
|
|
2744
2754
|
"mode": "0000644",
|
|
2745
|
-
"sha256": "
|
|
2755
|
+
"sha256": "ec11a2ed9d50d4558aee3aca03f217dc0dde1f717859eb7a77c706535324fd32"
|
|
2746
2756
|
},
|
|
2747
2757
|
{
|
|
2748
2758
|
"path": "docs/modules-contract.md",
|
|
@@ -4397,7 +4407,7 @@
|
|
|
4397
4407
|
{
|
|
4398
4408
|
"path": "modules/government/board.js",
|
|
4399
4409
|
"mode": "0000644",
|
|
4400
|
-
"sha256": "
|
|
4410
|
+
"sha256": "68bf53ab31ec3f21359665eea42127f1691b8dc17b59100c65cc1f56a04ad6a7"
|
|
4401
4411
|
},
|
|
4402
4412
|
{
|
|
4403
4413
|
"path": "modules/government/catalog.js",
|
|
@@ -4407,7 +4417,7 @@
|
|
|
4407
4417
|
{
|
|
4408
4418
|
"path": "modules/government/config.js",
|
|
4409
4419
|
"mode": "0000644",
|
|
4410
|
-
"sha256": "
|
|
4420
|
+
"sha256": "8edccad7dc69a95503d9e0d1de7795fd6082c8122e0695d1f018f49b37051fed"
|
|
4411
4421
|
},
|
|
4412
4422
|
{
|
|
4413
4423
|
"path": "modules/government/db.js",
|
|
@@ -4922,7 +4932,7 @@
|
|
|
4922
4932
|
{
|
|
4923
4933
|
"path": "modules/hall-ui/public/government.js",
|
|
4924
4934
|
"mode": "0000644",
|
|
4925
|
-
"sha256": "
|
|
4935
|
+
"sha256": "6333b05761e5bf3215fe3c19ae98b25ebdac8cf0761df28fbc4933a5317ce5fa"
|
|
4926
4936
|
},
|
|
4927
4937
|
{
|
|
4928
4938
|
"path": "modules/hall-ui/public/government.states.json",
|
|
@@ -5052,7 +5062,7 @@
|
|
|
5052
5062
|
{
|
|
5053
5063
|
"path": "modules/hall-ui/public/oversight.css",
|
|
5054
5064
|
"mode": "0000644",
|
|
5055
|
-
"sha256": "
|
|
5065
|
+
"sha256": "a199baf1c11bdbe6d30931253956e0c9a9709ae3f880af77b2a6737212fe0984"
|
|
5056
5066
|
},
|
|
5057
5067
|
{
|
|
5058
5068
|
"path": "modules/hall-ui/public/pair.css",
|
|
@@ -7642,12 +7652,12 @@
|
|
|
7642
7652
|
{
|
|
7643
7653
|
"path": "package-lock.json",
|
|
7644
7654
|
"mode": "0000644",
|
|
7645
|
-
"sha256": "
|
|
7655
|
+
"sha256": "44a869111a69105fd5f1778cb749ad2c40a067db90fee6e6bbd19fcd76acd2ab"
|
|
7646
7656
|
},
|
|
7647
7657
|
{
|
|
7648
7658
|
"path": "package.json",
|
|
7649
7659
|
"mode": "0000644",
|
|
7650
|
-
"sha256": "
|
|
7660
|
+
"sha256": "b974dffa8dcde89358258e710e0f0b2c9fbd560ff5ba4c505cc57ced1809ab0e"
|
|
7651
7661
|
},
|
|
7652
7662
|
{
|
|
7653
7663
|
"path": "public-docs/index.html",
|
|
@@ -7794,6 +7804,11 @@
|
|
|
7794
7804
|
"mode": "0000644",
|
|
7795
7805
|
"sha256": "a9f1064fbb683297e775c455ef65249cf809018a38a5aa2f975a62cbba16cd9c"
|
|
7796
7806
|
},
|
|
7807
|
+
{
|
|
7808
|
+
"path": "scripts/gds/backlog-review.js",
|
|
7809
|
+
"mode": "0000644",
|
|
7810
|
+
"sha256": "4646bd6da8be2d2c40c1063bba47f5fdce0f2f63143e26fc16e36ea941adbc88"
|
|
7811
|
+
},
|
|
7797
7812
|
{
|
|
7798
7813
|
"path": "scripts/gds/backup.js",
|
|
7799
7814
|
"mode": "0000644",
|
|
@@ -9372,7 +9387,7 @@
|
|
|
9372
9387
|
{
|
|
9373
9388
|
"path": "src/module-api.js",
|
|
9374
9389
|
"mode": "0000644",
|
|
9375
|
-
"sha256": "
|
|
9390
|
+
"sha256": "8d4f44cdde9eecac4f1d8294dcd8d1f1283d536c2adab2d9e49b74c8891a40e8"
|
|
9376
9391
|
},
|
|
9377
9392
|
{
|
|
9378
9393
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9679,6 +9694,11 @@
|
|
|
9679
9694
|
"mode": "0000644",
|
|
9680
9695
|
"sha256": "25f45e6a0f79e6e6299b062dd89306f898fc1ed8cefbf4fa16b76c9c511f0548"
|
|
9681
9696
|
},
|
|
9697
|
+
{
|
|
9698
|
+
"path": "tests/backlog_review.mjs",
|
|
9699
|
+
"mode": "0000644",
|
|
9700
|
+
"sha256": "788f3b3faba5f0568a251d3551c8776e2f383d59c8c2ed0728ef4c9af9a208c7"
|
|
9701
|
+
},
|
|
9682
9702
|
{
|
|
9683
9703
|
"path": "tests/backup_cli.mjs",
|
|
9684
9704
|
"mode": "0000644",
|
|
@@ -10689,6 +10709,16 @@
|
|
|
10689
10709
|
"mode": "0000644",
|
|
10690
10710
|
"sha256": "a8e575d151aca92789ffa9d3a49f77aa2579fa285374b2c1f5a65b12ac82163d"
|
|
10691
10711
|
},
|
|
10712
|
+
{
|
|
10713
|
+
"path": "tests/government_config_roots.mjs",
|
|
10714
|
+
"mode": "0000644",
|
|
10715
|
+
"sha256": "d3060b7302f17a1478588ee333ba9e2f4deea23d5896363b9bd42e30edb12c88"
|
|
10716
|
+
},
|
|
10717
|
+
{
|
|
10718
|
+
"path": "tests/government_constitution_divergence.mjs",
|
|
10719
|
+
"mode": "0000644",
|
|
10720
|
+
"sha256": "280bc5906ac8075495cd3b1fe10e4082291cf4497b166f6acbdec291a8fd6698"
|
|
10721
|
+
},
|
|
10692
10722
|
{
|
|
10693
10723
|
"path": "tests/government_constitution_view.mjs",
|
|
10694
10724
|
"mode": "0000644",
|
|
@@ -11442,7 +11472,7 @@
|
|
|
11442
11472
|
{
|
|
11443
11473
|
"path": "tests/module_api.mjs",
|
|
11444
11474
|
"mode": "0000644",
|
|
11445
|
-
"sha256": "
|
|
11475
|
+
"sha256": "cc6fb31e06f0b2accf46f1595f9548492698c88ff56c4d9f4f43c86fce6816a3"
|
|
11446
11476
|
},
|
|
11447
11477
|
{
|
|
11448
11478
|
"path": "tests/module_api_lazy.mjs",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backlog-review
|
|
3
|
+
description: >-
|
|
4
|
+
Daily walk of status='backlog' — the pre-workable state a human must say go on.
|
|
5
|
+
Separates rows waiting on a PERSON (walked: promote, kill, water) from rows
|
|
6
|
+
waiting on a TRIGGER, and surfaces rows stranded behind an abandoned dependency.
|
|
7
|
+
Triggers on "/backlog-review", "review the backlog", "what needs a nod", "walk
|
|
8
|
+
the backlog", "what's waiting on me to approve". Metic+ only.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are running the backlog-review session. Tasks at `status='backlog'` get
|
|
12
|
+
walked and verdicted, so work nobody has said go on doesn't sit un-nodded
|
|
13
|
+
forever.
|
|
14
|
+
|
|
15
|
+
## Why this exists (task 1003746)
|
|
16
|
+
|
|
17
|
+
`backlog` is the **pre-workable** state: a task lands there when the system will
|
|
18
|
+
not put it in the claimable queue without a human saying go. `routing.js` says it
|
|
19
|
+
plainly — *"'backlog' means a human still says go"* — and [ADR 0234](../../../docs/adr/<redacted>.md) §2
|
|
20
|
+
records why: a feature/cleanup/refactor filing *"reshape[s] what gets built and a
|
|
21
|
+
filer should not be able to set the queue's agenda alone."*
|
|
22
|
+
|
|
23
|
+
Every **other** queue in the methodology already has a walkable cadence:
|
|
24
|
+
`/idea-triage` for `idea_inbox`, `/blocker-review` for blockers, `/bug-triage`
|
|
25
|
+
for `kind=bug`, `/goal-review` for criteria. The backlog had none. Its only pull
|
|
26
|
+
surfaces were:
|
|
27
|
+
|
|
28
|
+
- the **nod queue** (`GET /inbox/awaiting-nod`), scoped `WHERE origin='route'` —
|
|
29
|
+
so it sees only filings that arrived through the idea inbox, and
|
|
30
|
+
- the **30-day rot timer** (`GET /inbox/rotting`, [ADR 0232](../../../docs/adr/<redacted>.md)).
|
|
31
|
+
|
|
32
|
+
Neither is a review of the backlog. This skill is that missing walk.
|
|
33
|
+
|
|
34
|
+
## Rank gate — Metic+ only
|
|
35
|
+
|
|
36
|
+
**Gated to Metic and above (Metic, Archon); not Xenos or Thetes.** Promoting a
|
|
37
|
+
backlog task puts it in the claimable queue for the whole team and, if its reward
|
|
38
|
+
is unset, prices it — `task.promote` and `task.abandon` are both floor-`metic`
|
|
39
|
+
permissions (`modules/government/catalog.js`). A sub-Metic can still **file**
|
|
40
|
+
work (`POST /tasks`, `capture.js`, Discord `#bugs`) — that path stays open; they
|
|
41
|
+
just can't say go on it.
|
|
42
|
+
|
|
43
|
+
**Enforce the gate at the top of the session, before reading the queue:**
|
|
44
|
+
|
|
45
|
+
1. Call `GET /api/gds/me` and read `builder.rank`. Compare case-insensitively.
|
|
46
|
+
2. **If `rank` lowercases to `xenos` or `thetes`** → stop immediately. Tell the
|
|
47
|
+
builder: *"Backlog review is a Metic+ operation — promoting a task puts it in
|
|
48
|
+
the claimable queue for everyone and sets its price. You can still file work;
|
|
49
|
+
ask an Archon to promote you if you need to nod it through."* Do not list the
|
|
50
|
+
queue, do not propose verdicts.
|
|
51
|
+
3. **If `rank` lowercases to `metic` or `archon`** → proceed below.
|
|
52
|
+
4. **If `rank` is absent** (pre-rank deployment) → treat the session as
|
|
53
|
+
available, the same pre-rank passthrough `/idea-triage` uses.
|
|
54
|
+
|
|
55
|
+
> Belt-and-braces: the server already rejects `POST /tasks/:id/promote` and
|
|
56
|
+
> `/abandon` with `403 permission_forbidden` for a sub-Metic caller. This check
|
|
57
|
+
> stops one early instead of after a wall of 403s. Markdown never grants
|
|
58
|
+
> authority ([`docs/canonical-permissions.md`](../../../docs/canonical-permissions.md), ADR 0016).
|
|
59
|
+
|
|
60
|
+
## The four buckets — and why only two get walked
|
|
61
|
+
|
|
62
|
+
**This is the whole point of the skill.** One read partitions the backlog into
|
|
63
|
+
four populations, and conflating them is the trap: *a row waiting on a TRIGGER is
|
|
64
|
+
not a row waiting on a PERSON.* Asking a human to verdict the former burns the
|
|
65
|
+
cadence on rows it cannot move.
|
|
66
|
+
|
|
67
|
+
| Bucket | What it is | Walked? |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| **stranded** | dep-gated, but a dependency is `abandoned` — that edge can never satisfy, so the auto-promote trigger will **never** fire | **first** — it only gets worse if left |
|
|
70
|
+
| **awaiting a nod** | no unsatisfied dependency; nothing gates it but a human | **yes — the review subject** |
|
|
71
|
+
| **waiting on a dependency** | an unsatisfied but still-live dep; shipping it auto-promotes the row (migration 163) | no — counted only |
|
|
72
|
+
| **spikes** | `kind='spike'` lives at backlog **by convention** (migration 020: *"Spikes stay in backlog by convention"*) | no — counted only |
|
|
73
|
+
|
|
74
|
+
A promote on a healthy dep-gated row would race the trigger and put work in the
|
|
75
|
+
queue whose prerequisites aren't done. Don't.
|
|
76
|
+
|
|
77
|
+
## The verbs are status-derived — do NOT offer prune
|
|
78
|
+
|
|
79
|
+
The rot card publishes four task verbs (prioritise / prune / kill / water), but
|
|
80
|
+
two are **status-conditional at the route**:
|
|
81
|
+
|
|
82
|
+
- `POST /tasks/:id/promote` accepts only `backlog | blocked | abandoned`
|
|
83
|
+
- `POST /tasks/:id/demote` accepts only `ready` (`routes/tasks.js`: `if (task.status !== 'ready') return res.fail('cannot_demote', 409)`)
|
|
84
|
+
|
|
85
|
+
So on a **backlog** row, "prune" is a guaranteed `409 cannot_demote` — it is
|
|
86
|
+
already at the bottom. This walk offers:
|
|
87
|
+
|
|
88
|
+
| Verb | Route | Meaning |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| **promote** | `POST /tasks/:id/promote` | the nod — backlog → ready, claimable now |
|
|
91
|
+
| **kill** | `POST /tasks/:id/abandon` | it is not going to happen (restorable — ADR 0152) |
|
|
92
|
+
| **water** | `POST /tasks/:id/water` | leave it; reset the rot clock. Changes no status, destroys nothing — its route is `requireBuilder` plus an ownership-or-atom check (creator, or the see-all-rot atom), which every Metic+ caller here holds per ADR 0157 |
|
|
93
|
+
| **skip** | — | no write; it surfaces again next review |
|
|
94
|
+
|
|
95
|
+
Every one is an **existing** route, so this skill adds no new authority — the
|
|
96
|
+
[ADR 0232](../../../docs/adr/<redacted>.md)
|
|
97
|
+
precedent (*"a verb that reuses a route also reuses its permission"*).
|
|
98
|
+
|
|
99
|
+
## How to use
|
|
100
|
+
|
|
101
|
+
**Step 1 — run the walk.**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
node scripts/gds/backlog-review.js
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It reads `GET /tasks?status=backlog&include=deps&limit=1000`, prints the four
|
|
108
|
+
bucket counts, then walks stranded rows first and the nod queue after — each
|
|
109
|
+
**grouped goal-by-goal** under a header naming the goal. Flags: `--limit N`
|
|
110
|
+
(default 20 per sitting), `--all`, `--goal ID`.
|
|
111
|
+
|
|
112
|
+
**Why goal-by-goal matters to how you prompt.** The nod is mostly a *scope*
|
|
113
|
+
judgment — "does this goal want this work next" — and that is one decision per
|
|
114
|
+
goal, not per row. So read the goal header, then offer its rows together; you
|
|
115
|
+
will often give the same verdict to three in a row. Within a goal the order is
|
|
116
|
+
oldest-first, and goals themselves are ordered by their own oldest waiting row,
|
|
117
|
+
so the longest-ignored goal comes up first and nothing hides behind a busy one.
|
|
118
|
+
`--goal ID` narrows to a single goal; grouping is what organises the *whole*
|
|
119
|
+
queue.
|
|
120
|
+
|
|
121
|
+
> The `include=deps` parameter is load-bearing — it is what returns `blocked_by[]`
|
|
122
|
+
> with a per-edge `satisfied` flag, and without it every row looks like it's
|
|
123
|
+
> waiting on a person. `limit` is capped at 1000 by the route.
|
|
124
|
+
|
|
125
|
+
**Step 2 — verdict each row.** For each, the walk shows title, description, goal,
|
|
126
|
+
kind, estimate, reward, and filing date, then asks:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
[p]romote (make claimable) / [k]ill / [w]ater (leave it, reset the clock) / [s]kip / [q]uit
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Frame the question around the **outcome**, not the mechanics — the prompter is
|
|
133
|
+
often the owner, who is not a software engineer:
|
|
134
|
+
|
|
135
|
+
> #1003678 "Goals have no intentionality record" — filed 2026-09-07, ~2h,
|
|
136
|
+
> goal 1000089. Say go, drop it, or leave it for now?
|
|
137
|
+
|
|
138
|
+
**Step 3 — watch the price on a promote.** If `credits_reward` is 0/NULL, promote
|
|
139
|
+
**auto-assigns** a capped suggestion rather than refusing (ADR 0096 + the task
|
|
140
|
+
1673 amendment). The walk echoes `assigned_credits_reward` — read it out, because
|
|
141
|
+
that number is what the task now pays.
|
|
142
|
+
|
|
143
|
+
**Step 4 — handle a stranded row deliberately.** A stranded row prints its dead
|
|
144
|
+
edges and the two ways out:
|
|
145
|
+
|
|
146
|
+
- **drop the dead edge** — `DELETE /api/bongos/dependencies` with
|
|
147
|
+
`{"from_kind":"task","from_id":<this>,"to_kind":"task","to_id":<dead>}`, then
|
|
148
|
+
the row becomes a normal nod decision; or
|
|
149
|
+
- **restore the dependency** — `POST /api/bongos/tasks/<dead>/promote` returns an
|
|
150
|
+
abandoned task to backlog (ADR 0152).
|
|
151
|
+
|
|
152
|
+
Never just promote a stranded row past its dead edge without one of those — the
|
|
153
|
+
dependency was declared for a reason, and dropping it silently is the decision
|
|
154
|
+
being made by accident.
|
|
155
|
+
|
|
156
|
+
**Step 5 — close out.** The walk prints a summary and how many rows still await a
|
|
157
|
+
human. Note the counts in the session log. If the same rows get skipped three
|
|
158
|
+
reviews running, they are telling you something: either kill them, or the goal
|
|
159
|
+
they sit in has gone quiet and belongs in `/goal-review`.
|
|
160
|
+
|
|
161
|
+
## Cadence
|
|
162
|
+
|
|
163
|
+
Run it **once a day**, like `/idea-triage` and `/blocker-review` — it is a manual
|
|
164
|
+
slash command, not a scheduled routine. Deferring a row is fine; the cadence is
|
|
165
|
+
the discipline. If a day passes without it, the queue quietly grows.
|
|
166
|
+
|
|
167
|
+
## Non-goals
|
|
168
|
+
|
|
169
|
+
- **Not a re-prioritiser.** Reordering what's already `ready` is
|
|
170
|
+
`/priority-session`. This skill only decides whether a row becomes claimable.
|
|
171
|
+
- **Not dependency surgery.** Editing the dep graph beyond dropping one dead edge
|
|
172
|
+
belongs in its own task — the graph is load-bearing (ADR 0015).
|
|
173
|
+
- **Not a duplicate-folder.** Merging near-duplicate reports is `/bug-triage`.
|
|
174
|
+
- **Not a promote-everything button.** A backlog that empties every day means the
|
|
175
|
+
nod has stopped being a decision, which is the gate ADR 0234 §2 exists to keep.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# 0268 — The constitution comes from two roots, and a decision that did not take must say so
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-09
|
|
5
|
+
- **Tasks:** [#1003739](https://cloudbongos.com/builders#/task/1003739) (goal [#1000111](https://cloudbongos.com/builders#/goal/1000111) — Government). Found while shipping [#1003733](https://cloudbongos.com/builders#/task/1003733) ([ADR 0267](<redacted>.md)).
|
|
6
|
+
- **Builds on:** [ADR 0108](<redacted>.md) §1 (the two configurable roots), [ADR 0175](<redacted>.md) §7 (a passed amendment becomes the live constitution), [ADR 0083](<redacted>.md) (a module reaches core only through the doorway).
|
|
7
|
+
|
|
8
|
+
## 1. What happened
|
|
9
|
+
|
|
10
|
+
On 2026-08-25 cloudbongos.com's board ratified a constitutional amendment:
|
|
11
|
+
`rank:metic+` · `consent` · a 1440-minute sitting. It passed, it was recorded,
|
|
12
|
+
the proposer was paid the ratification karma — and **the constitution never
|
|
13
|
+
changed.** Two weeks later `GET /government/constitution` still answered the
|
|
14
|
+
day-one monarchy, with the passed amendment sitting in its own `history` array
|
|
15
|
+
directly beneath the `board` block that contradicted it.
|
|
16
|
+
|
|
17
|
+
Every Board Room sitting in those two weeks was decided under a rule the board
|
|
18
|
+
had voted to replace. It also quietly un-shipped [ADR 0191](<redacted>.md)
|
|
19
|
+
(majority) and would have un-shipped ADR 0267 (unanimity) the same way: both
|
|
20
|
+
shipped as code, and neither could reach anybody.
|
|
21
|
+
|
|
22
|
+
Nobody noticed, because nothing was broken enough to notice. No error, no failed
|
|
23
|
+
write, no missing row.
|
|
24
|
+
|
|
25
|
+
## 2. The cause: one root where there are two
|
|
26
|
+
|
|
27
|
+
`modules/government/config.js` resolved both of its files from one place:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
31
|
+
const NEUTRAL_PATH = path.join(ROOT, 'config', 'government.neutral.json');
|
|
32
|
+
const INSTANCE_PATH = path.join(ROOT, 'config', 'government.json');
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The comment above it said the two roots coincided *today* and that the instance
|
|
36
|
+
path would route through `resolveInstanceRoot()` when ADR 0108's split landed.
|
|
37
|
+
**The split had landed.** cloudbongos.com is a standalone instance: its systemd
|
|
38
|
+
unit runs `node_modules/@cloudbongos/core`'s server with
|
|
39
|
+
`WorkingDirectory=<instance repo>`, so at runtime `__dirname` is *inside the core
|
|
40
|
+
package*. Which makes:
|
|
41
|
+
|
|
42
|
+
| file | resolved to | correct? |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `government.neutral.json` | `…/node_modules/@cloudbongos/core/config/` | ✅ — it really is core content |
|
|
45
|
+
| `government.json` | `…/node_modules/@cloudbongos/core/config/` | ❌ — the host's is at `<instance>/config/` |
|
|
46
|
+
|
|
47
|
+
So two things were true at once, and each hid the other:
|
|
48
|
+
|
|
49
|
+
1. **The host's own constitution was never read.** An instance authoring
|
|
50
|
+
`config/government.json` — the documented way to set one — was ignored.
|
|
51
|
+
2. **`applyBoardAmendment` wrote the ratified amendment into `node_modules`,**
|
|
52
|
+
where it worked until the next core upgrade deleted it. The nightly upgrade
|
|
53
|
+
sweep (ADR 0161 cuts a release per merge) did exactly that, within a day.
|
|
54
|
+
|
|
55
|
+
**Nothing threw, and nothing could have.** Both paths exist, both are writable,
|
|
56
|
+
and an absent instance pack is a legal state — a vanilla instance has none — so
|
|
57
|
+
the read fell through to the neutral monarchy precisely as designed. This class
|
|
58
|
+
of bug has no error to catch. `src/branding.js` had it right the whole time and
|
|
59
|
+
was three files away: neutral from `resolveCoreRoot()`, instance from
|
|
60
|
+
`resolveInstanceRoot()`.
|
|
61
|
+
|
|
62
|
+
## 3. The fix, and the doorway it needed
|
|
63
|
+
|
|
64
|
+
`NEUTRAL_PATH` now resolves from the core root and `INSTANCE_PATH` from the
|
|
65
|
+
instance root, reached through `src/module-api` — because ADR 0083 forbids a
|
|
66
|
+
module requiring a core internal, and until now the doorway exposed neither
|
|
67
|
+
resolver. That absence is *why* the module re-derived a root from `__dirname`:
|
|
68
|
+
the only tool it had was the wrong one. Both are now exported, with the failure
|
|
69
|
+
above written into the export comment.
|
|
70
|
+
|
|
71
|
+
Two smaller decisions travel with it:
|
|
72
|
+
|
|
73
|
+
- **Resolved at load, not per call.** `resolveInstanceRoot()` falls back to
|
|
74
|
+
`process.cwd()`, so resolving lazily would let a `chdir` mid-process move the
|
|
75
|
+
constitution — a worse failure than the one being fixed.
|
|
76
|
+
- **`applyBoardAmendment` creates the directory.** The write now targets a
|
|
77
|
+
directory the core does not own, and an instance that has never been branded
|
|
78
|
+
has no `config/` of its own. A ratified amendment lost to `ENOENT` is the same
|
|
79
|
+
failure in a different coat.
|
|
80
|
+
|
|
81
|
+
The single-checkout case is byte-identical, and a test asserts that rather than
|
|
82
|
+
the prose claiming it.
|
|
83
|
+
|
|
84
|
+
## 4. The detector, because the fix alone is not the lesson
|
|
85
|
+
|
|
86
|
+
Fixing the path fixes *this* cause. The thing worth keeping is that **the API
|
|
87
|
+
held both facts in one payload for two weeks and never compared them.**
|
|
88
|
+
|
|
89
|
+
`constitutionView` now carries a `divergence` block: the newest **passed**
|
|
90
|
+
amendment's proposal, sanitized the way an apply would have written it, compared
|
|
91
|
+
field by field against the constitution actually in force. Null when they agree.
|
|
92
|
+
The hall renders it above the dials — in words, not config keys — as *"the board
|
|
93
|
+
ratified a change that is not in force."*
|
|
94
|
+
|
|
95
|
+
Three properties, each chosen against a plausible alternative:
|
|
96
|
+
|
|
97
|
+
- **It reports; it never heals.** Re-applying automatically was refused: the
|
|
98
|
+
instance pack is *also* the file a human editing the constitution touches (ADR
|
|
99
|
+
0175 §7), so a silent re-apply would revert a legitimate hand edit with no way
|
|
100
|
+
to tell the two apart — trading a visible wrong constitution for an unstoppable
|
|
101
|
+
one. Naming the divergence is what those two weeks actually lacked.
|
|
102
|
+
- **A returned amendment is never compared.** That is the whole meaning of
|
|
103
|
+
*returned*; comparing against it would fire on every rejected proposal, i.e. on
|
|
104
|
+
the system working.
|
|
105
|
+
- **It compares the SANITIZED proposal.** Comparing the raw one would report a
|
|
106
|
+
phantom divergence on any field the sanitizer legitimately normalises — and a
|
|
107
|
+
warning that can never be cleared is a warning people learn to scroll past.
|
|
108
|
+
|
|
109
|
+
## 5. What this does not do
|
|
110
|
+
|
|
111
|
+
**It does not put the 2026-08-25 amendment back in force.** That is a board act,
|
|
112
|
+
not a deploy: once this ships, cloudbongos.com's Constitution tab will *say* the
|
|
113
|
+
amendment is not in force, and the remedy is to put it back to the board (now
|
|
114
|
+
that a pass will stick) or for an operator to author the instance's own
|
|
115
|
+
`config/government.json`. Choosing between those is the owner's, not this task's.
|
|
116
|
+
|
|
117
|
+
**It fixes one module, and the sweep for siblings was run rather than promised.**
|
|
118
|
+
Every other module deriving a root from `__dirname` was checked, and all but one
|
|
119
|
+
are reading genuine CORE content — `scripts/`, `infra/`, the UI kit — which the
|
|
120
|
+
core root is the *right* answer for. The exception is
|
|
121
|
+
`modules/security/artifact-scan/quarantine.js`, whose "a quarantine site must not
|
|
122
|
+
be inside the repository" refusal resolves to the core package on a standalone
|
|
123
|
+
instance and so does not refuse a site inside the instance repo itself. That is a
|
|
124
|
+
real, narrow weakening of a security guard; it is filed as
|
|
125
|
+
[#1003745](https://cloudbongos.com/builders#/task/1003745) on the Security area
|
|
126
|
+
rather than absorbed here, and the doorway exports this ADR adds are what its fix
|
|
127
|
+
will use.
|
|
128
|
+
|
|
129
|
+
**The rule to carry forward:** a `__dirname`-derived root is correct for reading
|
|
130
|
+
what the core ships and wrong for reading what the host owns, and on a
|
|
131
|
+
single-checkout instance the two are indistinguishable — which is why this kind
|
|
132
|
+
of bug is always found in production and never in a test that runs at home.
|
package/docs/adr/README.md
CHANGED
|
@@ -359,3 +359,4 @@ This keeps the decision history honest and traceable.
|
|
|
359
359
|
| 0265 | [**Work with no goal survives a version close, and the order the ten-area cut actually needs** ([task 1003713](https://cloudbongos.com/builders#/task/1003713) · goal 1000086 — *Strict versioning*, criteria `sv-every-task-has-a-goal` + `sv-cutover-proven-live`). `closeVersion`'s post-apply invariant counts GOALS, and every task write inside the close is scoped `WHERE goal_id = $1` — so a task belonging to no goal is reached by no disposition, counted by no re-count, and ships stranded on a version that is over. Not hypothetical: six such tasks already sat on shipped GDS-V3/GDS-V4, and 47 rows on BONGOS-V1 carried a null `goal_id` (R11 required the column going forward but never backfilled, because every catch-all lookup matches the exact title `<VERSION> — general` and BONGOS-V1's is titled 'Work with no goal yet'). Decision: a second invariant in the same transaction refuses a close leaving ANY non-terminal task on the version, failing `CLOSE_LEAVES_STRANDED_TASKS` with the ids; exempting only the maintenance goal (its carry-forward runs in the later `onClosed` hook) — through the SAME `maintenanceGoalExemptSql` the goal count uses, never a second copy — and `__smoke__` fixtures. Consequence recorded here too: because `PATCH /tasks/:id {goal_id}` refuses a cross-version move (409 `goal_version_mismatch`) and `roll_forward` CREATES a successor rather than merging into one, [ADR 0264](<redacted>.md)'s cut must run home-then-close-then-**fold on the planning version**, where task and destination finally share a version. Rejected: a `fold_into` disposition (puts scope decisions inside a shipping transaction), creating the six new areas on the building version via R07 admissions (spends ten overrides to route around the rule they price), and a backfill instead of a gate (fixes today's rows, not the forty-eighth).](<redacted>.md) | lifecycle / version boundary |
|
|
360
360
|
| 0266 | [**The Board Room is its own surface, reachable by whoever may vote** ([task 1003734](https://cloudbongos.com/builders#/task/1003734) · goal 1000111 — *Working area 7, Government*, criterion `wa7-government`; owner decision 2026-09-08). The hall's only **upward-pointing** gate ([ADR 0175](<redacted>.md)) shipped as a hash tab inside `/government`, and two things about that did not survive inspection. **The nav never followed the page:** R16 correctly widened the PAGE gate from `government.manage` (archon) to `page.view.government` (metic+) when the page became three rooms — its own comment says *"the shell follows the widest legitimate audience"* — but `shell.js` still declares that item as label Permissions with gate archon, so a Metic who SITS on the board has no nav link at all and the only word in the nav is Permissions. Watch, Harbor, Gate and Sessions each own a nav item; the voting room was reachable only by knowing a URL fragment. **The surface was narrower than the franchise:** `board.vote.cast` floors at XENOS deliberately (`board.js:149` — the floor is low *so that widening the board works*) while the shell demands metic+, so the moment a constitution widens membership below Metic — the exact act [ADR 0175](<redacted>.md) §6 exists to make a CONFIG change — those members may vote by API and cannot load the page they would vote on, reintroducing the code-path change §6 removed. Decision: **its own page at `/board-room`, gated on the same atom the vote route checks.** The floor stays coarse — membership is still checked in-handler per item against that item's own snapshotted constitution — so a builder who clears the floor but sits on no board sees the room and no ballot, which is honest (§9 made the ballot open on purpose). **The migration is client-side because a fragment never reaches the server:** `#board-room` cannot be caught by any server route, so `government.js` redirects on load, preserving `?item=N` — recorded as a decision because adding a regex to `serve-internal.js` looks like the obvious fix and silently never fires. Three writers move (Discord `board-broadcast.js`, the `board_votes` need href, docs), and the redirect stays a release regardless because announcements already in channel history carry the old link forever. A **waiting-vote count** goes on the nav item: the data already exists (`boardVotesNeed` carries a live count pre-filtered to what the vote route would accept, served on `GET /me`), but `shell.js` has NO badge/bell/dot mechanism at all, so one is built once, generically, zero-is-silent, visible to exactly whoever the item is. Found while scoping and filed separately ([task 1003738](https://cloudbongos.com/builders#/task/1003738)): the hall renders needs as a SINGLE slot and `computeNeeds` sorts only by state with a stable sort, so registry order decides inside the `action_needed` bucket and `boardVotesNeed` sits fourth behind `artKeyNeed` — any builder with an unresolved image key never sees the board notice, which was live for this decision's own owner at the moment they asked for the bell. Rejected: a nav item merely deep-linking to the existing tab (cheaper, keeps the hash links free, but leaves the voting room inside a page whose gate answers a different question); a bell in the global header (a second inbox, needing its own read/unread and dismissal semantics); and a SOUND (`alert.wav`/`chime.wav` already ship, so it would be easy — it was not asked for).](<redacted>.md) | government / the board / hall nav |
|
|
361
361
|
| 0267 | [**Unanimity, and the revise-and-re-sit loop** ([task 1003733](https://cloudbongos.com/builders#/task/1003733) · goal 1000111 — *Government*, owner decision 2026-09-08). A Full Idea is ratified by **unanimous** agreement from round one; a sitting that does not carry RETURNS to its author, who revises and re-sits it, unbounded, until the board is unanimous. `unanimous` joins `PASS_RULES` as a fourth answer on the axis these rules actually differ on — **what silence means**: it is two conditions, not one, because "nobody objected" is true of an empty room. Everyone who spoke must have said yes AND somebody must have spoken, so an unvoted sitting RETURNS, which is exactly what makes the clock the owner asked for safe under it ([ADR 0191](<redacted>.md) §4: a deadline may only be given to a rule whose expiry means return). **There is deliberately no membership denominator** — that is the whole difference from `majority`, and it is the owner's "silence does not block" as code: an absent member is not in the reckoning, so one yes out of four carries. The stated cost is that an attentive minority can ratify on a distracted board; the alternative is one person on holiday stopping the pipeline. Trigger changes follow from the rule: a single yes does NOT close it (a later member must still be able to object — the sitting runs its clock, full turnout closes early), while a reasoned objection closes it AT ONCE (no vote-changing in v1 fixes the outcome, and the author needs the feedback to revise). ADR 0191 §3's author rule rides both unchanged. **`BOARD_DEFAULTS` is NOT moved**: a fresh instance stays the day-one monarchy, because under any non-author-yes rule a solo founder can never ratify their own ideas (0191 §5) — adoption is a board amendment, and no env var can change a constitution. The revise-and-re-sit loop already existed (`POST /inbox/:id/resubmit` re-grades and re-fires the window hook); what the board owes it is now pinned by test — a return CLOSES the item so the next window may open, and pays nothing. Named limits: a zero-vote return carries no human objection (the author gets the mechanical weakest-section flag), and this changes nothing live until the separate defect where a ratified amendment does not survive to the next read is fixed.](<redacted>.md) | government / board room |
|
|
362
|
+
| 0268 | [**The constitution comes from two roots, and a decision that did not take must say so** ([task 1003739](https://cloudbongos.com/builders#/task/1003739) · goal 1000111 — *Government*). On 2026-08-25 cloudbongos.com's board RATIFIED an amendment (rank:metic+ · consent · a 1440-minute sitting) and the constitution never changed — for two weeks `GET /government/constitution` answered the day-one monarchy with the passed amendment sitting in its own `history` array directly beneath the contradicting `board` block. Every sitting since was decided under a rule the board had voted to replace, and it silently un-shipped [ADR 0191](<redacted>.md) (majority) and would have un-shipped [ADR 0267](<redacted>.md) the same way. **Cause:** `modules/government/config.js` resolved BOTH its neutral starter and its instance pack from one `path.resolve(__dirname, '..', '..')`, under a comment saying the roots coincided *today* — true in a single checkout, false on a STANDALONE instance ([ADR 0108](<redacted>.md) §1), where the server runs `node_modules/@cloudbongos/core` with `WorkingDirectory=<instance repo>`. The neutral path stayed right (it really is core content); the INSTANCE path resolved inside the core package, so the host's own `config/government.json` was never read and `applyBoardAmendment` wrote the ratified amendment into `node_modules`, where the nightly core upgrade ([ADR 0161](<redacted>.md) cuts a release per merge) erased it within a day. **Nothing threw and nothing could have** — both paths exist, both are writable, and an absent instance pack is a legal state, so the read fell through to the neutral monarchy exactly as designed. `src/branding.js` had the pattern right three files away. **Fix:** neutral from `resolveCoreRoot()`, instance from `resolveInstanceRoot()`, reached through `src/module-api` — which had exposed NEITHER resolver, and that absence is precisely why the module re-derived the wrong one ([ADR 0083](<redacted>.md) forbids requiring a core internal). Resolved at load, not per call, because `resolveInstanceRoot()` falls back to `process.cwd()` and a lazy resolve would let a `chdir` move the constitution; `applyBoardAmendment` now mkdirs, since the write targets a directory the core does not own. Single-checkout behaviour is byte-identical, asserted rather than claimed. **The detector, which is the durable half:** `constitutionView` carries a `divergence` block comparing the newest PASSED amendment (sanitized as an apply would write it) field-by-field against what is in force, rendered above the hall's dials in words rather than config keys. It REPORTS and never HEALS — the instance pack is also the file a human editing the constitution touches, so a silent re-apply would revert a legitimate hand edit with no way to tell the two apart. A RETURNED amendment is never compared (that is what *returned* means). Does NOT put the 2026-08-25 amendment back in force — that is a board act. Sibling sweep RUN, not promised: one real hit filed as [task 1003745](https://cloudbongos.com/builders#/task/1003745) (quarantine.js's repo-containment refusal is blind to the instance repo).](<redacted>.md) | government / instance roots |
|