@bongos/core 1.19.617 → 1.19.619

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,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.617",
6
- "core_contract": "1.19.617",
7
- "source_commit": "203592afd9cb34d665552529bb6dd17c36bb5c1e",
5
+ "core_version": "1.19.619",
6
+ "core_contract": "1.19.619",
7
+ "source_commit": "b3a494e84958a264492cf55b3525d70680f7bbda",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T05:58:55.608Z",
9
+ "built_at": "2026-09-09T06:39:30.103Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 460,
12
+ "docs_redacted": 463,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2081,
14
+ "functional_verbatim": 2086,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2565,
20
- "tree_sha256": "9832dfdbe8796c1219f41418c67e707f0b30103bcbc3b7950cacd7a90758f646",
19
+ "file_count": 2573,
20
+ "tree_sha256": "f0ad9c8d1e70405ab669163d2d2153cd7f6ae97a699036bb8e4278f1e3a435d5",
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,20 @@
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
+ },
1867
+ {
1868
+ "path": "docs/adr/0269-the-cli-session-store-is-host-keyed-at-a-fixed-anchor.md",
1869
+ "mode": "0000644",
1870
+ "sha256": "cc44045ea59f76dabc62fe43993ffab6004add88c7f6142bfe9215f7f2c33c99"
1871
+ },
1857
1872
  {
1858
1873
  "path": "docs/adr/README.md",
1859
1874
  "mode": "0000644",
1860
- "sha256": "c836e90436fea9c5a1f1a38e3640643602e8a639120c33770ab525c432655799"
1875
+ "sha256": "d660b4bc0bc3306f5fcf583118ca9149ba3deafe82dfe3894fe0b1c11b60cc2f"
1861
1876
  },
