@bongos/core 1.19.611 → 1.19.613
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +24 -19
- package/docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md +17 -0
- package/docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md +150 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api.js +10 -1
- package/scripts/gds/box-connect-lib.js +7 -3
- package/scripts/gds/build-cli-package.js +10 -3
- package/scripts/gds/cli-lib.js +99 -9
- package/scripts/gds/setup.js +11 -6
- package/src/module-api.js +1 -1
- package/tests/cli_surface.mjs +169 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.613",
|
|
6
|
+
"core_contract": "1.19.613",
|
|
7
|
+
"source_commit": "86c4eeb0c9ed29d15cfa0331071100cabd8c0837",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-09T04:38:02.786Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 459,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 2080,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2563,
|
|
20
|
+
"tree_sha256": "9254c8e86a95c04620946437ea658877a5d12f5d40f1342f3088ea735f733d71",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1807,7 +1807,7 @@
|
|
|
1807
1807
|
{
|
|
1808
1808
|
"path": "docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md",
|
|
1809
1809
|
"mode": "0000644",
|
|
1810
|
-
"sha256": "
|
|
1810
|
+
"sha256": "43bf2232da3353ab49bfd956a73d09c5045e5fa155e0004d0855a50587c299cd"
|
|
1811
1811
|
},
|
|
1812
1812
|
{
|
|
1813
1813
|
"path": "docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md",
|
|
@@ -1844,10 +1844,15 @@
|
|
|
1844
1844
|
"mode": "0000644",
|
|
1845
1845
|
"sha256": "c9cd485b1ffce55fea15fd5b544a01e5753309e291c88bd223d33e6c11c4ebec"
|
|
1846
1846
|
},
|
|
1847
|
+
{
|
|
1848
|
+
"path": "docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md",
|
|
1849
|
+
"mode": "0000644",
|
|
1850
|
+
"sha256": "ac348289412efce1b3d67d74fccb5ff81347d196715ff53fd77e79e22228fad3"
|
|
1851
|
+
},
|
|
1847
1852
|
{
|
|
1848
1853
|
"path": "docs/adr/README.md",
|
|
1849
1854
|
"mode": "0000644",
|
|
1850
|
-
"sha256": "
|
|
1855
|
+
"sha256": "6cdaf9ffd4b14ae27231158fc040d59f084b86a93e6ecc43fb16bdea54e32fb6"
|
|
1851
1856
|
},
|
|
1852
1857
|
{
|
|
1853
1858
|
"path": "docs/api-reference.md",
|
|
@@ -2732,7 +2737,7 @@
|
|
|
2732
2737
|
{
|
|
2733
2738
|
"path": "docs/module-api-changelog.md",
|
|
2734
2739
|
"mode": "0000644",
|
|
2735
|
-
"sha256": "
|
|
2740
|
+
"sha256": "b91dcdf5bd99bbaa08679535a2ee122afb40367b275d7f0790236fc358283890"
|
|
2736
2741
|
},
|
|
2737
2742
|
{
|
|
2738
2743
|
"path": "docs/modules-contract.md",
|
|
@@ -7632,12 +7637,12 @@
|
|
|
7632
7637
|
{
|
|
7633
7638
|
"path": "package-lock.json",
|
|
7634
7639
|
"mode": "0000644",
|
|
7635
|
-
"sha256": "
|
|
7640
|
+
"sha256": "16da9aa01a2a13938d0160a61d612c0d3757631e74c7236a545b86a87bd5fd1d"
|
|
7636
7641
|
},
|
|
7637
7642
|
{
|
|
7638
7643
|
"path": "package.json",
|
|
7639
7644
|
"mode": "0000644",
|
|
7640
|
-
"sha256": "
|
|
7645
|
+
"sha256": "8137fdc6cc9e9bdebbf3376f0a3095c75818c1e807c3fec8f37f95d4313cd37d"
|
|
7641
7646
|
},
|
|
7642
7647
|
{
|
|
7643
7648
|
"path": "public-docs/index.html",
|
|
@@ -7712,7 +7717,7 @@
|
|
|
7712
7717
|
{
|
|
7713
7718
|
"path": "scripts/gds/api.js",
|
|
7714
7719
|
"mode": "0000644",
|
|
7715
|
-
"sha256": "
|
|
7720
|
+
"sha256": "a018f4ccc228950065ef0a376445ba6fc79318e451f9a6bc648dc8dd97235b87"
|
|
7716
7721
|
},
|
|
7717
7722
|
{
|
|
7718
7723
|
"path": "scripts/gds/api.ps1",
|
|
@@ -7807,7 +7812,7 @@
|
|
|
7807
7812
|
{
|
|
7808
7813
|
"path": "scripts/gds/box-connect-lib.js",
|
|
7809
7814
|
"mode": "0000644",
|
|
7810
|
-
"sha256": "
|
|
7815
|
+
"sha256": "dbc4dd22d858a7d17c34ebf211edbd6d885afa39ab7b9e3032a7390cd5526638"
|
|
7811
7816
|
},
|
|
7812
7817
|
{
|
|
7813
7818
|
"path": "scripts/gds/box-infra.js",
|
|
@@ -7837,7 +7842,7 @@
|
|
|
7837
7842
|
{
|
|
7838
7843
|
"path": "scripts/gds/build-cli-package.js",
|
|
7839
7844
|
"mode": "0000644",
|
|
7840
|
-
"sha256": "
|
|
7845
|
+
"sha256": "81ad7fbc99f5c85219f1385cdd6e6954d27a0e2943f285676f060611a4878f4e"
|
|
7841
7846
|
},
|
|
7842
7847
|
{
|
|
7843
7848
|
"path": "scripts/gds/bump-version.js",
|
|
@@ -7897,7 +7902,7 @@
|
|
|
7897
7902
|
{
|
|
7898
7903
|
"path": "scripts/gds/cli-lib.js",
|
|
7899
7904
|
"mode": "0000644",
|
|
7900
|
-
"sha256": "
|
|
7905
|
+
"sha256": "44b8c997a25d9fcc68ba573d33391546ff98a871c7d99e9aa6d982c69a703824"
|
|
7901
7906
|
},
|
|
7902
7907
|
{
|
|
7903
7908
|
"path": "scripts/gds/client-baseurl-guard.js",
|
|
@@ -8717,7 +8722,7 @@
|
|
|
8717
8722
|
{
|
|
8718
8723
|
"path": "scripts/gds/setup.js",
|
|
8719
8724
|
"mode": "0000644",
|
|
8720
|
-
"sha256": "
|
|
8725
|
+
"sha256": "36b5c59af3cde0398e432db37f1cd645aab08ea70b308a5e53671a4fce06f7fe"
|
|
8721
8726
|
},
|
|
8722
8727
|
{
|
|
8723
8728
|
"path": "scripts/gds/shell.js",
|
|
@@ -9362,7 +9367,7 @@
|
|
|
9362
9367
|
{
|
|
9363
9368
|
"path": "src/module-api.js",
|
|
9364
9369
|
"mode": "0000644",
|
|
9365
|
-
"sha256": "
|
|
9370
|
+
"sha256": "5f2889a1593cd4d896fdb5bf56cbad44d99809b8a60e955bf378d321f8320891"
|
|
9366
9371
|
},
|
|
9367
9372
|
{
|
|
9368
9373
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9967,7 +9972,7 @@
|
|
|
9967
9972
|
{
|
|
9968
9973
|
"path": "tests/cli_surface.mjs",
|
|
9969
9974
|
"mode": "0000644",
|
|
9970
|
-
"sha256": "
|
|
9975
|
+
"sha256": "5a8cc9320d66980280965ee34fbb5fc4bbcc30c8ffa860324a99ce0ce4e4d61f"
|
|
9971
9976
|
},
|
|
9972
9977
|
{
|
|
9973
9978
|
"path": "tests/cli_token_reissue.mjs",
|
package/docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md
CHANGED
|
@@ -124,6 +124,23 @@ benefit. It gains one when ADR 0099's mirror lands.
|
|
|
124
124
|
dependency — a public package that depended on a private one would be uninstallable for
|
|
125
125
|
everyone.
|
|
126
126
|
- Task 1002025 is superseded and should be closed against this ADR rather than done.
|
|
127
|
+
- **The journey above is instance-dependent, and 0.1.1 shipped assuming it was not**
|
|
128
|
+
([task 1003730](https://cloudbongos.com/builders#/task/1003730)). `bongos shell` is the answer
|
|
129
|
+
this package gives to `bongos claim`, but `dev-box` is a **control-plane** module — it
|
|
130
|
+
provisions droplets, so every hosted co-tenant checked (hermeslines-marketing, mercury, demo)
|
|
131
|
+
runs without it while cloudbongos.com has it. On those instances the only route the CLI offered
|
|
132
|
+
to a checkout ended in `HTTP 404: not_found`: a newcomer could list the work and not start it.
|
|
133
|
+
The instance manifest had always said so — `/api/gds/instance` carries `modules.enabled` — and
|
|
134
|
+
no CLI script read it. It now does, **failing open** (a core too old to publish a roster must
|
|
135
|
+
never block a working box), and the dispatcher's checkout-only text presents `bongos shell` as
|
|
136
|
+
conditional rather than promising a box it cannot verify.
|
|
137
|
+
- **A surface conversion is only as complete as the paths it is tested on.** Task 1003680 routed
|
|
138
|
+
`start.js` through `surface.js` and pinned exactly that file, so eleven `/builder-*` lines
|
|
139
|
+
shipped in 0.1.1 — including `requireSession()`, the gate every unauthenticated verb passes
|
|
140
|
+
through, which told terminal users to run `/builder-setup`, and `setup.js`'s completion footer,
|
|
141
|
+
which told a newly-registered builder to run two verbs this package does not have. Several also
|
|
142
|
+
named `node scripts/gds/<x>.js`, the checkout this package exists to avoid needing. The test now
|
|
143
|
+
**runs** the guidance printers under both surfaces rather than scanning one file's source.
|
|
127
144
|
|
|
128
145
|
## Rejected
|
|
129
146
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 0266 — The Board Room is its own surface, reachable by whoever may vote
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-08
|
|
5
|
+
- **Tasks:** [#1003734](https://cloudbongos.com/builders#/task/1003734) (this record). Executed by [#1003735](https://cloudbongos.com/builders#/task/1003735) (the carve), [#1003736](https://cloudbongos.com/builders#/task/1003736) (the deep links), [#1003737](https://cloudbongos.com/builders#/task/1003737) (the waiting-vote count).
|
|
6
|
+
- **Goal:** [#1000111](https://cloudbongos.com/builders#/goal/1000111) — Working area 7, Government; criterion `wa7-government`.
|
|
7
|
+
- **Decided by:** the owner (Masterqua / `IAMMASTERQUA`, Archon, member of area 7), 2026-09-08.
|
|
8
|
+
- **Builds on:** [ADR 0175](<redacted>.md) (the Board Room, and §6 — who governs is configuration), [ADR 0157](<redacted>.md) (the `page.view.government` floor at metic), [ADR 0151](<redacted>.md) (permissions as the atom), [ADR 0016](<redacted>.md) (server-enforced, uncached, per request).
|
|
9
|
+
- **Related:** [ADR 0174](<redacted>.md) (government, not governance).
|
|
10
|
+
|
|
11
|
+
## 1. Context — the room with no door
|
|
12
|
+
|
|
13
|
+
The Board Room is the hall's only upward-pointing gate (ADR 0175): the surface
|
|
14
|
+
on which a Full Idea that cleared the completeness bar is ratified, and the only
|
|
15
|
+
surface on which the constitution itself can be amended. It ships today as a
|
|
16
|
+
**hash tab** (`#board-room`) inside the `/government` page, beside Permissions
|
|
17
|
+
and Constitution.
|
|
18
|
+
|
|
19
|
+
Two facts about that placement do not survive inspection.
|
|
20
|
+
|
|
21
|
+
**The nav never followed the page.** When R16 turned `/government` into three
|
|
22
|
+
rooms it correctly widened the *page* gate from `government.manage` (archon) to
|
|
23
|
+
`page.view.government` (metic+). The comment it left in
|
|
24
|
+
[`serve-internal.js`](../../src/bongos/serve-internal.js) states the principle
|
|
25
|
+
plainly — *"The shell follows the widest legitimate audience — the audience
|
|
26
|
+
changed when the page did."* But `modules/hall-ui/public/shell.js` still
|
|
27
|
+
declares that nav item as `label: 'Permissions'`, `gate: 'archon'`. So the page
|
|
28
|
+
widened and the door did not: a Metic who sits on the board has **no nav link to
|
|
29
|
+
the Board Room at all**, and the only word an Archon sees in the nav is
|
|
30
|
+
"Permissions", which does not suggest a voting room is behind it. Every other
|
|
31
|
+
government room — Watch, Harbor, Gate, Sessions — carries its own nav item. The
|
|
32
|
+
Board Room is the one room reachable only by knowing a URL fragment.
|
|
33
|
+
|
|
34
|
+
**The surface is narrower than the franchise.** `board.vote.cast` floors at
|
|
35
|
+
**xenos**, and that is deliberate: `modules/government/board.js:149` records the
|
|
36
|
+
floor is low *so that widening the board works*. The page shell requires
|
|
37
|
+
metic+. Today `membership: rank:archon` and every Archon clears both, so the
|
|
38
|
+
contradiction is latent. The moment a constitution widens membership below
|
|
39
|
+
Metic — the exact act ADR 0175 §6 exists to make a configuration change — those
|
|
40
|
+
members may cast a vote through the API and **cannot load the page they would
|
|
41
|
+
cast it on**.
|
|
42
|
+
|
|
43
|
+
The second is the load-bearing one. ADR 0175 §6 put membership, pass rule and
|
|
44
|
+
window in `config/government.json`, resolved per request and uncached, so that
|
|
45
|
+
"flipping the switch later changes numbers, not a code path that has never run."
|
|
46
|
+
A page gate keyed to a different atom than the vote reintroduces exactly the
|
|
47
|
+
code-path change that decision removed.
|
|
48
|
+
|
|
49
|
+
## 2. Decision
|
|
50
|
+
|
|
51
|
+
### 1. The Board Room becomes its own page at `/board-room`
|
|
52
|
+
|
|
53
|
+
Carved out of `/government`, which keeps Permissions and Constitution. It joins
|
|
54
|
+
Watch, Harbor, Gate and Sessions as a first-class room in the Government nav
|
|
55
|
+
group, with its own nav item.
|
|
56
|
+
|
|
57
|
+
The alternative — leaving the tab in place and adding a nav item that deep-links
|
|
58
|
+
to `/government#board-room` — was considered and rejected. It is cheaper and it
|
|
59
|
+
preserves the existing hash links for free, but it leaves the voting room inside
|
|
60
|
+
a page whose gate answers a different question, which is defect (2) above left
|
|
61
|
+
standing. The room that constrains the owner should not be a tab inside the
|
|
62
|
+
owner's admin page.
|
|
63
|
+
|
|
64
|
+
### 2. Its reach follows `board.vote.cast`, not `page.view.government`
|
|
65
|
+
|
|
66
|
+
The page gate resolves the **same atom the vote route checks**. The surface
|
|
67
|
+
reach and the right to vote become one thing, and widening the board stays a
|
|
68
|
+
configuration change.
|
|
69
|
+
|
|
70
|
+
`board.vote.cast`'s floor is only the coarse gate. **Membership is still checked
|
|
71
|
+
in-handler, per item, against that item's own snapshotted constitution** — the
|
|
72
|
+
floor decides who may receive the shell, never who may vote on what. A builder
|
|
73
|
+
who clears the floor but sits on no board sees the room and no ballot, which is
|
|
74
|
+
the correct and honest state: ADR 0175 §9 made the ballot open on purpose,
|
|
75
|
+
because "a board whose votes are invisible cannot constrain anyone."
|
|
76
|
+
|
|
77
|
+
### 3. The old deep links are migrated client-side, because a fragment never reaches the server
|
|
78
|
+
|
|
79
|
+
`#board-room` is a URL fragment. It is **not sent to the server**, so no
|
|
80
|
+
server-side route or redirect can ever see it. The compatibility path is
|
|
81
|
+
therefore a client-side redirect in `government.js`: on load, a `#board-room`
|
|
82
|
+
hash sends the browser to `/board-room`, preserving `?item=N`.
|
|
83
|
+
|
|
84
|
+
This is recorded as a decision rather than left as an implementation note
|
|
85
|
+
because it is the thing a reader will otherwise get wrong — adding
|
|
86
|
+
`GOVERNMENT_BOARD_RE` to `serve-internal.js` looks like the obvious fix and
|
|
87
|
+
silently never fires.
|
|
88
|
+
|
|
89
|
+
Three writers emit the old form and all three move (task
|
|
90
|
+
[#1003736](https://cloudbongos.com/builders#/task/1003736)): the Discord
|
|
91
|
+
window-open announcement (`modules/discord/board-broadcast.js`), the
|
|
92
|
+
`board_votes` need's action href (`modules/builder-settings/builder-needs.js`),
|
|
93
|
+
and the docs. The redirect stays for at least one release regardless, because
|
|
94
|
+
announcements already sitting in Discord channel history carry the old link
|
|
95
|
+
forever and ADR 0175 §8 makes that mention the "vote within X" notice.
|
|
96
|
+
|
|
97
|
+
### 4. A waiting-vote count belongs on the nav item, and the hall has no mechanism for one
|
|
98
|
+
|
|
99
|
+
The owner's second ask was to see that items are waiting **without going
|
|
100
|
+
inside**. The data already exists and needs nothing new server-side:
|
|
101
|
+
`boardVotesNeed` (`builder-needs.js`) carries a live count and the oldest
|
|
102
|
+
waiting item id, pre-filtered to what the vote route would actually accept, and
|
|
103
|
+
`GET /me` already serves it.
|
|
104
|
+
|
|
105
|
+
What does not exist is any badge, bell, dot or count mechanism in
|
|
106
|
+
`shell.js` — the nav renders label and icon and nothing else. Task
|
|
107
|
+
[#1003737](https://cloudbongos.com/builders#/task/1003737) adds that once,
|
|
108
|
+
generically, so a second consumer can adopt it without a second pattern. Zero is
|
|
109
|
+
silent, and the badge is visible to exactly whoever the nav item is: a count is
|
|
110
|
+
board activity, and it must not leak to someone who cannot see the room.
|
|
111
|
+
|
|
112
|
+
A **sound** is deliberately out of scope. The instance ships `alert.wav` and
|
|
113
|
+
`chime.wav`, so it would be easy and it was not asked for.
|
|
114
|
+
|
|
115
|
+
## 3. Consequences
|
|
116
|
+
|
|
117
|
+
**The `/government` tab strip loses a room, and that is the point.** Two rooms
|
|
118
|
+
remain, both administrative, both correctly behind the archon-shaped item they
|
|
119
|
+
already sit behind. The existing Permissions nav item keeps `gate: 'archon'` —
|
|
120
|
+
it *is* the archon admin surface, so that gate was never wrong for it. It was
|
|
121
|
+
wrong as the only door to three rooms.
|
|
122
|
+
|
|
123
|
+
**Widening the board becomes a real config change.** After this, an instance may
|
|
124
|
+
set `membership: rank:metic` — or wider — and the members it names can actually
|
|
125
|
+
reach the room. Before it, that edit produced members who could vote only by
|
|
126
|
+
`curl`. This is the property ADR 0175 §6 claimed and did not have.
|
|
127
|
+
|
|
128
|
+
**One more page joins the statically-parsed gate map.** `tests/hall_page_gate_map.mjs`
|
|
129
|
+
rebuilds the URL→gate map by parsing `serve-internal.js` as text, so the new
|
|
130
|
+
`const BOARD_ROOM_PAGE_RE = /…/;` must keep the bare form with nothing between
|
|
131
|
+
the `=` and the literal — a comment in that gap drops the regex out of the map
|
|
132
|
+
and the page silently tests as ungated. The file already records this rule at
|
|
133
|
+
`PROJECT_SETTINGS_PAGE_RE`; this ADR notes it because the failure is invisible.
|
|
134
|
+
|
|
135
|
+
**A defect found while scoping this, filed separately.** The hall renders needs
|
|
136
|
+
as a *single slot* (`needs.items.find(n => n.state !== 'satisfied')`), and
|
|
137
|
+
`computeNeeds` sorts only by state with a stable sort — so inside the
|
|
138
|
+
`action_needed` bucket the registry array order decides, and `boardVotesNeed` is
|
|
139
|
+
fourth, behind `artKeyNeed`. Any builder with an unresolved image-generation key
|
|
140
|
+
therefore never sees the board notice at all. It was live for the owner of this
|
|
141
|
+
decision at the moment they asked for the bell. Filed as
|
|
142
|
+
[#1003738](https://cloudbongos.com/builders#/task/1003738) rather than folded in
|
|
143
|
+
here: the badge and the banner are two surfaces, and fixing the ordering is a
|
|
144
|
+
change to what every need in the hall does.
|
|
145
|
+
|
|
146
|
+
**Rejected: a bell in the global header rather than on the item.** A header bell
|
|
147
|
+
is a second inbox — it needs its own read/unread state, its own dismissal
|
|
148
|
+
semantics, and a home for notifications that are not board votes. The ask was to
|
|
149
|
+
see the count on the section, and the nav item is where the reader already looks
|
|
150
|
+
for the room.
|
package/docs/adr/README.md
CHANGED
|
@@ -357,3 +357,4 @@ This keeps the decision history honest and traceable.
|
|
|
357
357
|
| 0263 | [**How a version closes: auto, early, roll-forward, and the maintenance exemption** ([task 1003593](https://cloudbongos.com/builders#/task/1003593) · goal 1000086 — *Strict versioning*). [ADR 0250](<redacted>.md) D5 decided a version *can* close; this is the design pass that makes its five build tasks mechanical, so they do not each invent their own refusal codes and payload shapes and meet only in production. **The asymmetry that shrinks the feature:** auto-close fires when the last non-maintenance goal achieves — so by construction every non-maintenance goal is already closed and the set needing a disposition is EMPTY. All the machinery (the disposition map, the refusal that returns the goals, successor lineage) belongs to EARLY close alone; R16 is small and R12 carries the weight. **Auto-close hangs off BOTH achievement writers** (`db-goals.achieveGoalIfComplete` and the `done-when` cascade — the deliberate duplicate D2 already governs, a require cycle being the reason they are not one helper), on the caller `exec` so it sees the ship's uncommitted flip, and **may never fail a ship**: wrapped and swallowed like `closeCompletedWorkForShip`, idempotent under `WHERE status='building'`, so a failure leaves a version un-closed (self-healing on the next reconciler sweep) and never half-closed. **Early close** is the R10 archive two-step verbatim in spirit — refuse with `version_holds_open_goals` **returning the goals**, then proceed with a per-goal `roll_forward`/`abandon` map and a required `reason`; six named codes, because the callers are agents. **Two briefs are corrected by measurement:** the lineage column is `succeeded_by_goal_id` (migration 160, on the OLD row pointing forward) — not the `succeeds_goal_id` both 0250 and R17's brief name, so R17 needs NO migration; and R18's `is_maintenance` flag is load-bearing rather than cosmetic, because answering the close count with a `title NOT LIKE` would let a hand-titled goal silently exempt itself from the gate that decides when a VERSION closes. Promotion runs inside the close txn and BEFORE roll-forward (`ensureMaintenanceGoal` returns null for a non-`building` version, so a carry-over run first silently carries nothing); zero planning versions is not an error; R19's one migration should carry the `building` AND `planning` partial unique indexes so the promotion race resolves like a 23505 rather than a prayer. Rejected: auto-creating the successor version inside a close (the "fake hotfix version" escape hatch 0250 built the override counter to prevent), writing `limitations/<version>-shipped.md` from the route (file I/O in a ship transaction — it stays R23's CLI work), and a version reopen.](<redacted>.md) | version lifecycle / scope closure |
|
|
358
358
|
| 0264 | [**The ten working areas: one goal per area, held until 5,000 builders** ([task 1003696](https://cloudbongos.com/builders#/task/1003696) · goal 1000086 — *Strict versioning*, criterion `sv-cutover-proven-live`). The owner and the full team ran a manual goal review on 2026-09-07 and cut **38 open goals down to 10**, one per working area, each with a named owner and a described 5,000-builder end state: project creation (Rini), account/community (Rini), human project management (Scott), core distribution (Lars/Will), module distribution & economy (Will), the four-role experience incl. agents (Masterqua/Rini), government (Will/Masterqua), the credit economy (Scott), platform analytics (Lars), security (everyone bar Nils and Scott). **Why the rules were not enough on their own:** [ADR 0250](<redacted>.md) D1 closes the goal set of a BUILDING version — which freezes 38 goals exactly as effectively as it would freeze 10. Enforcement cannot shrink a scope that is already too large, so the rules and the cut are two halves of one move; the horizon is a SCALE (5,000 builders), not a date, because that is the question that decides when this set stops being the right set. **The finding that reorders the goal:** none of the cut can be executed today. R10 ([task 1003597](https://cloudbongos.com/builders#/task/1003597)) made `POST /goals/:id/archive` refuse a goal holding unfinished tasks and demand a disposition — correct, and exactly D3 — but the vector that SUPPLIES a disposition is R14 ([task 1003601](https://cloudbongos.com/builders#/task/1003601)), unshipped. Every one of the 12 deletes and every fold goes through that door, so **R14 is the keystone of the whole cut**, not one feature among thirteen, and is built before the rest of the version-close chain rather than in rank order. It looks like a bug and is not: the refusal is the design, landed one task ahead of its own remedy. "Delete" means `archived`, never a row removal (`goals.status` is open|achieved|archived, migration 160; reversible via `/reopen`) — preserving the rows keeps shipped-task attribution and credit history a delete would strand. Three open goals were NOT covered by the review and are surfaced as open questions rather than guessed: 1000003 (the catch-all — already R25's target), 1000065 (assumed to be area 2's community half), and 1000088 (repo cleanup — maps to no area, needs an owner decision). Deliberately does not decide: the areas' priority order, criteria for the six goals R29 must create (inventing criteria for someone else's area is how a goal acquires scope its owner never agreed to), or any task rehoming (R25/R26).](<redacted>.md) | scope / goal set / owner decision |
|
|
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
|
+
| 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 |
|
|
@@ -1671,5 +1671,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1671
1671
|
landed since 1.19.609 with no explicit bump. run 34305007041. (task 1002620)
|
|
1672
1672
|
1.19.611 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1673
1673
|
landed since 1.19.610 with no explicit bump. run 34305857169. (task 1002620)
|
|
1674
|
+
1.19.612 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1675
|
+
landed since 1.19.611 with no explicit bump. run 34310708322. (task 1002620)
|
|
1676
|
+
1.19.613 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1677
|
+
landed since 1.19.612 with no explicit bump. run 34311691842. (task 1002620)
|
|
1674
1678
|
---------------------------------------------------------------------------
|
|
1675
1679
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.613",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.613",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.613",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/api.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
// on a 2xx, 1 on any non-2xx or transport error, 2 on bad invocation.
|
|
19
19
|
|
|
20
20
|
const fs = require('node:fs');
|
|
21
|
-
const { cliClient, arg, stripBom, resolveQuiet, isBoxScope, printBoxScopeGuidanceAndExit, cliExit, repairMsysMangledPath
|
|
21
|
+
const { cliClient, arg, stripBom, resolveQuiet, isBoxScope, printBoxScopeGuidanceAndExit, cliExit, repairMsysMangledPath,
|
|
22
|
+
instanceDevBoxSupport, devBoxUnavailableMessage } = require('./cli-lib');
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
(async () => {
|
|
@@ -89,6 +90,14 @@ const { cliClient, arg, stripBom, resolveQuiet, isBoxScope, printBoxScopeGuidanc
|
|
|
89
90
|
if (res.data !== undefined && res.data !== null) {
|
|
90
91
|
console.log(JSON.stringify(res.data, null, 2));
|
|
91
92
|
}
|
|
93
|
+
// task 1003730: `bongos box` in the public CLI is this passthrough aimed at
|
|
94
|
+
// POST /box/ensure, so on an instance without the dev-box module a builder got a raw
|
|
95
|
+
// 404 body and no idea why. api.js stays generic — it just adds the one explanation
|
|
96
|
+
// that a /box/ 404 always means — and only for a reader who typed a box command.
|
|
97
|
+
if (res.status === 404 && /^\/api\/(?:bongos\/v1|bongos|gds)\/box\b/.test(urlPath)) {
|
|
98
|
+
const { available } = await instanceDevBoxSupport();
|
|
99
|
+
if (!available) console.error(`\n✖ ${devBoxUnavailableMessage()}`);
|
|
100
|
+
}
|
|
92
101
|
// task 1002734: process.exit() here aborted node 5/5 on Windows —
|
|
93
102
|
// "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c"
|
|
94
103
|
// — AFTER the JSON had already printed, so every caller that checks the exit code
|
|
@@ -30,7 +30,7 @@ const path = require('node:path');
|
|
|
30
30
|
const crypto = require('node:crypto');
|
|
31
31
|
const { spawnSync } = require('node:child_process');
|
|
32
32
|
const ic = require('../../src/instance-config');
|
|
33
|
-
const { cliClient } = require('./cli-lib');
|
|
33
|
+
const { cliClient, surfaceAction, assertDevBoxAvailable } = require('./cli-lib');
|
|
34
34
|
|
|
35
35
|
// The box always runs as root and the repo is cloned to /workspace (the devcontainer
|
|
36
36
|
// standardizes the same path). NOT ~/workspace — root's ~ is /root, which has no clone.
|
|
@@ -330,9 +330,9 @@ async function ensureKeyRegistered({ created, keyPath, pubKey, log = console.log
|
|
|
330
330
|
log(' ✓ Registered your public key with the dev system.');
|
|
331
331
|
return { registered: true };
|
|
332
332
|
}
|
|
333
|
-
log(` ! Could not register your key (${res.data?.error || res.status}); if SSH is refused, add it via
|
|
333
|
+
log(` ! Could not register your key (${res.data?.error || res.status}); if SSH is refused, add it via ${surfaceAction('box')}.`);
|
|
334
334
|
} catch (err) {
|
|
335
|
-
log(` ! Could not register your key (${err.message}); if SSH is refused, add it via
|
|
335
|
+
log(` ! Could not register your key (${err.message}); if SSH is refused, add it via ${surfaceAction('box')}.`);
|
|
336
336
|
}
|
|
337
337
|
return { registered: false };
|
|
338
338
|
}
|
|
@@ -349,6 +349,7 @@ function sleep(ms) {
|
|
|
349
349
|
// public box object from /box/me ({ state, hostname, ip, host_keys, … }).
|
|
350
350
|
// Throws on a provision denial (rank/blocked) or a timeout.
|
|
351
351
|
async function ensureBoxActive({ log = console.log, timeoutMs = 6 * 60 * 1000, intervalMs = 5000, sleepFn = sleep } = {}) {
|
|
352
|
+
await assertDevBoxAvailable();
|
|
352
353
|
const api = await cliClient();
|
|
353
354
|
const ensureRes = await api.box.postBoxEnsure();
|
|
354
355
|
if (ensureRes.status === 403) {
|
|
@@ -462,6 +463,9 @@ function pinKnownHosts({ hostname, hostKeys, ip = null, log = console.log }) {
|
|
|
462
463
|
// The one-shot "get the box ready + local SSH set up" used by both verbs.
|
|
463
464
|
// Returns { box, keyPath }.
|
|
464
465
|
async function prepareConnection({ log = console.log } = {}) {
|
|
466
|
+
// Before touching keys: key registration hits the box API too, so without this the reader gets
|
|
467
|
+
// a puzzling "Could not register your key (not_found)" line before the real explanation.
|
|
468
|
+
await assertDevBoxAvailable();
|
|
465
469
|
const { keyPath, pubKey, created } = ensureSshKey({ log });
|
|
466
470
|
await ensureKeyRegistered({ created, keyPath, pubKey, log });
|
|
467
471
|
const box = await ensureBoxActive({ log });
|
|
@@ -35,7 +35,7 @@ const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
|
35
35
|
|
|
36
36
|
// The package's OWN version — deliberately independent of the core's. The core moves many times
|
|
37
37
|
// a day (CI auto-patch, ADR 0161); the CLI's public contract should not.
|
|
38
|
-
const PACKAGE_VERSION = '0.1.
|
|
38
|
+
const PACKAGE_VERSION = '0.1.2';
|
|
39
39
|
const PACKAGE_NAME = '@cloudbongos/cli';
|
|
40
40
|
|
|
41
41
|
// ── What the package carries ────────────────────────────────────────────────────────────────
|
|
@@ -140,6 +140,10 @@ const VERBS = {
|
|
|
140
140
|
// Verbs that exist in the full core CLI and are deliberately absent here. The dispatcher prints
|
|
141
141
|
// the reason and the actual next step, because a newcomer hitting a bare "unknown command" learns
|
|
142
142
|
// nothing — that silence is the exact failure this whole task is fixing.
|
|
143
|
+
//
|
|
144
|
+
// The next step it names is CONDITIONAL prose, not a promise: dev boxes are a control-plane
|
|
145
|
+
// capability many instances run without (task 1003730). The dispatcher has no session or manifest
|
|
146
|
+
// yet, so it must not claim `bongos shell` will work — that verb makes the determination itself.
|
|
143
147
|
const CHECKOUT_ONLY = {
|
|
144
148
|
claim: 'Claiming a task writes code, so it needs a real checkout and a worktree.',
|
|
145
149
|
ship: 'Shipping grades, merges and deploys from a real checkout.',
|
|
@@ -307,10 +311,13 @@ function main(argv) {
|
|
|
307
311
|
if (CHECKOUT_ONLY[verb]) {
|
|
308
312
|
process.stderr.write(
|
|
309
313
|
'bongos: "' + verb + '" is not in the public CLI.\\n ' + CHECKOUT_ONLY[verb] + '\\n\\n' +
|
|
310
|
-
'
|
|
314
|
+
'If this instance offers cloud dev boxes, you can get a checkout without\\n' +
|
|
315
|
+
'installing anything locally:\\n' +
|
|
311
316
|
' bongos shell open a terminal on your cloud dev box\\n' +
|
|
312
317
|
' bongos code open VS Code on it over Remote-SSH\\n\\n' +
|
|
313
|
-
'
|
|
318
|
+
'Not every instance runs dev boxes. If yours does not, bongos shell says so,\\n' +
|
|
319
|
+
'and the route is a checkout of the project repo instead. The full CLI\\n' +
|
|
320
|
+
'(including ' + verb + ') is installed in both.\\n'
|
|
314
321
|
);
|
|
315
322
|
return 2;
|
|
316
323
|
}
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -363,11 +363,19 @@ function printUnauthGuidanceAndExit() {
|
|
|
363
363
|
console.error('');
|
|
364
364
|
console.error('✖ Your Bongos CLI session is no longer valid.');
|
|
365
365
|
console.error('');
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
366
|
+
if (isTerminalSurface()) {
|
|
367
|
+
// No checkout here, so `node scripts/gds/*.js` is not a path this reader has (task 1003730).
|
|
368
|
+
console.error(` Recover the fast (UI-first) way — run ${surfaceAction('reauth')}`);
|
|
369
|
+
console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
|
|
370
|
+
console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
|
|
371
|
+
console.error(' Fallback (browser sign-in): bongos login <instance-url>');
|
|
372
|
+
} else {
|
|
373
|
+
console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
|
|
374
|
+
console.error(' node scripts/gds/reauth.js');
|
|
375
|
+
console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
|
|
376
|
+
console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
|
|
377
|
+
console.error(' Fallback (terminal Device Flow): node scripts/gds/setup.js --force');
|
|
378
|
+
}
|
|
371
379
|
console.error('');
|
|
372
380
|
console.error(' Possible causes:');
|
|
373
381
|
console.error(' - Your session token expired (24h idle TTL, auto-refreshes on each request)');
|
|
@@ -375,7 +383,8 @@ function printUnauthGuidanceAndExit() {
|
|
|
375
383
|
console.error(' - A recent deploy applied an auth-changing migration (see deploy banner)');
|
|
376
384
|
console.error('');
|
|
377
385
|
console.error(' Once re-authed, re-run whichever command you were trying.');
|
|
378
|
-
|
|
386
|
+
// `doctor` is checkout-only — it is not one of the public CLI's verbs (ADR 0258).
|
|
387
|
+
if (!isTerminalSurface()) console.error(' For a full diagnosis, run: node scripts/gds/doctor.js');
|
|
379
388
|
console.error('');
|
|
380
389
|
process.exit(2);
|
|
381
390
|
}
|
|
@@ -414,7 +423,7 @@ function printBoxScopeGuidanceAndExit() {
|
|
|
414
423
|
console.error(' not claim / ship / grade / etc. (the box holds no full-authority token by');
|
|
415
424
|
console.error(' design — ADR 0053). Re-auth to a real CLI session, then re-run:');
|
|
416
425
|
console.error('');
|
|
417
|
-
console.error(' /builder-reauth (or: node scripts/gds/reauth.js)');
|
|
426
|
+
console.error(isTerminalSurface() ? ' bongos reauth' : ' /builder-reauth (or: node scripts/gds/reauth.js)');
|
|
418
427
|
console.error('');
|
|
419
428
|
console.error(` It opens your Settings page (${hallBase()}/settings) on`);
|
|
420
429
|
console.error(' YOUR machine — under "CLI Access" click "Re-issue CLI token", copy the one-liner, paste it back.');
|
|
@@ -655,13 +664,78 @@ async function cliClient(opts = {}) {
|
|
|
655
664
|
return client;
|
|
656
665
|
}
|
|
657
666
|
|
|
667
|
+
// ---- does this instance even HAVE dev boxes? (task 1003730) -----------------
|
|
668
|
+
// The dev-box module is a CONTROL-PLANE capability: it provisions DigitalOcean droplets, so a
|
|
669
|
+
// hosted co-tenant instance runs without it. The instance manifest has always said so —
|
|
670
|
+
// GET /api/gds/instance carries modules.enabled — but no CLI script read it, so `bongos shell`
|
|
671
|
+
// on such an instance died with a bare `POST /box/ensure failed (HTTP 404: not_found)`.
|
|
672
|
+
//
|
|
673
|
+
// That mattered far more than a bad error string: the public CLI (ADR 0258) answers
|
|
674
|
+
// `bongos claim` by pointing at `bongos shell`, so on every dev-box-less instance the ONLY route
|
|
675
|
+
// it offered to a checkout was a dead end — a newcomer could see the work and not start it.
|
|
676
|
+
//
|
|
677
|
+
// Cached per process, and FAILS OPEN: a manifest hiccup must never block a working box.
|
|
678
|
+
let _devBoxCheck = null;
|
|
679
|
+
|
|
680
|
+
// The decision, kept pure so it is testable without a network: does THIS manifest prove the
|
|
681
|
+
// instance has no dev box? Anything short of an explicit module roster answers "assume yes" —
|
|
682
|
+
// an older core, a truncated body or an unreachable manifest must never block a working box.
|
|
683
|
+
function devBoxEnabledInManifest(manifest) {
|
|
684
|
+
const enabled = manifest && manifest.modules && manifest.modules.enabled;
|
|
685
|
+
if (!enabled || typeof enabled !== 'object') return true;
|
|
686
|
+
return enabled['dev-box'] === true;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function instanceDevBoxSupport() {
|
|
690
|
+
if (_devBoxCheck) return _devBoxCheck;
|
|
691
|
+
_devBoxCheck = (async () => {
|
|
692
|
+
try {
|
|
693
|
+
const api = await cliClient();
|
|
694
|
+
const res = await api.instance.getInstance();
|
|
695
|
+
return { available: devBoxEnabledInManifest(res && res.ok ? res.data : null) };
|
|
696
|
+
} catch (_) {
|
|
697
|
+
return { available: true };
|
|
698
|
+
}
|
|
699
|
+
})();
|
|
700
|
+
return _devBoxCheck;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// The explanation a reader can act on. Deliberately does NOT name the instance: a co-tenant's
|
|
704
|
+
// manifest currently reports the platform's name rather than its own, so naming it would be wrong
|
|
705
|
+
// more often than helpful.
|
|
706
|
+
function devBoxUnavailableMessage() {
|
|
707
|
+
return (
|
|
708
|
+
`This instance doesn't offer cloud dev boxes.\n` +
|
|
709
|
+
` It runs without the dev-box module, so there is no box to open a shell on.\n` +
|
|
710
|
+
` Claiming and shipping happen in a checkout of the project's repo, where the full\n` +
|
|
711
|
+
` CLI is installed.\n` +
|
|
712
|
+
`\n` +
|
|
713
|
+
` → Ask the project's owner for repo access, then run ${surfaceAction('claim', 'N')} from that checkout.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Throw that explanation when this instance has no dev box. Callers run it BEFORE any box call,
|
|
718
|
+
// so nothing prints a confusing intermediate 404 first.
|
|
719
|
+
async function assertDevBoxAvailable() {
|
|
720
|
+
const { available } = await instanceDevBoxSupport();
|
|
721
|
+
if (available) return;
|
|
722
|
+
throw new Error(devBoxUnavailableMessage());
|
|
723
|
+
}
|
|
724
|
+
|
|
658
725
|
async function requireSession() {
|
|
659
726
|
const s = await loadSession();
|
|
660
727
|
if (!s || !s.token) {
|
|
661
728
|
console.error(`No Bongos session found at ${SESSION_PATH}.`);
|
|
662
729
|
console.error(`You're not signed in yet — pick the line that fits:`);
|
|
663
|
-
|
|
664
|
-
|
|
730
|
+
if (isTerminalSurface()) {
|
|
731
|
+
// A terminal reader may have no instance configured at all — naming one is the whole point
|
|
732
|
+
// of `login`, and `/builder-*` does not exist outside a Claude Code session (task 1003730).
|
|
733
|
+
console.error(` • First time here → run bongos login <instance-url> (sign in with a browser click)`);
|
|
734
|
+
console.error(` • Signed in before / expired → run bongos reauth (re-issue your CLI token)`);
|
|
735
|
+
} else {
|
|
736
|
+
console.error(` • First time on the project → run /builder-setup (one-time GitHub sign-in that also registers you)`);
|
|
737
|
+
console.error(` • Signed in before / expired → run /builder-reauth (fast UI re-issue of your CLI token)`);
|
|
738
|
+
}
|
|
665
739
|
process.exit(2);
|
|
666
740
|
}
|
|
667
741
|
return s;
|
|
@@ -752,11 +826,21 @@ function tableTask(t) {
|
|
|
752
826
|
// The nudges name an action the reader can run. Which spelling is right depends on who is
|
|
753
827
|
// reading — see scripts/gds/surface.js (task 1003680). Fail-open: a missing surface module must
|
|
754
828
|
// never break a nudge.
|
|
829
|
+
//
|
|
830
|
+
// Both helpers are `function` declarations so they hoist — the guidance printers above (which run
|
|
831
|
+
// far earlier in the file) call them, and moving them up would churn the nudge block they belong to.
|
|
755
832
|
function surfaceAction(verb, arg) {
|
|
756
833
|
try { return require('./surface').action(verb, arg); }
|
|
757
834
|
catch (_) { return `/builder-${verb}${arg == null ? '' : ` ${arg}`}`; }
|
|
758
835
|
}
|
|
759
836
|
|
|
837
|
+
// Is a person at a console reading this, rather than a Claude Code session? Fails open to AGENT,
|
|
838
|
+
// so a missing/broken surface module leaves the agent contract byte-identical (task 1003730).
|
|
839
|
+
function isTerminalSurface() {
|
|
840
|
+
try { return require('./surface').isTerminal(); }
|
|
841
|
+
catch (_) { return false; }
|
|
842
|
+
}
|
|
843
|
+
|
|
760
844
|
function nudgeActionForStage(stage) {
|
|
761
845
|
switch (stage) {
|
|
762
846
|
case 'auth_complete':
|
|
@@ -937,6 +1021,12 @@ module.exports = {
|
|
|
937
1021
|
_noKeepAliveResetForTest,
|
|
938
1022
|
cliClient,
|
|
939
1023
|
requireSession,
|
|
1024
|
+
surfaceAction,
|
|
1025
|
+
isTerminalSurface,
|
|
1026
|
+
devBoxEnabledInManifest,
|
|
1027
|
+
instanceDevBoxSupport,
|
|
1028
|
+
devBoxUnavailableMessage,
|
|
1029
|
+
assertDevBoxAvailable,
|
|
940
1030
|
printUnauthGuidanceAndExit,
|
|
941
1031
|
isBoxScope,
|
|
942
1032
|
printBoxScopeGuidanceAndExit,
|
package/scripts/gds/setup.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// home directory) and prints the builder profile.
|
|
20
20
|
|
|
21
21
|
const readline = require('readline');
|
|
22
|
-
const { cliClient, saveSession, SESSION_PATH, loadSession, printOnboardingNudge, API_BASE, hallBase } = require('./cli-lib');
|
|
22
|
+
const { cliClient, saveSession, SESSION_PATH, loadSession, printOnboardingNudge, API_BASE, hallBase, surfaceAction, isTerminalSurface } = require('./cli-lib');
|
|
23
23
|
|
|
24
24
|
// The instance's status dashboard, from branding — never a hardcoded host, so a
|
|
25
25
|
// non-OTB instance points its builders at ITS OWN dashboard (task 1002428).
|
|
@@ -386,8 +386,11 @@ async function finishSetup(builder) {
|
|
|
386
386
|
console.log('');
|
|
387
387
|
console.log('Newcomer path:');
|
|
388
388
|
console.log(` 1. Choose thy craft: open ${hallBase()} (creative / engineering)`);
|
|
389
|
-
console.log(
|
|
390
|
-
console.log(
|
|
389
|
+
console.log(` 2. Survey work: ${surfaceAction('start')}`);
|
|
390
|
+
console.log(` 3. Claim + ship: ${surfaceAction('claim', 'N')} → ${surfaceAction('ship')}`);
|
|
391
|
+
// Claiming writes code, so it needs a checkout. In a terminal the public CLI answers
|
|
392
|
+
// `bongos claim` with where to get one; say so here rather than let step 3 look self-serve.
|
|
393
|
+
if (isTerminalSurface()) console.log(' (claim + ship run in a checkout — `bongos claim` shows how to get one)');
|
|
391
394
|
console.log(' 4. Ship three works and you rise to Thetes — the whole queue opens.');
|
|
392
395
|
}
|
|
393
396
|
} catch (_) {
|
|
@@ -546,11 +549,11 @@ async function main() {
|
|
|
546
549
|
console.error('');
|
|
547
550
|
console.error('This GitHub account has not been approved yet.');
|
|
548
551
|
console.error('An Archon must approve your access request before you can enroll.');
|
|
549
|
-
console.error(
|
|
552
|
+
console.error(` → Ask an Archon to invite you, then re-run ${surfaceAction('setup')}.`);
|
|
550
553
|
process.exit(1);
|
|
551
554
|
}
|
|
552
555
|
if (flow.state !== 'ok') {
|
|
553
|
-
console.error(`\nSign-in did not complete (${flow.state}). Re-run
|
|
556
|
+
console.error(`\nSign-in did not complete (${flow.state}). Re-run ${surfaceAction('setup')}.`);
|
|
554
557
|
process.exit(1);
|
|
555
558
|
}
|
|
556
559
|
const { token, builder } = flow;
|
|
@@ -629,7 +632,9 @@ async function main() {
|
|
|
629
632
|
console.error(`${require('./cli-lib').productName()} admits new builders by invitation: an Archon must`);
|
|
630
633
|
console.error('approve your access request before you can enroll.');
|
|
631
634
|
console.error(' → Ask an Archon to invite you / approve your access request,');
|
|
632
|
-
console.error(
|
|
635
|
+
console.error(isTerminalSurface()
|
|
636
|
+
? ` then re-run ${surfaceAction('setup')}.`
|
|
637
|
+
: ' then re-run /builder-setup (or: node scripts/gds/setup.js).');
|
|
633
638
|
process.exit(1);
|
|
634
639
|
}
|
|
635
640
|
if (!poll.ok) {
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.613'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/cli_surface.mjs
CHANGED
|
@@ -144,3 +144,172 @@ test('the skills still invoke the scripts directly, which is what keeps agents o
|
|
|
144
144
|
const skill = fs.readFileSync(path.join(REPO_ROOT, '.claude', 'skills', 'builder-start', 'SKILL.md'), 'utf8');
|
|
145
145
|
assert.match(skill, /node scripts\/gds\/start\.js/, 'builder-start must call the script directly');
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
// ── 4. The rest of the shipped CLI, not just start.js (task 1003730) ────────────────────────
|
|
149
|
+
//
|
|
150
|
+
// Section 3 above pinned start.js and nothing else, which is exactly why eleven `/builder-*`
|
|
151
|
+
// lines shipped in @cloudbongos/cli@0.1.1 — including the one EVERY unauthenticated verb prints.
|
|
152
|
+
// A terminal reader was told to run `/builder-setup`, which exists only inside Claude Code.
|
|
153
|
+
|
|
154
|
+
import { spawnSync } from 'node:child_process';
|
|
155
|
+
import fs from 'node:fs';
|
|
156
|
+
|
|
157
|
+
// Every .js the public package ships, read out of the build manifest so a newly-shipped script
|
|
158
|
+
// is covered the day it is added rather than the day someone remembers to list it here.
|
|
159
|
+
function shippedScripts() {
|
|
160
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
|
|
161
|
+
const block = src.match(/const FILES = \[([\s\S]*?)\n\];/);
|
|
162
|
+
assert.ok(block, 'could not read the FILES manifest out of build-cli-package.js');
|
|
163
|
+
return [...block[1].matchAll(/'([^']+\.js)'/g)].map((m) => m[1]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Run one cli-lib guidance printer in a child process (they all call process.exit) and return
|
|
167
|
+
// everything it wrote. HOME is pointed at nothing so no real session can satisfy requireSession.
|
|
168
|
+
function runGuidance(fn, env) {
|
|
169
|
+
const code =
|
|
170
|
+
`const l=require(${JSON.stringify(path.join(REPO_ROOT, 'scripts', 'gds', 'cli-lib.js'))});` +
|
|
171
|
+
`Promise.resolve(l[${JSON.stringify(fn)}]()).catch(()=>{});`;
|
|
172
|
+
const r = spawnSync(process.execPath, ['-e', code], {
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
env: { ...process.env, ...env, HOME: path.join(REPO_ROOT, 'no-such-home-for-tests') },
|
|
175
|
+
});
|
|
176
|
+
return `${r.stdout || ''}${r.stderr || ''}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const GUIDANCE = ['printUnauthGuidanceAndExit', 'printBoxScopeGuidanceAndExit', 'requireSession'];
|
|
180
|
+
|
|
181
|
+
test('no guidance path speaks slash commands to a terminal reader', () => {
|
|
182
|
+
for (const fn of GUIDANCE) {
|
|
183
|
+
const out = runGuidance(fn, { BONGOS_SURFACE: 'terminal' });
|
|
184
|
+
const leaks = [...out.matchAll(/\/builder-[a-z-]+/g)].map((m) => m[0]);
|
|
185
|
+
assert.deepEqual(leaks, [], `${fn} leaked to a terminal reader: ${leaks.join(', ')}\n${out}`);
|
|
186
|
+
assert.ok(!/node scripts\/gds\//.test(out), `${fn} told a terminal reader to run a repo script:\n${out}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('the agent still gets its slash commands from those same paths', () => {
|
|
191
|
+
// The inverse guard: a blanket rewrite that "fixed" the leak by deleting the slash commands
|
|
192
|
+
// would break the reader they were written for, and nothing else here would notice.
|
|
193
|
+
assert.match(runGuidance('printUnauthGuidanceAndExit', {}), /\/builder-reauth/);
|
|
194
|
+
assert.match(runGuidance('printBoxScopeGuidanceAndExit', {}), /\/builder-reauth/);
|
|
195
|
+
assert.match(runGuidance('requireSession', {}), /\/builder-setup/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('every shipped script that names a slash command routes it through the surface switch', () => {
|
|
199
|
+
// A `/builder-*` literal is only legitimate inside a surface branch. Rather than parse
|
|
200
|
+
// branches, require that any shipped file printing one also consults the switch — and pin the
|
|
201
|
+
// set, so a NEW unguarded file fails here instead of on a newcomer's laptop.
|
|
202
|
+
const ALLOWED = new Set([
|
|
203
|
+
'scripts/gds/cli-lib.js', // agent side of the three branches, proven above
|
|
204
|
+
'scripts/gds/setup.js', // re-run + newcomer-path lines, asserted below
|
|
205
|
+
'scripts/gds/box-connect-lib.js',// key-registration hint, asserted below
|
|
206
|
+
'scripts/gds/surface.js', // the switch itself
|
|
207
|
+
'scripts/gds/start.js', // section 3's documented agent-only exemptions
|
|
208
|
+
]);
|
|
209
|
+
const offenders = [];
|
|
210
|
+
for (const rel of shippedScripts()) {
|
|
211
|
+
const full = path.join(REPO_ROOT, rel);
|
|
212
|
+
if (!fs.existsSync(full)) continue;
|
|
213
|
+
const code = fs.readFileSync(full, 'utf8')
|
|
214
|
+
.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
215
|
+
// Only lines that actually PRINT — a comment or a require path is not user-facing.
|
|
216
|
+
const prints = code.split('\n').filter((l) => /\/builder-[a-z-]/.test(l) && /console\.(log|error)|log\(/.test(l));
|
|
217
|
+
if (prints.length && !ALLOWED.has(rel)) offenders.push(`${rel}: ${prints[0].trim()}`);
|
|
218
|
+
}
|
|
219
|
+
assert.deepEqual(offenders, [], `shipped scripts print slash commands with no surface switch:\n${offenders.join('\n')}`);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('setup.js routes its newcomer path and re-run lines through the surface', () => {
|
|
223
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'setup.js'), 'utf8');
|
|
224
|
+
// The completion footer is the worst possible place for a wrong command: it is the first thing
|
|
225
|
+
// a brand-new builder reads after registering.
|
|
226
|
+
assert.match(src, /Survey work:\s*\$\{surfaceAction\('start'\)\}/, 'the newcomer path must use surfaceAction');
|
|
227
|
+
assert.match(src, /surfaceAction\('claim', 'N'\)/, 'claim step must use surfaceAction');
|
|
228
|
+
assert.ok(!/console\.log\(' 2\. Survey work: \/builder-start'\)/.test(src), 'hard-coded /builder-start left in setup.js');
|
|
229
|
+
assert.match(src, /re-run \$\{surfaceAction\('setup'\)\}/, 'the re-run lines must use surfaceAction');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('box-connect-lib names the box command for the reader it has', () => {
|
|
233
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'box-connect-lib.js'), 'utf8');
|
|
234
|
+
assert.ok(!/add it via \/builder-box/.test(src), 'hard-coded /builder-box left in box-connect-lib.js');
|
|
235
|
+
assert.match(src, /add it via \$\{surfaceAction\('box'\)\}/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ── 5. The dead end: an instance with no dev box (task 1003730) ─────────────────────────────
|
|
239
|
+
//
|
|
240
|
+
// `bongos claim N` refuses and points at `bongos shell`. On an instance whose manifest carries
|
|
241
|
+
// no dev-box module that call 404s, so the public CLI's only offered route to a checkout was a
|
|
242
|
+
// hard error — a newcomer could see the work and not start it. The manifest always said so;
|
|
243
|
+
// nothing read it.
|
|
244
|
+
|
|
245
|
+
const cliLib = require(path.join(REPO_ROOT, 'scripts', 'gds', 'cli-lib.js'));
|
|
246
|
+
|
|
247
|
+
test('the manifest decides, and only an explicit roster can prove absence', () => {
|
|
248
|
+
assert.equal(cliLib.devBoxEnabledInManifest({ modules: { enabled: { 'dev-box': true } } }), true);
|
|
249
|
+
assert.equal(cliLib.devBoxEnabledInManifest({ modules: { enabled: { economy: true } } }), false);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('the check fails OPEN — a hiccup must never block a working box', () => {
|
|
253
|
+
// Every one of these is "I could not tell", and the safe answer to that is to proceed and let
|
|
254
|
+
// the real API call speak. Failing closed here would break `bongos shell` on every instance
|
|
255
|
+
// running a core too old to publish a module roster.
|
|
256
|
+
for (const manifest of [null, undefined, {}, { modules: {} }, { modules: { enabled: null } }, 'nonsense']) {
|
|
257
|
+
assert.equal(cliLib.devBoxEnabledInManifest(manifest), true, `should fail open for ${JSON.stringify(manifest)}`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('the explanation names a command the reader can actually run', () => {
|
|
262
|
+
// It routes through the same switch as everything else: a terminal reader must not be handed
|
|
263
|
+
// `/builder-claim`, and an agent in a checkout must still get it.
|
|
264
|
+
const prev = process.env.BONGOS_SURFACE;
|
|
265
|
+
try {
|
|
266
|
+
process.env.BONGOS_SURFACE = 'terminal';
|
|
267
|
+
const term = cliLib.devBoxUnavailableMessage();
|
|
268
|
+
assert.ok(!/\/builder-/.test(term), `slash command in the terminal dev-box message: ${term}`);
|
|
269
|
+
assert.match(term, /bongos claim N/);
|
|
270
|
+
assert.match(term, /doesn't offer cloud dev boxes/);
|
|
271
|
+
assert.match(term, /checkout/, 'must name the actual alternative, not just refuse');
|
|
272
|
+
|
|
273
|
+
delete process.env.BONGOS_SURFACE;
|
|
274
|
+
assert.match(cliLib.devBoxUnavailableMessage(), /\/builder-claim N/, 'agents keep the slash command');
|
|
275
|
+
} finally {
|
|
276
|
+
if (prev === undefined) delete process.env.BONGOS_SURFACE;
|
|
277
|
+
else process.env.BONGOS_SURFACE = prev;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('both box entry points check BEFORE they touch the box API', () => {
|
|
282
|
+
// Ordering is the whole point: key registration hits the box API too, so gating only the
|
|
283
|
+
// ensure call still printed "Could not register your key (not_found)" first.
|
|
284
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'box-connect-lib.js'), 'utf8');
|
|
285
|
+
assert.match(
|
|
286
|
+
src,
|
|
287
|
+
/async function prepareConnection\([^)]*\) \{[\s\S]{0,400}?await assertDevBoxAvailable\(\);[\s\S]{0,200}?ensureSshKey/,
|
|
288
|
+
'prepareConnection must assert before ensureSshKey/key registration',
|
|
289
|
+
);
|
|
290
|
+
assert.match(
|
|
291
|
+
src,
|
|
292
|
+
/async function ensureBoxActive\([^)]*\) \{\s*await assertDevBoxAvailable\(\);/,
|
|
293
|
+
'ensureBoxActive must assert before postBoxEnsure',
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('the raw box passthrough explains a 404 too', () => {
|
|
298
|
+
// `bongos box` is api.js aimed at POST /box/ensure, so without this it dumped a bare 404 body.
|
|
299
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'api.js'), 'utf8');
|
|
300
|
+
assert.match(src, /instanceDevBoxSupport/, 'api.js must consult the dev-box check');
|
|
301
|
+
assert.match(src, /res\.status === 404/, 'only on a 404');
|
|
302
|
+
assert.match(src, /\\\/box\\b/, 'and only for a box path — api.js stays generic');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('the generated dispatcher does not promise a dev box it cannot verify', () => {
|
|
306
|
+
// The dispatcher has no session or manifest yet, so it must present `bongos shell` as
|
|
307
|
+
// conditional. Stating it flatly is what sent newcomers into the 404.
|
|
308
|
+
const gen = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
|
|
309
|
+
assert.ok(
|
|
310
|
+
!/'To get a checkout without installing anything locally/.test(gen),
|
|
311
|
+
'the unconditional promise is back in the checkout-only refusal',
|
|
312
|
+
);
|
|
313
|
+
assert.match(gen, /If this instance offers cloud dev boxes/);
|
|
314
|
+
assert.match(gen, /Not every instance runs dev boxes/);
|
|
315
|
+
});
|