1862
1877
  {
1863
1878
  "path": "docs/api-reference.md",
@@ -1892,12 +1907,12 @@
1892
1907
  {
1893
1908
  "path": "docs/copy-inventory.md",
1894
1909
  "mode": "0000644",
1895
- "sha256": "00e8dd0db1a70a4f9fec1f1622688f78308f560485bfb8a0df24d50ceb4a8c66"
1910
+ "sha256": "0e4c54e6bd6d76fdd7853b19c343b747fabfc2f59ee8baab74cbaebc6064aa1e"
1896
1911
  },
1897
1912
  {
1898
1913
  "path": "docs/copy-registry.json",
1899
1914
  "mode": "0000644",
1900
- "sha256": "4dcbb030236a1267b336e0ecb1c13623794cba823b61304ab4cb7b4db06c55b1"
1915
+ "sha256": "4357446e8f95c430791743a43f893140d862ab7dd5453f6a42791b194956d61c"
1901
1916
  },
1902
1917
  {
1903
1918
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2732,7 +2747,7 @@
2732
2747
  {
2733
2748
  "path": "docs/file-map.md",
2734
2749
  "mode": "0000644",
2735
- "sha256": "8b9c303b7cb35b420ff13c0a265fe2e505769c6d1625977eb85c0d077ff1ddf2"
2750
+ "sha256": "0cec3b144d8cca893b49c1d08bdacf37220dc1b670fd771656dda659bae16edc"
2736
2751
  },
2737
2752
  {
2738
2753
  "path": "docs/handoff-template.md",
@@ -2742,7 +2757,7 @@
2742
2757
  {
2743
2758
  "path": "docs/module-api-changelog.md",
2744
2759
  "mode": "0000644",
2745
- "sha256": "86545cc0ed7296d979d78e5a6bf3a6259e358f1673c00db4d9c9c0906aaa5a2f"
2760
+ "sha256": "1fe02fc79e2952efb61450bbd45e6b09e41e8cf73453c6e0a2fbbf2944c1cdab"
2746
2761
  },
2747
2762
  {
2748
2763
  "path": "docs/modules-contract.md",
@@ -4397,7 +4412,7 @@
4397
4412
  {
4398
4413
  "path": "modules/government/board.js",
4399
4414
  "mode": "0000644",
4400
- "sha256": "e79c0a72c27c3773308ad3e21040a6e22c4ba63af46aec8ab4d26e5d588a1b2e"
4415
+ "sha256": "68bf53ab31ec3f21359665eea42127f1691b8dc17b59100c65cc1f56a04ad6a7"
4401
4416
  },
4402
4417
  {
4403
4418
  "path": "modules/government/catalog.js",
@@ -4407,7 +4422,7 @@
4407
4422
  {
4408
4423
  "path": "modules/government/config.js",
4409
4424
  "mode": "0000644",
4410
- "sha256": "3e66902145db7a64e7932b8a071bbbe496dd63f450abe629168813349cf92211"
4425
+ "sha256": "8edccad7dc69a95503d9e0d1de7795fd6082c8122e0695d1f018f49b37051fed"
4411
4426
  },
4412
4427
  {
4413
4428
  "path": "modules/government/db.js",
@@ -4922,7 +4937,7 @@
4922
4937
  {
4923
4938
  "path": "modules/hall-ui/public/government.js",
4924
4939
  "mode": "0000644",
4925
- "sha256": "dea97ea504d81b69b97679db8dda2180f156b3fead18bc18a4d7c95e83d4fe9e"
4940
+ "sha256": "6333b05761e5bf3215fe3c19ae98b25ebdac8cf0761df28fbc4933a5317ce5fa"
4926
4941
  },
4927
4942
  {
4928
4943
  "path": "modules/hall-ui/public/government.states.json",
@@ -5052,7 +5067,7 @@
5052
5067
  {
5053
5068
  "path": "modules/hall-ui/public/oversight.css",
5054
5069
  "mode": "0000644",
5055
- "sha256": "13d46f29e77ba9ca30ab52de2e0413f2fcb68bfec44de032730d4d135530e4ba"
5070
+ "sha256": "a199baf1c11bdbe6d30931253956e0c9a9709ae3f880af77b2a6737212fe0984"
5056
5071
  },
5057
5072
  {
5058
5073
  "path": "modules/hall-ui/public/pair.css",
@@ -7642,12 +7657,12 @@
7642
7657
  {
7643
7658
  "path": "package-lock.json",
7644
7659
  "mode": "0000644",
7645
- "sha256": "3885cce1d32ce4a6a9c0ca430b7f03250a0cdb520f41aeae05cf69e32463fa93"
7660
+ "sha256": "41aca5e97b1cb72e389656c14ddd1bb35a3d5fdce8145f05e294c3fa3f52dc15"
7646
7661
  },
7647
7662
  {
7648
7663
  "path": "package.json",
7649
7664
  "mode": "0000644",
7650
- "sha256": "a810ca7406dc6408d628d4b6fafd83268bf994bb407e8a19acf91e35e3348192"
7665
+ "sha256": "2084b4c59fd9b3857bb83b5a4198d43c308f0180edc0e21a698e4285809323fd"
7651
7666
  },
7652
7667
  {
7653
7668
  "path": "public-docs/index.html",
@@ -7794,6 +7809,11 @@
7794
7809
  "mode": "0000644",
7795
7810
  "sha256": "a9f1064fbb683297e775c455ef65249cf809018a38a5aa2f975a62cbba16cd9c"
7796
7811
  },
7812
+ {
7813
+ "path": "scripts/gds/backlog-review.js",
7814
+ "mode": "0000644",
7815
+ "sha256": "4646bd6da8be2d2c40c1063bba47f5fdce0f2f63143e26fc16e36ea941adbc88"
7816
+ },
7797
7817
  {
7798
7818
  "path": "scripts/gds/backup.js",
7799
7819
  "mode": "0000644",
@@ -7847,7 +7867,7 @@
7847
7867
  {
7848
7868
  "path": "scripts/gds/build-cli-package.js",
7849
7869
  "mode": "0000644",
7850
- "sha256": "81ad7fbc99f5c85219f1385cdd6e6954d27a0e2943f285676f060611a4878f4e"
7870
+ "sha256": "de1cdede17761775722509b1e1edb4bb43fbbf336450556f22119b18b93e43d3"
7851
7871
  },
7852
7872
  {
7853
7873
  "path": "scripts/gds/bump-version.js",
@@ -7907,7 +7927,7 @@
7907
7927
  {
7908
7928
  "path": "scripts/gds/cli-lib.js",
7909
7929
  "mode": "0000644",
7910
- "sha256": "44b8c997a25d9fcc68ba573d33391546ff98a871c7d99e9aa6d982c69a703824"
7930
+ "sha256": "eaa4c3a172009d5ce825e528f5077a480e034658501a7d0c532a7807886beb9e"
7911
7931
  },
7912
7932
  {
7913
7933
  "path": "scripts/gds/client-baseurl-guard.js",
@@ -8287,7 +8307,7 @@
8287
8307
  {
8288
8308
  "path": "scripts/gds/login.js",
8289
8309
  "mode": "0000644",
8290
- "sha256": "f16497d7eae23657e83f631b420f82a55ed8216480f3cd5488609d3b93274dee"
8310
+ "sha256": "bdc7fcc0876a1185fb15339740793944c755d716d998bb68533ab3a721d39ab2"
8291
8311
  },
8292
8312
  {
8293
8313
  "path": "scripts/gds/main-audit.js",
@@ -9372,7 +9392,7 @@
9372
9392
  {
9373
9393
  "path": "src/module-api.js",
9374
9394
  "mode": "0000644",
9375
- "sha256": "1607f76a96146a4b3bfdda5b3206097dbee812115f47d57fe545513490fb350f"
9395
+ "sha256": "983d456cff4042137990a5baacb002919c3d213a33f663d6e8fcc7e9f5945ae6"
9376
9396
  },
9377
9397
  {
9378
9398
  "path": "src/module-loader/catalog.js",
@@ -9679,6 +9699,11 @@
9679
9699
  "mode": "0000644",
9680
9700
  "sha256": "25f45e6a0f79e6e6299b062dd89306f898fc1ed8cefbf4fa16b76c9c511f0548"
9681
9701
  },
9702
+ {
9703
+ "path": "tests/backlog_review.mjs",
9704
+ "mode": "0000644",
9705
+ "sha256": "788f3b3faba5f0568a251d3551c8776e2f383d59c8c2ed0728ef4c9af9a208c7"
9706
+ },
9682
9707
  {
9683
9708
  "path": "tests/backup_cli.mjs",
9684
9709
  "mode": "0000644",
@@ -9974,6 +9999,11 @@
9974
9999
  "mode": "0000644",
9975
10000
  "sha256": "a57516ed9e904cf1c86fe99688415f834dcae1a19f01b22e986f199fa5473258"
9976
10001
  },
10002
+ {
10003
+ "path": "tests/cli_sessions.mjs",
10004
+ "mode": "0000644",
10005
+ "sha256": "6ec406bf2bcbae261ab37ecaf820cfbe196c11007b0d8d3ee47cf21b6a3919ca"
10006
+ },
9977
10007
  {
9978
10008
  "path": "tests/cli_surface.mjs",
9979
10009
  "mode": "0000644",
@@ -10689,6 +10719,16 @@
10689
10719
  "mode": "0000644",
10690
10720
  "sha256": "a8e575d151aca92789ffa9d3a49f77aa2579fa285374b2c1f5a65b12ac82163d"
10691
10721
  },
10722
+ {
10723
+ "path": "tests/government_config_roots.mjs",
10724
+ "mode": "0000644",
10725
+ "sha256": "d3060b7302f17a1478588ee333ba9e2f4deea23d5896363b9bd42e30edb12c88"
10726
+ },
10727
+ {
10728
+ "path": "tests/government_constitution_divergence.mjs",
10729
+ "mode": "0000644",
10730
+ "sha256": "280bc5906ac8075495cd3b1fe10e4082291cf4497b166f6acbdec291a8fd6698"
10731
+ },
10692
10732
  {
10693
10733
  "path": "tests/government_constitution_view.mjs",
10694
10734
  "mode": "0000644",
@@ -11442,7 +11482,7 @@
11442
11482
  {
11443
11483
  "path": "tests/module_api.mjs",
11444
11484
  "mode": "0000644",
11445
- "sha256": "157d551b9a99320198f22d32ce335fa4166414dc1818d11ef23a0ea05ac80a76"
11485
+ "sha256": "cc6fb31e06f0b2accf46f1595f9548492698c88ff56c4d9f4f43c86fce6816a3"
11446
11486
  },
11447
11487
  {
11448
11488
  "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.
@@ -0,0 +1,104 @@
1
+ # ADR 0269 — The CLI's session store is host-keyed at a fixed anchor; the per-brand file stays the active pointer
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-09
5
+ - **Task:** [task 1003741](https://cloudbongos.com/builders#/task/1003741) (BONGOS-V2, goal 1000090 — *Working area 4, Bongos Core distribution*)
6
+ - **Deciders:** Claude, under the Archon's standing scope
7
+ - **Related:** [ADR 0258](<redacted>.md) (the public CLI, which is where this bites) · [ADR 0108](<redacted>.md) (each instance its own checkout) · [ADR 0083](<redacted>.md) (why the store directory may not be called `sessions`)
8
+
9
+ ---
10
+
11
+ ## Context
12
+
13
+ The CLI resolved its session path from the **branding pack**, which is read out of whatever
14
+ checkout the process happens to be standing in. That gave it exactly **one session slot**, and the
15
+ slot moved depending on where you ran from:
16
+
17
+ - **Standalone** — the public `@cloudbongos/cli`'s entire situation — no checkout means no brand,
18
+ so every instance shared `~/.config/cloudbongos/gds-session.json`. Signing into a second
19
+ instance **destroyed** the first. Measured on the published 0.1.1: `configHome()` resolved to
20
+ `<HOME>/.config/cloudbongos` whichever instance was named.
21
+ - **In-repo** the brand resolved and the session landed in `~/.config/<slug>/`, where the
22
+ standalone CLI could never find it. `npx @cloudbongos/cli api GET /api/gds/me` from a bare
23
+ directory returned the cloudbongos.com builder while a valid hermeslines session sat on the same
24
+ machine.
25
+
26
+ This was not theoretical. The owner's own config dir carries a hand-made
27
+ `gds-session.<redacted>.bak.json`, alongside four more rescues of the same
28
+ shape. For a newcomer with one project it is invisible; it bites precisely the people running more
29
+ than one, which is the direction the platform is going.
30
+
31
+ ## Decision
32
+
33
+ **Keep one session file per instance, keyed by the instance's host, under a FIXED anchor —
34
+ `~/.config/<FALLBACK_DIR>/instances/<host>.json` — and leave the per-brand `gds-session.json`
35
+ exactly where it is, as the ACTIVE pointer.**
36
+
37
+ Four parts are load-bearing.
38
+
39
+ ### 1. The anchor is fixed, never brand-derived
40
+
41
+ A brand-derived anchor is the bug. `sessionStoreDir()` is built from `FALLBACK_DIR` and must not
42
+ read `configHome()`, `configDirName()` or the pack — so the same instance resolves to the same file
43
+ whether you run from a checkout or from `npx`. A test asserts the function body names none of them,
44
+ because "helpfully" routing it back through the brand would silently reinstate the split.
45
+
46
+ It lives in `scripts/gds/cli-lib.js`, **not** `src/instance-config.js`. That module is generic
47
+ config-path machinery shared with the server, and a CLI session store is not its concern; cli-lib
48
+ already owns `SESSION_PATH` and `SESSION_READ_PATHS`, so one file now describes where a session
49
+ lives. (The first cut put it in instance-config and the knip dead-code ratchet caught it: knip
50
+ cannot trace that module's `require()` consumers, so every export it gains counts as dead — three
51
+ new ones broke the gate. The right answer was better placement, not a bigger baseline.)
52
+
53
+ ### 2. The active pointer does not move
54
+
55
+ Every existing reader — in-repo skills, the card hook, the dev box, `readSessionToken` — looks at
56
+ `gds-session.json` in the configured dir. The store is **additive**: `saveSession` still writes that
57
+ file last and unchanged. Moving it would have been the tidier design and would have broken every one
58
+ of those readers at once, for no gain a builder can see.
59
+
60
+ ### 3. The OUTGOING session is archived before the incoming one lands
61
+
62
+ Order matters, and this step is the one that is easy to omit. A session written *before* the store
63
+ existed is not in it; without archiving the session being replaced, the very first login after
64
+ upgrading would still lose it — the exact bug, surviving its own fix. Archiving is best-effort and
65
+ never blocks a real sign-in.
66
+
67
+ ### 4. A stored token is verified before it is trusted
68
+
69
+ `bongos login <url>` now switches back to an instance you are already signed into with no browser
70
+ flow — but it calls `/api/gds/me` with the stored token first. Reinstating an expired or revoked
71
+ token unchecked would leave a builder "signed in" to a session every later command then fails on.
72
+ An unverified token falls straight through to the real device flow; `--force` signs in as somebody
73
+ else.
74
+
75
+ Because sessions are now kept rather than overwritten, `login` also names the other instances this
76
+ machine knows and how to move between them — silence would leave them unfindable.
77
+
78
+ ## Consequences
79
+
80
+ - Signing into a second instance is non-destructive, and moving between two projects costs no
81
+ browser round-trip.
82
+ - Session files are `0600` in a `0700` directory: they hold bearer tokens.
83
+ - An `api_base` that names no host is **not stored** rather than stored under a guessed name, so a
84
+ malformed or hostile base cannot write outside the store directory.
85
+ - **The directory is `instances/`, not `sessions/`.** `sessions` is a live module key, and
86
+ ADR 0083 §Decision #4 forbids kernel machinery from naming one — `src/instance-config.js` is a
87
+ kernel file and the fitness gate caught it. `instances/` is also the truer name: one file per
88
+ instance is exactly what it holds.
89
+ - A corrupt entry in the store is skipped, never fatal.
90
+ - Still open: the store is per-machine and holds no notion of which instance a *directory* belongs
91
+ to, so running in an instance repo does not automatically select that instance — the active
92
+ pointer is whatever you last signed into. That is a smaller, separable question.
93
+
94
+ ## Rejected
95
+
96
+ - **Moving the active pointer into the store** — breaks every existing reader at once for no
97
+ visible gain.
98
+ - **One file holding a map of instances** — changes the on-disk format that in-repo readers,
99
+ hooks and the dev box all parse today.
100
+ - **Keying the store under `configHome()`** — brand-derived, which is the bug being fixed.
101
+ - **Restoring a stored session without verifying it** — hands the builder a dead session that
102
+ looks live.
103
+ - **A new `bongos switch` verb** — `login <url>` is the command a builder already reaches for, and
104
+ the public CLI's verb surface is deliberately small (ADR 0258).