@bongos/core 1.19.777 → 1.19.779
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 +27 -47
- package/.claude/skills/builder-end/SKILL.md +10 -24
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-ship/SKILL.md +3 -3
- package/.claude/skills/builder-stage/SKILL.md +12 -18
- package/.claude/skills/merge-mode/SKILL.md +5 -5
- package/docs/adr/0171-profile-rollup-join-and-consent.md +2 -0
- package/docs/adr/0182-project-visibility-two-axes.md +1 -1
- package/docs/file-map.md +3 -7
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/slash-commands.md +0 -2
- package/modules/platform-identity/platform-identity.js +9 -12
- package/modules/platform-identity/projects-feed.js +16 -2
- package/modules/platform-identity/tests/platform-identity.mjs +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hub_recent_ships.mjs +13 -0
- package/tests/profile_rollup_consent.mjs +50 -24
- package/tests/profile_route.mjs +58 -7
- package/tests/visible_projects_feed.mjs +3 -1
- package/.claude/skills/builder-box/SKILL.md +0 -59
- package/.claude/skills/builder-connect/SKILL.md +0 -60
- package/.claude/skills/builder-sync/SKILL.md +0 -42
- package/.claude/skills/close-box/SKILL.md +0 -44
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.779",
|
|
6
|
+
"core_contract": "1.19.779",
|
|
7
|
+
"source_commit": "1b37b29ce2a7832804f9b44a6982ea25b57643e3",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-16T20:08:25.581Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 482,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
14
|
"functional_verbatim": 2153,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2660,
|
|
20
|
+
"tree_sha256": "dbb45ccb2f44e4771d7a73e06e5ac46faea7115961c48cac9da3877e62f2e61c",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -49,21 +49,11 @@
|
|
|
49
49
|
"mode": "0000644",
|
|
50
50
|
"sha256": "722cb6ec3054b041e6f39e6ae2f1afa7977153cbcb8b005ef943509962c55d05"
|
|
51
51
|
},
|
|
52
|
-
{
|
|
53
|
-
"path": ".claude/skills/builder-box/SKILL.md",
|
|
54
|
-
"mode": "0000644",
|
|
55
|
-
"sha256": "2c99f7d3dc8bd89a4c3f4f49b32c630b0a18a5acf004d849b1e3564effa00ced"
|
|
56
|
-
},
|
|
57
52
|
{
|
|
58
53
|
"path": ".claude/skills/builder-claim/SKILL.md",
|
|
59
54
|
"mode": "0000644",
|
|
60
55
|
"sha256": "e98c9fc650b38916f81b453d20035259006160043c4170d36505756b314c4ffa"
|
|
61
56
|
},
|
|
62
|
-
{
|
|
63
|
-
"path": ".claude/skills/builder-connect/SKILL.md",
|
|
64
|
-
"mode": "0000644",
|
|
65
|
-
"sha256": "5c8f33777f987c6382e7d232c5ceb1883d0a6964365401169cd5941557e93364"
|
|
66
|
-
},
|
|
67
57
|
{
|
|
68
58
|
"path": ".claude/skills/builder-cost/SKILL.md",
|
|
69
59
|
"mode": "0000644",
|
|
@@ -72,12 +62,12 @@
|
|
|
72
62
|
{
|
|
73
63
|
"path": ".claude/skills/builder-end/SKILL.md",
|
|
74
64
|
"mode": "0000644",
|
|
75
|
-
"sha256": "
|
|
65
|
+
"sha256": "e6d9739aca81fdc54897049d2d5926b0923150a2f8866f5d1f5945f1412014f8"
|
|
76
66
|
},
|
|
77
67
|
{
|
|
78
68
|
"path": ".claude/skills/builder-exit/SKILL.md",
|
|
79
69
|
"mode": "0000644",
|
|
80
|
-
"sha256": "
|
|
70
|
+
"sha256": "9679ffdcb10af1858d45aa1d00be1141b798f167ee5daead42e0f6374e4e9fd4"
|
|
81
71
|
},
|
|
82
72
|
{
|
|
83
73
|
"path": ".claude/skills/builder-key/SKILL.md",
|
|
@@ -112,28 +102,18 @@
|
|
|
112
102
|
{
|
|
113
103
|
"path": ".claude/skills/builder-ship/SKILL.md",
|
|
114
104
|
"mode": "0000644",
|
|
115
|
-
"sha256": "
|
|
105
|
+
"sha256": "fee2003d2f6b12f404109217c14f7e0e22ad2abaf073322e08e8079549a136b3"
|
|
116
106
|
},
|
|
117
107
|
{
|
|
118
108
|
"path": ".claude/skills/builder-stage/SKILL.md",
|
|
119
109
|
"mode": "0000644",
|
|
120
|
-
"sha256": "
|
|
110
|
+
"sha256": "5f3c09420b08538d66dbe0ab858618907c0b92cf82a6aa44fe0eb53004f224c0"
|
|
121
111
|
},
|
|
122
112
|
{
|
|
123
113
|
"path": ".claude/skills/builder-start/SKILL.md",
|
|
124
114
|
"mode": "0000644",
|
|
125
115
|
"sha256": "fd023863bf2f53447be736ac8972423477005cb542c203b4e089d948507d8944"
|
|
126
116
|
},
|
|
127
|
-
{
|
|
128
|
-
"path": ".claude/skills/builder-sync/SKILL.md",
|
|
129
|
-
"mode": "0000644",
|
|
130
|
-
"sha256": "ed647c782b56f40792676341344f4b31d1a6f05e24c93d947d921b48c8117159"
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
"path": ".claude/skills/close-box/SKILL.md",
|
|
134
|
-
"mode": "0000644",
|
|
135
|
-
"sha256": "5cac7dffb190e027b9d3f011c35c16924280c07f6c98ecd60f8a44fcb18d2b50"
|
|
136
|
-
},
|
|
137
117
|
{
|
|
138
118
|
"path": ".claude/skills/design-sync/SKILL.md",
|
|
139
119
|
"mode": "0000644",
|
|
@@ -192,7 +172,7 @@
|
|
|
192
172
|
{
|
|
193
173
|
"path": ".claude/skills/merge-mode/SKILL.md",
|
|
194
174
|
"mode": "0000644",
|
|
195
|
-
"sha256": "
|
|
175
|
+
"sha256": "ef2c92c34d047186eaa016a99d4679f09be028120b9d61bc92b1a999bf87bec0"
|
|
196
176
|
},
|
|
197
177
|
{
|
|
198
178
|
"path": ".claude/skills/new-project/SKILL.md",
|
|
@@ -1347,7 +1327,7 @@
|
|
|
1347
1327
|
{
|
|
1348
1328
|
"path": "docs/adr/0171-profile-rollup-join-and-consent.md",
|
|
1349
1329
|
"mode": "0000644",
|
|
1350
|
-
"sha256": "
|
|
1330
|
+
"sha256": "943103bc2fcb66b89a06b13a49593d230b90f33bd3dec35aec82078cdd08f54f"
|
|
1351
1331
|
},
|
|
1352
1332
|
{
|
|
1353
1333
|
"path": "docs/adr/0172-editable-rank-roles-substrate-axis.md",
|
|
@@ -1407,7 +1387,7 @@
|
|
|
1407
1387
|
{
|
|
1408
1388
|
"path": "docs/adr/0182-project-visibility-two-axes.md",
|
|
1409
1389
|
"mode": "0000644",
|
|
1410
|
-
"sha256": "
|
|
1390
|
+
"sha256": "15387fac9c92e30d305444616865b6aab0b0c391ddc91f08656c74b9e71385ad"
|
|
1411
1391
|
},
|
|
1412
1392
|
{
|
|
1413
1393
|
"path": "docs/adr/0183-criteria-close-themselves.md",
|
|
@@ -2517,7 +2497,7 @@
|
|
|
2517
2497
|
{
|
|
2518
2498
|
"path": "docs/file-map.md",
|
|
2519
2499
|
"mode": "0000644",
|
|
2520
|
-
"sha256": "
|
|
2500
|
+
"sha256": "c3130f09be6637d33eb1ccdaf4fdadf6adbebdeae3e264f2f845e668c9fc90fa"
|
|
2521
2501
|
},
|
|
2522
2502
|
{
|
|
2523
2503
|
"path": "docs/handoff-template.md",
|
|
@@ -2527,7 +2507,7 @@
|
|
|
2527
2507
|
{
|
|
2528
2508
|
"path": "docs/module-api-changelog.md",
|
|
2529
2509
|
"mode": "0000644",
|
|
2530
|
-
"sha256": "
|
|
2510
|
+
"sha256": "b5409eae5a7466352e67fee988116c208a7c748e5dd88052f8a2b45a8e58f907"
|
|
2531
2511
|
},
|
|
2532
2512
|
{
|
|
2533
2513
|
"path": "docs/modules-contract.md",
|
|
@@ -2597,7 +2577,7 @@
|
|
|
2597
2577
|
{
|
|
2598
2578
|
"path": "docs/onboarding/slash-commands.md",
|
|
2599
2579
|
"mode": "0000644",
|
|
2600
|
-
"sha256": "
|
|
2580
|
+
"sha256": "748df9bec7434093e32f70915f151b9cc6ac67beebbe226b997d81eba327de31"
|
|
2601
2581
|
},
|
|
2602
2582
|
{
|
|
2603
2583
|
"path": "docs/packs/artist.md",
|
|
@@ -6102,7 +6082,7 @@
|
|
|
6102
6082
|
{
|
|
6103
6083
|
"path": "modules/platform-identity/platform-identity.js",
|
|
6104
6084
|
"mode": "0000644",
|
|
6105
|
-
"sha256": "
|
|
6085
|
+
"sha256": "c59bc528fb1bb451b1f1b751b2903b763b1d3e892a8a5bbc5144ee97db670cf3"
|
|
6106
6086
|
},
|
|
6107
6087
|
{
|
|
6108
6088
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -6127,7 +6107,7 @@
|
|
|
6127
6107
|
{
|
|
6128
6108
|
"path": "modules/platform-identity/projects-feed.js",
|
|
6129
6109
|
"mode": "0000644",
|
|
6130
|
-
"sha256": "
|
|
6110
|
+
"sha256": "14166982e6306ff8dd63f476af582970634eb9c210e9afa76166668e351244c9"
|
|
6131
6111
|
},
|
|
6132
6112
|
{
|
|
6133
6113
|
"path": "modules/platform-identity/publish-gate.js",
|
|
@@ -6187,7 +6167,7 @@
|
|
|
6187
6167
|
{
|
|
6188
6168
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
6189
6169
|
"mode": "0000644",
|
|
6190
|
-
"sha256": "
|
|
6170
|
+
"sha256": "da06ab89ee01bf3a704f677e7e50041220916f40cac23fa063c212dc9af2f56d"
|
|
6191
6171
|
},
|
|
6192
6172
|
{
|
|
6193
6173
|
"path": "modules/platform-identity/ui/.gitkeep",
|
|
@@ -7632,12 +7612,12 @@
|
|
|
7632
7612
|
{
|
|
7633
7613
|
"path": "package-lock.json",
|
|
7634
7614
|
"mode": "0000644",
|
|
7635
|
-
"sha256": "
|
|
7615
|
+
"sha256": "73c88d2a70b9f77171344eb76f4c5c9f2ecbbc4d1a1b6b3008d636e974b01f9b"
|
|
7636
7616
|
},
|
|
7637
7617
|
{
|
|
7638
7618
|
"path": "package.json",
|
|
7639
7619
|
"mode": "0000644",
|
|
7640
|
-
"sha256": "
|
|
7620
|
+
"sha256": "4c30d22b31621113b656c60c5a7d71d70fc1544326be0d506ca16a242783defd"
|
|
7641
7621
|
},
|
|
7642
7622
|
{
|
|
7643
7623
|
"path": "public-docs/index.html",
|
|
@@ -9497,7 +9477,7 @@
|
|
|
9497
9477
|
{
|
|
9498
9478
|
"path": "src/module-api.js",
|
|
9499
9479
|
"mode": "0000644",
|
|
9500
|
-
"sha256": "
|
|
9480
|
+
"sha256": "3a9d2a54bc76ea41d67b42be5f2c07da60fcdeec3bbed8f23111163dfa17f1ff"
|
|
9501
9481
|
},
|
|
9502
9482
|
{
|
|
9503
9483
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11312,7 +11292,7 @@
|
|
|
11312
11292
|
{
|
|
11313
11293
|
"path": "tests/hub_recent_ships.mjs",
|
|
11314
11294
|
"mode": "0000644",
|
|
11315
|
-
"sha256": "
|
|
11295
|
+
"sha256": "0d8f07c64f4d17c8c894b2d6395281882efd4e186c5714fe9aab61e2808d4314"
|
|
11316
11296
|
},
|
|
11317
11297
|
{
|
|
11318
11298
|
"path": "tests/idea_capture_grades.mjs",
|
|
@@ -12052,12 +12032,12 @@
|
|
|
12052
12032
|
{
|
|
12053
12033
|
"path": "tests/profile_rollup_consent.mjs",
|
|
12054
12034
|
"mode": "0000644",
|
|
12055
|
-
"sha256": "
|
|
12035
|
+
"sha256": "d63e22b8ddbcbe7e21a59ee907e5a4261bebc067c52de111c32a88f97a172406"
|
|
12056
12036
|
},
|
|
12057
12037
|
{
|
|
12058
12038
|
"path": "tests/profile_route.mjs",
|
|
12059
12039
|
"mode": "0000644",
|
|
12060
|
-
"sha256": "
|
|
12040
|
+
"sha256": "d70924dd2ddfc4e05c4b52d9763d76b0a68cf0ed16db6ffd1b07a1834bef0890"
|
|
12061
12041
|
},
|
|
12062
12042
|
{
|
|
12063
12043
|
"path": "tests/profile_ui_cross_project.mjs",
|
|
@@ -13257,7 +13237,7 @@
|
|
|
13257
13237
|
{
|
|
13258
13238
|
"path": "tests/visible_projects_feed.mjs",
|
|
13259
13239
|
"mode": "0000644",
|
|
13260
|
-
"sha256": "
|
|
13240
|
+
"sha256": "8a8df7e2a025029ffe7e18b7c7b18c6bd92a256822609931f0cd88628e446041"
|
|
13261
13241
|
},
|
|
13262
13242
|
{
|
|
13263
13243
|
"path": "tests/wandering_prefs.mjs",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: builder-end
|
|
3
3
|
description: >-
|
|
4
|
-
Close out this session: resolve the active claim, confirm the tree is clean, tear down the worktree
|
|
4
|
+
Close out this session: resolve the active claim, confirm the tree is clean, and tear down the worktree. Own-scoped; /builder-exit offboards a whole builder. Triggers: "/builder-end", "end my session", "sign off", "wrap up and clean up".
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are closing out the current Claude Code **session** safely: resolve the held claim, confirm no work is stranded uncommitted, and clean up the session's git worktree. It is the bookend to `/builder-start` — one command so a session ends in a known-clean state instead of leaving a half-shipped claim and an orphaned worktree behind.
|
|
@@ -15,9 +15,8 @@ Composes operations that already exist, in a safe order, with the destructive on
|
|
|
15
15
|
1. **Resolve the claim** — `/builder-ship` when there's committed, shippable work; `/builder-release` when there isn't. (It calls the sibling skills; it does not re-implement shipping.)
|
|
16
16
|
2. **Clean-git verify** — refuse to go further if any *tracked* file is uncommitted. This is the same guard `scripts/gds/ship.js` enforces (`trackedDirty` in [`src/bongos/ship-preflight.js`](../../../src/bongos/ship-preflight.js)): untracked scratch is ignored, but a modified/staged/deleted tracked file means real work isn't committed — stop before anything is lost.
|
|
17
17
|
3. **Worktree teardown** — from the main checkout, remove this session's linked worktree via `node scripts/gds/worktree.js remove` (junction-aware), `prune`, and delete the now-merged `claude/<name>` branch.
|
|
18
|
-
4. **Box close-out (dev box only)** — if running on a dev box (`/etc/otb/box.env` exists), offer to `close-box` so the box stops billing. Optional and gated on a yes; declining just lets the box idle-park. On a laptop this step is skipped — there's no box to close.
|
|
19
18
|
|
|
20
|
-
It never pushes to `main`, never `ssh`-es, never runs a deploy — landing is `/builder-ship`'s job (server-mediated). It only ever touches the caller's **own** claim
|
|
19
|
+
It never pushes to `main`, never `ssh`-es, never runs a deploy — landing is `/builder-ship`'s job (server-mediated). It only ever touches the caller's **own** claim and **own** worktree.
|
|
21
20
|
|
|
22
21
|
## How to use
|
|
23
22
|
|
|
@@ -25,7 +24,6 @@ It never pushes to `main`, never `ssh`-es, never runs a deploy — landing is `/
|
|
|
25
24
|
|
|
26
25
|
- **Held claim(s):** `node scripts/gds/api.js GET /api/gds/me` → `active_claims`. (A builder can hold parallel claims across sessions; resolve only the one this session worked.)
|
|
27
26
|
- **Are we in a linked worktree?** `git rev-parse --git-dir` differs from `git rev-parse --git-common-dir` ⇒ yes (`--git-dir` is the per-worktree `<main>/.git/worktrees/<name>`; `--git-common-dir` is the shared `<main>/.git`); they're equal ⇒ main checkout (skip step 3). Test `--git-dir`, **not** `--git-common-dir` — the latter always returns the shared `<main>/.git` even inside a worktree, so checking *it* for `/worktrees/` never matches.
|
|
28
|
-
- **Are we on a dev box?** `/etc/otb/box.env` exists ⇒ yes. This is the codebase's canonical "am I on a box?" marker — written by `box.js provision`, the same presence-check `scripts/gds/sandbox-stage.js` (`isDevBox`) uses. On a box the close-out has an extra step: the box keeps billing until it's shut down or idle-parks (step 4). On a laptop there's no box — skip step 4.
|
|
29
27
|
- **Working-tree state:** `git status --porcelain`.
|
|
30
28
|
- **Commits ahead of main:** `git rev-list --count main..HEAD` (fall back to `origin/main..HEAD`). Real commits ⇒ likely shippable; zero ⇒ likely a release.
|
|
31
29
|
|
|
@@ -34,7 +32,7 @@ It never pushes to `main`, never `ssh`-es, never runs a deploy — landing is `/
|
|
|
34
32
|
- **Commits ahead AND clean tree ⇒ shippable.** Confirm with the user, then run the **`builder-ship`** skill with honest handoff `--notes` and a one-line non-jargon `--summary`. Don't fabricate the summary — if you can't state the value plainly, it isn't shippable; release instead.
|
|
35
33
|
- **No commits / no shippable progress ⇒** run the **`builder-release`** skill with a one-phrase reason.
|
|
36
34
|
- **No active claim ⇒** nothing to resolve; go to step 2.
|
|
37
|
-
- **Ship strands at `confirmed`** (merge conflict or a failing check — the
|
|
35
|
+
- **Ship strands at `confirmed`** (merge conflict or a failing check — the ship lands, the merge doesn't): **stop here.** The branch still needs to land via `/merge-mode`. Surface that and do NOT tear down the worktree — its branch isn't merged yet (step 3's `git branch -d` would correctly refuse it).
|
|
38
36
|
|
|
39
37
|
### 2. Clean-git verify
|
|
40
38
|
|
|
@@ -52,31 +50,19 @@ git worktree prune
|
|
|
52
50
|
git branch -d claude/<worktree-name> # -d (not -D): git refuses an unmerged branch, by design
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
Removing this session's worktree ends its filesystem — do it last, and tell the user the session is closed and they can start fresh from `/builder-start` anytime.
|
|
53
|
+
Removing this session's worktree ends its filesystem — do it last, and tell the user the session is closed and they can start fresh from `/builder-start` anytime.
|
|
56
54
|
|
|
57
|
-
**On the main checkout (not a worktree):** skip the teardown above — there's nothing to remove.
|
|
58
|
-
|
|
59
|
-
### 4. Box close-out — dev box only, gated, after the claim is resolved
|
|
60
|
-
|
|
61
|
-
Skip this entirely if step 0 found no `/etc/otb/box.env` (a laptop — there's no box to close). On a box, resolving the claim does **not** stop the box billing; only closing it (or the idle-suspend sweep) does. Once the claim is resolved and the tree is clean, ask plainly:
|
|
62
|
-
|
|
63
|
-
> *"Close the box down too? `close-box` deprovisions it — anything in `/workspace` you haven't shipped is gone. Or leave it: closing the tab keeps your session, and the box idle-parks on its own (cheap)."*
|
|
64
|
-
|
|
65
|
-
- **Yes, shut it down** → run the **`close-box`** skill (`node scripts/gds/api.js POST /api/gds/box/close`). It enqueues a deprovision that runs within ~a minute. This **subsumes** worktree teardown — the whole box disk is going away, so step 3 was moot.
|
|
66
|
-
- **No, keep the box** → leave it; it idle-parks automatically. The close-out is complete (claim resolved, worktree torn down in step 3 if applicable). Tell them they can reopen the terminal to resume, or `/builder-box` to check/wake it later.
|
|
67
|
-
|
|
68
|
-
`close-box` is **own-scoped** — it only ever deprovisions the caller's own box. Never another builder's.
|
|
55
|
+
**On the main checkout (not a worktree):** skip the teardown above — there's nothing to remove. Resolving the claim + the clean-git verify is then the whole close-out.
|
|
69
56
|
|
|
70
57
|
## Constraints
|
|
71
58
|
|
|
72
|
-
- **Own-scoped.** Only this session's claim
|
|
59
|
+
- **Own-scoped.** Only this session's claim and this session's worktree — never another builder's claim, never another worktree, never `git worktree remove` a path you didn't create.
|
|
73
60
|
- **Never destroy uncommitted work.** The clean-git verify gates teardown; the dirty-tree refusal in `git worktree remove` and `git branch -d` (not `-D`) are the load-bearing safety nets — keep them as written. But note: that refusal does NOT protect a Windows worktree carrying a `node_modules` junction — node_modules is gitignored, so the clean check passes and `git worktree remove` (even without `--force`) follows the junction and deletes the SHARED node_modules (task 1002809). That is why teardown runs through `node scripts/gds/worktree.js remove`, which unlinks the junction first.
|
|
74
|
-
- **
|
|
75
|
-
- **
|
|
76
|
-
- **The destructive steps (teardown, box-close) never run unattended without a clear go.** Ship vs. release can be inferred from state (commits ⇒ ship, none ⇒ release) and proceed; removing the worktree ends the session and closing the box destroys it, so wait for an explicit yes before steps 3 and 4.
|
|
61
|
+
- **Never push / ssh / deploy.** Landing goes through `/builder-ship` (server-mediated); this skill stays local to the builder's machine and makes no API writes of its own.
|
|
62
|
+
- **Teardown never runs unattended without a clear go.** Ship vs. release can be inferred from state (commits ⇒ ship, none ⇒ release) and proceed; removing the worktree ends the session, so wait for an explicit yes before step 3.
|
|
77
63
|
- **Don't tear down a branch that isn't merged.** A released task or a ship stuck at `confirmed` still has an un-landed `claude/*` branch — `git branch -d` will refuse it; respect that and leave the worktree for `/merge-mode`.
|
|
78
64
|
|
|
79
65
|
## Files this skill touches
|
|
80
66
|
|
|
81
|
-
- Reads: `~/.config/otb/gds-session.json
|
|
82
|
-
- Runs: the `builder-ship` / `builder-release` skills (`scripts/gds/ship.js` · `release.js`), `node scripts/gds/api.js GET /api/gds/me`, and local `git worktree` / `git status` / `git branch` commands.
|
|
67
|
+
- Reads: `~/.config/otb/gds-session.json`
|
|
68
|
+
- Runs: the `builder-ship` / `builder-release` skills (`scripts/gds/ship.js` · `release.js`), `node scripts/gds/api.js GET /api/gds/me`, and local `git worktree` / `git status` / `git branch` commands. It makes no API writes of its own.
|
|
@@ -12,8 +12,8 @@ You are gracefully offboarding a builder from Bongos, or reactivating one who ha
|
|
|
12
12
|
|
|
13
13
|
`/builder-exit` is **not** how you end a working session. It permanently **offboards an entire builder** — an Archon deactivating *someone else's* account (releases all their claims, drops them to Xenos, blocks new claims). "Exit" is the natural word for "log me off," so builders reach for this first ([#1448](https://example.com/builders#/task/1448)) — but it's the wrong, and gated, tool for the everyday close-out. Before touching anything, check intent:
|
|
14
14
|
|
|
15
|
-
- **The caller wants to wrap up their *own* online-terminal / Claude Code session** — no builder id given, the id is their own, or they said "close out / end / sign off / I'm done" → **do not attempt an offboard.** Say plainly: *"`/builder-exit` offboards an entire builder (releases all their claims, drops them to Xenos) and is Archon-only — it isn't the session close-out. To close out your session, run **`/builder-end`**: it resolves your claim (ship or release) and
|
|
16
|
-
- **The caller is not an Archon** (rank below `archon`, or you can't confirm Archon) → same redirect. The endpoint would 403 (`rank_forbidden`) anyway; don't dead-end them on the error — point them at `/builder-end
|
|
15
|
+
- **The caller wants to wrap up their *own* online-terminal / Claude Code session** — no builder id given, the id is their own, or they said "close out / end / sign off / I'm done" → **do not attempt an offboard.** Say plainly: *"`/builder-exit` offboards an entire builder (releases all their claims, drops them to Xenos) and is Archon-only — it isn't the session close-out. To close out your session, run **`/builder-end`**: it resolves your claim (ship or release) and tears down the session's worktree."* Then stop and hand off to `/builder-end`.
|
|
16
|
+
- **The caller is not an Archon** (rank below `archon`, or you can't confirm Archon) → same redirect. The endpoint would 403 (`rank_forbidden`) anyway; don't dead-end them on the error — point them at `/builder-end`.
|
|
17
17
|
- **An Archon is deliberately offboarding (or reactivating) *another* builder, by id** → this is the real operation. Proceed with the steps below.
|
|
18
18
|
|
|
19
19
|
Never run the destructive offboard just because someone wanted to log off — the two are different operations.
|
|
@@ -25,15 +25,15 @@ The CLI drives a **three-state lifecycle** with a subagent grader between states
|
|
|
25
25
|
- **`GRADER UNAVAILABLE`** (`panel_outcome='unavailable'`) — the grading panel could not run (worker infra outage or a pre-spawn bypass). This is **not a quality rejection**: no quality signal was produced, there are no findings to fix, and the task parks at `completed` ungraded (aggregates exclude these rounds as `n_unavailable` — an outage never counts as a zero). Recovery: when the grader is reachable again, re-run just the grade step — `node scripts/gds/ship.js <id> --regrade`.
|
|
26
26
|
- **A genuine `FAIL`** — the panel ran and returned findings. Fix the real issues, commit on the claim branch (the grader reads committed history, not the working tree), then re-grade with the same command: `node scripts/gds/ship.js <id> --regrade`.
|
|
27
27
|
|
|
28
|
-
(
|
|
28
|
+
(Game-surface changes, `--approved`) Persists the builder's sandbox self-approval to `tasks.sandbox_review` — `{ by, at, stamp, url, method }`, with `by`/`at` stamped server-side ([#1048](https://example.com/builders#/task/1048), ADR 0046).
|
|
29
29
|
|
|
30
30
|
## How to use
|
|
31
31
|
|
|
32
32
|
1. **Confirm the task id** the user is shipping (it must match their currently-active claim).
|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**With game-surface changes, stage first (#927, ADR 0046 + #1048).** If the diff touches the game (`public/`, `src/world/`, `src/rooms/`, `server.js`), the flow is: stage → the builder reviews the change on their local sandbox preview → approve → ship. `ship.js` enforces this before the claim resolves: un-staged game work gets staged and the ship **aborts with a `🔍 REVIEW REQUIRED` block** carrying the URL (claim intact, nothing lost).
|
|
35
35
|
|
|
36
|
-
**When you see that block, position the sandbox link prominently for the builder.** `ship.js` also emits the deterministic lifecycle card for the `review` stage (an `[otb-card-html] <path>` marker) — if an in-feed visualization tool is available, render that card (its primary action is **Open in sandbox ↗**, the URL); it's the cleanest way to make the link impossible to miss. Either way, surface the URL as its own standalone, clickable line in chat — a clear callout, NOT buried inside a paragraph of command output — and say in one line what to look at. This is the moment the change is ready to review; the link must be impossible to miss. Let the builder open it, review, and suggest changes; iterate if needed. **Only once the builder explicitly approves, re-run with `node scripts/gds/ship.js <id> --approved`** — that records their sign-off as a durable `sandbox_review` fact on the task (who, when, and the content stamp of exactly what was reviewed). Never pass `--approved` yourself without a real human go; the pause + sign-off IS the feature. `--skip-stage` is the escape hatch (use sparingly — the gate exists so unreviewed game changes don't ride straight to prod).
|
|
36
|
+
**When you see that block, position the sandbox link prominently for the builder.** `ship.js` also emits the deterministic lifecycle card for the `review` stage (an `[otb-card-html] <path>` marker) — if an in-feed visualization tool is available, render that card (its primary action is **Open in sandbox ↗**, the URL); it's the cleanest way to make the link impossible to miss. Either way, surface the URL as its own standalone, clickable line in chat — a clear callout, NOT buried inside a paragraph of command output — and say in one line what to look at. This is the moment the change is ready to review; the link must be impossible to miss. Let the builder open it, review, and suggest changes; iterate if needed. **Only once the builder explicitly approves, re-run with `node scripts/gds/ship.js <id> --approved`** — that records their sign-off as a durable `sandbox_review` fact on the task (who, when, and the content stamp of exactly what was reviewed). Never pass `--approved` yourself without a real human go; the pause + sign-off IS the feature. `--skip-stage` is the escape hatch (use sparingly — the gate exists so unreviewed game changes don't ride straight to prod). Non-game diffs are unaffected.
|
|
37
37
|
|
|
38
38
|
2. **Compose the handoff notes (`--notes-file`)** following the structure in `docs/handoff-template.md`:
|
|
39
39
|
- What shipped (with confidence tags: verified-prod | verified-smoke | implemented-not-verified)
|
|
@@ -4,28 +4,24 @@ description: >-
|
|
|
4
4
|
Stage the working tree on the builder's own live preview to see a change in a browser BEFORE shipping. Own-scoped and machine-local. Triggers: "/builder-stage", "stage my work", "let me see it before shipping", "preview this change".
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
You are staging a builder's in-progress work on **their own** sandbox — the live game preview running on their
|
|
7
|
+
You are staging a builder's in-progress work on **their own** sandbox — the live game preview running on their own machine — so they can see the change in a browser, suggest fixes, and only then decide to ship it to the main game (ADR 0046).
|
|
8
8
|
|
|
9
9
|
## Background
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **Dev box:** `box-game-preview.service` serves the `/workspace` tree at `https://sandbox-<login>.example.com` (ADR 0044). Staging restarts that service.
|
|
14
|
-
- **Local full clone (laptop):** a private detached process serves it at `http://localhost:<port>` (default 3100, `$OTB_PREVIEW_PORT` to override), managed by `scripts/gds/local-preview.js` (task 1056). Staging (re)starts that process. No tunnel, no public URL — it never leaves your machine.
|
|
15
|
-
|
|
16
|
-
Either way staging = (re)start the preview, health-check it, and record a marker of exactly what tree state was staged. `/builder-ship` checks that marker: a game-surface diff that wasn't staged at its current state triggers the sandbox review gate before the claim resolves — on a box AND on a local clone.
|
|
11
|
+
Staging (re)starts the preview, which serves the **game-only** entry (`src/preview-server.js`, ADR 0052), degraded-DB (fully playable, no saved state, isolated from prod, no `/api/gds`): a private detached process at `http://localhost:<port>` (default 3100, `$OTB_PREVIEW_PORT` to override), managed by `scripts/gds/local-preview.js` (task 1056). No tunnel, no public URL — it never leaves your machine.
|
|
12
|
+
Staging = (re)start the preview, health-check it, and record a marker of exactly what tree state was staged. `/builder-ship` checks that marker: a game-surface diff that wasn't staged at its current state triggers the sandbox review gate before the claim resolves.
|
|
17
13
|
|
|
18
14
|
## How to use
|
|
19
15
|
|
|
20
|
-
1. **Run the stage command** from
|
|
16
|
+
1. **Run the stage command** from your clone root:
|
|
21
17
|
```bash
|
|
22
18
|
node scripts/gds/sandbox-stage.js
|
|
23
19
|
```
|
|
24
|
-
It (re)starts the preview, waits until the game answers, writes the staged marker, and prints the review URL (
|
|
20
|
+
It (re)starts the preview, waits until the game answers, writes the staged marker, and prints the review URL (`http://localhost:<port>`).
|
|
25
21
|
|
|
26
|
-
> **
|
|
22
|
+
> **First-time setup:** the preview needs the game's dependencies, so run `npm install` once in your clone. You can also drive the preview directly: `npm run preview` (start), `node scripts/gds/local-preview.js {status|restart|stop|logs}`. Reviewing via the Claude Code preview tools? A `local-sandbox` server is pre-wired in `.claude/launch.json`.
|
|
27
23
|
|
|
28
|
-
2. **Hand the builder the URL** and tell them plainly: this is *their* private copy of the game running on *their*
|
|
24
|
+
2. **Hand the builder the URL** and tell them plainly: this is *their* private copy of the game running on *their own machine* — nothing has shipped. Ask them to open it, walk around, and check the change.
|
|
29
25
|
|
|
30
26
|
> **Generated art?** If the change was a pixel-art tile, the builder can also see it at the **`/art`** path of the same sandbox URL (`…/art`) — the generated-art gallery (#1262), which shows every tile they've made *even when it isn't placed in the game world yet*. Point them there when the work is "I made a tile" rather than "I changed the map."
|
|
31
27
|
|
|
@@ -39,14 +35,12 @@ Either way staging = (re)start the preview, health-check it, and record a marker
|
|
|
39
35
|
|
|
40
36
|
## Constraints
|
|
41
37
|
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
-
|
|
45
|
-
- **Never present the sandbox as prod.** No credits, no broadcast, nothing shipped — staging is review, `/builder-ship` is the decision. The local preview is private to the builder's own machine (not shareable).
|
|
46
|
-
- If the preview won't come up healthy, point the builder at the logs — `box-preview logs` on a box, `node scripts/gds/local-preview.js logs` locally — rather than guessing.
|
|
38
|
+
- **A full local clone.** The ONLY machine with no sandbox is a partial clone that lacks the game (`src/preview-server.js`) — there the script says so and exits.
|
|
39
|
+
- **Never present the sandbox as prod.** No credits, no broadcast, nothing shipped — staging is review, `/builder-ship` is the decision. The preview is private to the builder's own machine (not shareable).
|
|
40
|
+
- If the preview won't come up healthy, point the builder at the logs — `node scripts/gds/local-preview.js logs` — rather than guessing.
|
|
47
41
|
|
|
48
42
|
## Files this skill touches
|
|
49
43
|
|
|
50
|
-
- Runs: `scripts/gds/sandbox-stage.js
|
|
51
|
-
- Reads:
|
|
44
|
+
- Runs: `scripts/gds/sandbox-stage.js`, which drives `scripts/gds/local-preview.js` + `local-preview-lib.js` to (re)start `node src/preview-server.js`
|
|
45
|
+
- Reads: `$OTB_PREVIEW_PORT` (local port, default 3100)
|
|
52
46
|
- Writes: the staged marker in the OS temp dir (`otb-sandbox-staged.json`); locally, the preview pid/log in the OS temp dir (`otb-local-preview.{pid,log}`)
|
|
@@ -29,10 +29,10 @@ If it exits non-zero, **stop**: this machine cannot finish the procedure, and th
|
|
|
29
29
|
partial run is the dangerous outcome (it mutates the main worktree). The skill's
|
|
30
30
|
`requires:` frontmatter is what it checks — `main-checkout`, `push-credential`,
|
|
31
31
|
`gh`, `droplet-ssh`. This gate exists because the note below was prose only, and
|
|
32
|
-
prose cannot refuse: merge-mode was run on a
|
|
33
|
-
`ship.js` separately already knew (`strandNextStep` tells a
|
|
34
|
-
NOT available here"). The knowledge existed twice and neither copy
|
|
35
|
-
(task 1003167).
|
|
32
|
+
prose cannot refuse: merge-mode was run on a machine that had none of them, while
|
|
33
|
+
`ship.js` separately already knew (`strandNextStep` tells such a machine
|
|
34
|
+
"/merge-mode is NOT available here"). The knowledge existed twice and neither copy
|
|
35
|
+
stopped the run (task 1003167).
|
|
36
36
|
|
|
37
37
|
> **Environment note:** merge-mode is an **ops procedure** — it mutates the single
|
|
38
38
|
> main worktree and deploys to the Linux droplet (owner-gated SSH). The git /
|
|
@@ -120,7 +120,7 @@ This mirrors what `ship.js` does automatically; it's here because /merge-mode de
|
|
|
120
120
|
|
|
121
121
|
> **Deploy mode (#679 / [ADR 0042](../../../docs/adr/0042-builder-self-deploy-ci-auto-merge.md)).** `config/deploy.json` is now `mode: ci` (CI cutover landed 2026-06-13), so the **`ci`** path is current: there is no laptop merge and no SSH at all — landing happens via PR auto-merge and [`.github/workflows/deploy-prod.yml`](../../../.github/workflows/deploy-prod.yml) deploys `main`→prod from Actions. In `ci` mode, do NOT run the `git push origin main` + `ssh … ~/deploy.sh` lines below — instead confirm the PR auto-merged and the `deploy-prod` Actions run went green. The block below is the **legacy `laptop`** deploy mode (the operator's machine pushes `main` and SSHes the droplet) — it is the fallback path and only applies if `config/deploy.json` is reverted to `mode: laptop`. Check the active mode with `node scripts/gds/ship.js --print-deploy-mode`.
|
|
122
122
|
|
|
123
|
-
> **Do NOT probe `parseDeployMode` directly (task 1003221).** It reads only the env var and `config/deploy.json`, and **defaults to `laptop`** when neither is present —
|
|
123
|
+
> **Do NOT probe `parseDeployMode` directly (task 1003221).** It reads only the env var and `config/deploy.json`, and **defaults to `laptop`** when neither is present — so on any machine whose checkout does not carry `config/`, that probe answers `laptop` every time. It sent a builder down the laptop merge path — local merge, `push-main.js`, SSH deploy — on a machine with none of those credentials (2026-08-21, 163 minutes). `effectiveDeployMode` is the credential-aware answer and reroutes to `ci` when the machine cannot push locally; the flag above prints that one.
|
|
124
124
|
|
|
125
125
|
```bash
|
|
126
126
|
node scripts/gds/smoke-gds.js # 16/16 expected (cross-platform; replaces smoke-gds.sh)
|
|
@@ -25,6 +25,8 @@ Five facts found during this spike shape the decisions:
|
|
|
25
25
|
|
|
26
26
|
**2. One safe read path, enforced next to the data.** The port exposes `getPublicProfileExtras(githubId)`: `null` unless the account exists, is publicly addressable (Decision 3), and `hide_stats` is false — otherwise `{ totals: { projects, credits, works_shipped, karma }, disciplines, last_active, projects: [{ name, origin, rank_held }...], more_projects }`, whitelist-shaped **inside the module** (`client_id` and raw rows never cross the port; non-numeric `github_id` values like the `system:*` principals return `null` instead of hitting a bigint cast error). Every surface that shows cross-project numbers reads through the module's filtered functions at render time, never from a stored copy — and that includes the two that already exist: **`listScoutingRollup` itself gains the `profile_state = 'active' AND NOT hide_stats` predicate and a whitelisted projection**, so `/people` and `/scouting` (live today at Metic floor) honor hide by construction, not by memory. The hub hall profile, the `/u/<handle>` page, recruiting (R14/R15) and applications (R09) all sit on the same functions, which is what makes C2's "honored live everywhere, including pending applications" structural. Two deliberate side doors, both narrow: `getOwnProfileExtras(githubId)` serves the builder their own numbers regardless of flags (Settings), and spike 1002785's recruiter flow is expected to add a second **authorization-scoped** read for its opt-in discoverability sliver — `hide_stats` is the invariant every reader honors; public addressability is not.
|
|
27
27
|
|
|
28
|
+
> **D2 amendment — which projects a profile may NAME (owner, 2026-09-16, task 1003558).** Until then a project was named only when `featured AND status = 'active'`, a predicate the public map dropped in R06 (ADR 0182 D3). It is now **on the map AND Public**: `onMapSql('p') AND p.visibility = 'public'`, where `onMapSql` is the feed's own fragment, so a profile and the map cannot disagree about who is on the map. `featured` gates no name. **Private is deliberately NOT named**, though its name is on the map: naming it on a profile publishes who works there, at what rank and on which works, which the map never says. Stealth stays unnamed (ADR 0182 D2a). Everything unnamed is counted in `more_projects`, and `recent_ships` follow the name.
|
|
29
|
+
|
|
28
30
|
**3. Consent is a fail-closed `profile_state` on `platform_identity_accounts`, with one discriminator.** Migration `platform_identity_007_profile_consent.sql` (lands with R02) adds `profile_state text NOT NULL DEFAULT 'provisional' CHECK (profile_state IN ('provisional','active'))` and `hide_stats boolean NOT NULL DEFAULT false`. The single rule: **an account is `active` iff it was touched by a hub-session `/sso/authorize`** — the one path where a human is demonstrably at the hub (and where T&C acceptance can later be captured, task 1002913). Device-poll and check-in create `provisional`; the rollup endpoint creates nothing (orphan activity rows are never publicly addressable). Backfill: **all existing accounts start `provisional`** and lazily upgrade on their next hub-session authorize — no join to core `builders` (that join is type-unsafe against `system:*` text ids and would put a core-table read inside a module migration; the price is that existing hub builders' profiles publish on their next sign-in rather than immediately). Public surfaces read only `active` accounts. **R02 ships this fail-closed default without waiting on P03**, because the asymmetry only runs one way: P03 can then only ever *widen* exposure, never claw back something already published. This deliberately narrows the Phase-4 interview's "no extra consent gate" to the accounts whose consent premise actually holds; P03 is the owner either ratifying that boundary (recommended) or reverting to the interview decision verbatim — and the revert branch has a named precondition: check-in/device-created identities have **no self-serve hide path** (no hub builder session exists for them; hub admission is invite-gated), so rejecting P03 first requires an assertion-authenticated hide path or open hub enrollment. Task 1002799 carries this full decision payload.
|
|
29
31
|
|
|
30
32
|
> **P03 outcome — RATIFIED (owner, 2026-08-16, task 1002799).** The fail-closed boundary stands: check-in/device-created accounts stay `provisional` (not publicly listed, not at `/u/<handle>`, not in the sitemap or hub search) until their human signs in at the hub at least once. The owner's rationale: in the intended product flow a builder passes through cloudbongos.com at least once anyway — to find or create the projects they join — so the activation gate is satisfied naturally and costs nothing; the cloudbongos builders hall predating the hub flow is a one-time bootstrap exception, and those accounts lazily activate on their next hub sign-in exactly as the backfill rule above already provides (no backfill activation). Tasks 1002913 (T&C artifact) and 1002914 (criterion text amendments) unblock on this ship.
|
|
@@ -169,7 +169,7 @@ The last sentence of this decision is therefore now spent — `stealth` unqualif
|
|
|
169
169
|
- Anything reading `featured = true AND status = 'active'` as "is this public" is now wrong and must be found. `modules/platform-identity/routes/projects.js` is the known site.
|
|
170
170
|
- **R16 makes D3's bound real, and D4's gate reach a second module.** Shipping R16 (task 1002335) surfaced one thing this ADR left implicit. D3 says `featured` may never put an unpublishable project on the map, so `PATCH /projects/:id/featured` now refuses a dark-matter row (409 `not_publishable`, `missing` in the details); un-featuring stays unconditional, since an already-featured husk must remain removable. And D4's "name what is missing" is owed to the owner *before* their project is ever catalogued on the hub — which is the `provisioning` plane, a different module that ADR 0083 forbids from importing `platform-identity`. Rather than let a two-field rule exist twice, the gate is published as a kernel seam port, `project.publishGate` (`modules/platform-identity/publish-gate.js`). `provisioning` resolves it **optionally** and declares no `consumes`: `platform-identity` is hub-only, the public map is a hub concept, and a platform without one must answer "no verdict" rather than invent a local rule — which is the same drift-avoidance argument that produced `publishableSql` in the first place.
|
|
171
171
|
|
|
172
|
-
- **R06 landed the predicate swap and the redaction, and settled three things this ADR left to the implementer** (task 1002325, `modules/platform-identity/projects-feed.js`). (a) **A black hole carries no stable per-project key.** D2a lists `name`/`tagline`/`art_url`/`origin`, and an id looks harmless beside them — but any identifier that survives a state change lets an observer who kept yesterday's public feed diff it against today's and *name* the project that went stealth. So stealth rows are byte-identical to one another and the feed grew no public id at all; the orbs page keys by position. (b) **The redaction is an ALLOW-list, not a deny-list** — every key nulled except `state` and `orb` — so a column added to the feed later is anonymous without anyone remembering to come back to it, and it NULLS rather than deletes, keeping the feed's "never drop a key" contract. (c) **An unrecognised `visibility` draws as a black hole, not a planet**: this mapping is a privacy boundary, so a fourth enum value added without mapping it must fail closed. Two smaller notes: the route PATH keeps the word `featured` (it is rank:public and CORS-open, already fetched by a static site and other origins — renaming it would break them for nothing, and `ORDER BY featured DESC` is where the flag's D3 ranking job now lives); and `getRollupForBuilder`'s `is_public` was deliberately left on the OLD `featured AND active` predicate rather than widened with the feed, because a stealth project is on the map *anonymously* and naming it in a builder's profile would be the exact leak D2a forbids — how much of a cross-project record may be NAMED is ADR 0171 D2's question, and under-naming is the safe direction meanwhile.
|
|
172
|
+
- **R06 landed the predicate swap and the redaction, and settled three things this ADR left to the implementer** (task 1002325, `modules/platform-identity/projects-feed.js`). (a) **A black hole carries no stable per-project key.** D2a lists `name`/`tagline`/`art_url`/`origin`, and an id looks harmless beside them — but any identifier that survives a state change lets an observer who kept yesterday's public feed diff it against today's and *name* the project that went stealth. So stealth rows are byte-identical to one another and the feed grew no public id at all; the orbs page keys by position. (b) **The redaction is an ALLOW-list, not a deny-list** — every key nulled except `state` and `orb` — so a column added to the feed later is anonymous without anyone remembering to come back to it, and it NULLS rather than deletes, keeping the feed's "never drop a key" contract. (c) **An unrecognised `visibility` draws as a black hole, not a planet**: this mapping is a privacy boundary, so a fourth enum value added without mapping it must fail closed. Two smaller notes: the route PATH keeps the word `featured` (it is rank:public and CORS-open, already fetched by a static site and other origins — renaming it would break them for nothing, and `ORDER BY featured DESC` is where the flag's D3 ranking job now lives); and `getRollupForBuilder`'s `is_public` was deliberately left on the OLD `featured AND active` predicate rather than widened with the feed, because a stealth project is on the map *anonymously* and naming it in a builder's profile would be the exact leak D2a forbids — how much of a cross-project record may be NAMED is ADR 0171 D2's question, and under-naming is the safe direction meanwhile. (Widened by task 1003558 to on-the-map AND Public; see the ADR 0171 D2 amendment.)
|
|
173
173
|
|
|
174
174
|
## Alternatives rejected
|
|
175
175
|
|
package/docs/file-map.md
CHANGED
|
@@ -393,11 +393,9 @@ tests/
|
|
|
393
393
|
├── blocker-solve/SKILL.md ← /blocker-solve N — drive ONE blocker to done: do the doable parts, hand back owner-only steps, verify, auto-resolve (auto-promotes waiters); Metic+
|
|
394
394
|
├── bug-triage/SKILL.md ← /bug-triage — daily walk through open kind=bug tasks (batch-cluster near-dups, merge / won't-fix); the idea-triage counterpart built on the new tasks.merge primitive (task 1001478); Metic+
|
|
395
395
|
├── builder-backup/SKILL.md ← /builder-backup — check DB backup status or trigger a fresh local dump before risky changes (migrations, destructive SQL); no SSH required
|
|
396
|
-
├── builder-box/SKILL.md ← /builder-box — show the builder their own dev box (state, IP/host, cost, SSH keys) and help them (re)connect
|
|
397
396
|
├── builder-claim/SKILL.md ← /builder-claim N — atomic claim; prints the claim-time context pack + discipline playbook routing
|
|
398
|
-
├── builder-connect/SKILL.md ← /builder-connect — one-command connect of the builder's Claude Desktop to their Example dev box (ADR 0031)
|
|
399
397
|
├── builder-cost/SKILL.md ← /builder-cost — log a cost-ledger entry (api|compute|infra|domain|art|other)
|
|
400
|
-
├── builder-end/SKILL.md ← /builder-end — safely close out a session: resolve the claim (ship/release) + clean-git verify + worktree teardown
|
|
398
|
+
├── builder-end/SKILL.md ← /builder-end — safely close out a session: resolve the claim (ship/release) + clean-git verify + worktree teardown; the bookend to /builder-start, and the own-scoped session close-out that /builder-exit redirects non-Archons to
|
|
401
399
|
├── builder-exit/SKILL.md ← /builder-exit — gracefully offboard (or reactivate) a builder: release claims, seal session logs, drop rank to xenos
|
|
402
400
|
├── builder-key/SKILL.md ← /builder-key — list API-key status, render the visual 'key needed' callout for anything missing, drive the UI-first Settings add-flow + re-sync/confirm; never accepts a key pasted in chat (task 1391)
|
|
403
401
|
├── builder-reauth/SKILL.md ← /builder-reauth — refresh an expired Bongos CLI session the UI-first way (no terminal Device Flow)
|
|
@@ -405,11 +403,9 @@ tests/
|
|
|
405
403
|
├── builder-release/SKILL.md ← /builder-release — abandon a claim (task returns to ready, no credits)
|
|
406
404
|
├── builder-sequence/SKILL.md ← /builder-sequence N — walk a whole dependency chain from one kickoff (claim → build → ship → next), one claim at a time; halts and reports on migration/security/protected-path/thin-spec/judgement tasks (scripts/gds/sequence.js, task 1002484)
|
|
407
405
|
├── builder-setup/SKILL.md ← /builder-setup — one-time GitHub Device Flow auth (Bongos); multi-builder-on-one-device gate
|
|
408
|
-
├── builder-ship/SKILL.md ← /builder-ship — record + award credits (runs touches[] scanner; warns on drift; sandbox-first review gate
|
|
409
|
-
├── builder-stage/SKILL.md ← /builder-stage — stage work on the builder's sandbox (
|
|
406
|
+
├── builder-ship/SKILL.md ← /builder-ship — record + award credits (runs touches[] scanner; warns on drift; sandbox-first review gate for game-surface changes, ADR 0046)
|
|
407
|
+
├── builder-stage/SKILL.md ← /builder-stage — stage work on the builder's own local sandbox preview (http://localhost:<port>) for browser review before shipping (#927, ADR 0046)
|
|
410
408
|
├── builder-start/SKILL.md ← /builder-start — list claimable tasks (parallel-safe vs active claims)
|
|
411
|
-
├── builder-sync/SKILL.md ← /builder-sync — 'sync now': re-run the box source-fetch on demand so a freshly-claimed task's code lands within seconds, re-scoped to the active claim (BV1.R96, ADR 0148)
|
|
412
|
-
├── close-box/SKILL.md ← /close-box — tear down the dev box you're working on, from inside the box terminal (POST /box/close)
|
|
413
409
|
├── design/SKILL.md ← /design — the UI design session: the operating playbook for ui-discipline work on ANY instance (load the instance's pack + DESIGN.md first, look before you ship, build inside the platform floors, ship the tests with the UI); routed from a `ui` claim via discipline-modes.json; shipped by the ui-design module (task 1003324, ADR 0197)
|
|
414
410
|
├── design-sync/SKILL.md ← /design-sync — export repo design tokens + surfaces into Claude Design (token-preview bundle) and land generated UI code back as a Bongos task (ADR 0081)
|
|
415
411
|
├── feedback/SKILL.md ← /feedback — pull the latest Bongos feedback bundle (prompt.md + screenshot file paths) into a Claude Code session; backed by scripts/gds/feedback-latest.js (BV1.R13, ADR 0087)
|
|
@@ -2028,5 +2028,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2028
2028
|
landed since 1.19.775 with no explicit bump. run 35135604764. (task 1002620)
|
|
2029
2029
|
1.19.777 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2030
2030
|
landed since 1.19.776 with no explicit bump. run 35137334438. (task 1002620)
|
|
2031
|
+
1.19.778 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2032
|
+
landed since 1.19.777 with no explicit bump. run 35139523999. (task 1002620)
|
|
2033
|
+
1.19.779 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2034
|
+
landed since 1.19.778 with no explicit bump. run 35144678111. (task 1002620)
|
|
2031
2035
|
---------------------------------------------------------------------------
|
|
2032
2036
|
```
|
|
@@ -42,8 +42,6 @@ Every one of these talks to the build system. They're grouped by when you'd reac
|
|
|
42
42
|
| `/builder-setup` | One-time enrolment — signs you in and registers you |
|
|
43
43
|
| `/builder-reauth` | Refresh an expired session |
|
|
44
44
|
| `/builder-key` | Check / set your own API key status |
|
|
45
|
-
| `/builder-box` | Your dev box control panel — wake, create, check cost |
|
|
46
|
-
| `/builder-connect` · `/builder-sync` · `/close-box` | Connect to, re-sync, and tear down that box |
|
|
47
45
|
| `/builder-cost` | Log a cost entry against your claim |
|
|
48
46
|
| `/builder-backup` | Check or trigger a database backup before risky work |
|
|
49
47
|
| `/builder-exit` | Offboard or reactivate a builder *(Archon only)* |
|
|
@@ -322,7 +322,7 @@ const {
|
|
|
322
322
|
// D1/D2a/D3; R06, task 1002325). Its own file for the publish-gate.js reason: this
|
|
323
323
|
// file is at the size ratchet. Re-exported below, so the surface is unchanged.
|
|
324
324
|
const {
|
|
325
|
-
ORB_BY_STATE, orbForState, isStealthProject, feedShapeFor, listPublicProjects, PUBLIC_FEED_MAX_PAGE_SIZE,
|
|
325
|
+
ORB_BY_STATE, orbForState, isStealthProject, feedShapeFor, listPublicProjects, PUBLIC_FEED_MAX_PAGE_SIZE, onMapSql,
|
|
326
326
|
} = require('./projects-feed');
|
|
327
327
|
|
|
328
328
|
// Upsert a catalog row (keyed by origin). Called when a client registers (so every
|
|
@@ -753,17 +753,14 @@ function isNumericGithubId(githubId) {
|
|
|
753
753
|
// listScoutingRollup's corrected shape: scalars over the UN-FANNED activity
|
|
754
754
|
// table, the disciplines union computed separately (never a pre-GROUP-BY unnest
|
|
755
755
|
// fan-out — bug task 1002911). Per-project rows join the module-internal
|
|
756
|
-
// client_id → sso_clients.origin → projects.origin two-hop
|
|
757
|
-
//
|
|
758
|
-
// Everything else is counted in more_projects, never named ("the aggregate is
|
|
759
|
-
// not the leak, the name is").
|
|
756
|
+
// client_id → sso_clients.origin → projects.origin two-hop. Everything not
|
|
757
|
+
// named is counted in more_projects ("the aggregate is not the leak, the name is").
|
|
760
758
|
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
// client_id never appears in the returned shape.
|
|
759
|
+
// NAMED = on the map by the feed's own predicate (onMapSql) AND Public — the
|
|
760
|
+
// owner's call on task 1003558 (ADR 0171 D2 amendment). A Private project is
|
|
761
|
+
// named on the map but not here: who works there, their rank and their work
|
|
762
|
+
// titles stay counted. Stealth is on the map anonymously (ADR 0182 D2a).
|
|
763
|
+
// `featured` only ranks (D3), so it gates no name. client_id never crosses.
|
|
767
764
|
async function getRollupForBuilder(githubId, { pool = defaultPool } = {}) {
|
|
768
765
|
if (!isNumericGithubId(githubId)) return null;
|
|
769
766
|
const { rows: aggRows } = await pool.query(
|
|
@@ -790,7 +787,7 @@ async function getRollupForBuilder(githubId, { pool = defaultPool } = {}) {
|
|
|
790
787
|
const agg = aggRows[0];
|
|
791
788
|
const { rows: projRows } = await pool.query(
|
|
792
789
|
`SELECT p.name, p.origin, a.rank_held, a.recent_ships,
|
|
793
|
-
(p.id IS NOT NULL AND p
|
|
790
|
+
(p.id IS NOT NULL AND ${onMapSql('p')} AND p.visibility = 'public') AS is_public
|
|
794
791
|
FROM platform_identity_builder_project_activity a
|
|
795
792
|
LEFT JOIN platform_identity_sso_clients c ON c.client_id = a.client_id
|
|
796
793
|
LEFT JOIN platform_identity_projects p ON p.origin = c.origin
|
|
@@ -207,7 +207,20 @@ const PUBLIC_FEED_MAX_PAGE_SIZE = 500;
|
|
|
207
207
|
// The same predicate written without the `visibility_source` half — or shipped without
|
|
208
208
|
// migration 020's backfill, which stamps every pre-existing row 'control-plane' — would
|
|
209
209
|
// empty the public map on deploy. Both halves are load-bearing.
|
|
210
|
-
|
|
210
|
+
function doorKnownSql(prefix = '') {
|
|
211
|
+
return `(${prefix}visibility_source = 'control-plane' OR ${prefix}visibility_reported_at IS NOT NULL)`;
|
|
212
|
+
}
|
|
213
|
+
const DOOR_KNOWN_SQL = doorKnownSql();
|
|
214
|
+
|
|
215
|
+
// ON THE MAP, as one SQL fragment: active, publishable and door-known. The feed's WHERE
|
|
216
|
+
// and a builder profile's naming rule (getRollupForBuilder, task 1003558) both read it,
|
|
217
|
+
// so a profile can never name a project by a different idea of "on the map" than the map's.
|
|
218
|
+
// publishableSql() refuses a non-identifier alias before this interpolates it too.
|
|
219
|
+
function onMapSql(alias) {
|
|
220
|
+
const publishable = publishableSql(alias);
|
|
221
|
+
const prefix = alias ? `${alias}.` : '';
|
|
222
|
+
return `(${prefix}status = 'active' AND ${publishable} AND ${doorKnownSql(prefix)})`;
|
|
223
|
+
}
|
|
211
224
|
|
|
212
225
|
// Every project ON THE MAP, in the public projection (no owner_github_id, no status) — ONE
|
|
213
226
|
// PAGE of it (task 1003561, which absorbed task 1003560). `visibility` is projected because
|
|
@@ -235,7 +248,7 @@ async function listPublicProjects({ pool = defaultPool, limit = PUBLIC_FEED_MAX_
|
|
|
235
248
|
const { rows } = await pool.query(
|
|
236
249
|
`SELECT origin, name, tagline, art_url, type, template, created_at, visibility
|
|
237
250
|
FROM platform_identity_projects
|
|
238
|
-
WHERE
|
|
251
|
+
WHERE ${onMapSql()}
|
|
239
252
|
ORDER BY ${IDENTIFIED_SQL} DESC,
|
|
240
253
|
(${IDENTIFIED_SQL} AND featured) DESC,
|
|
241
254
|
(CASE WHEN ${IDENTIFIED_SQL} THEN created_at END) DESC,
|
|
@@ -260,5 +273,6 @@ module.exports = {
|
|
|
260
273
|
feedShapeFor,
|
|
261
274
|
PUBLIC_FEED_MAX_PAGE_SIZE,
|
|
262
275
|
DOOR_KNOWN_SQL,
|
|
276
|
+
onMapSql,
|
|
263
277
|
listPublicProjects,
|
|
264
278
|
};
|
|
@@ -18,6 +18,7 @@ import { readFileSync } from 'node:fs';
|
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const P = require('../platform-identity.js');
|
|
21
|
+
const { onMapSql } = require('../projects-feed.js');
|
|
21
22
|
const terms = require('../terms-acceptance.js');
|
|
22
23
|
|
|
23
24
|
let passed = 0, failed = 0;
|
|
@@ -287,7 +288,7 @@ await ta('listPublicProjects returns a PUBLIC projection (on-the-map only, no ow
|
|
|
287
288
|
// R06 (task 1002325) replaced the `featured = true` gate with the derived publish
|
|
288
289
|
// gate: a project is on the map iff it is active AND publishable (ADR 0182 D3).
|
|
289
290
|
// The full predicate + the stealth redaction are pinned by tests/visible_projects_feed.mjs.
|
|
290
|
-
assert.match(c.text, /WHERE status = 'active' AND /);
|
|
291
|
+
assert.match(c.text, /WHERE \(status = 'active' AND /);
|
|
291
292
|
assert.ok(c.text.includes(P.publishableSql()), 'the map predicate is the shared gate');
|
|
292
293
|
// Pin the SELECT LIST itself, not the whole query — an ORDER BY mention must
|
|
293
294
|
// not satisfy a projection guard, and SELECT * must never sneak owner/status
|
|
@@ -681,6 +682,11 @@ await ta('getRollupForBuilder aggregates un-fanned + names only publicly-catalog
|
|
|
681
682
|
const aggCte = aggSql.slice(aggSql.indexOf('WITH agg AS'), aggSql.indexOf('disc AS'));
|
|
682
683
|
assert.doesNotMatch(aggCte, /unnest/i, 'scalar CTE must not fan rows out');
|
|
683
684
|
assert.match(aggSql.slice(aggSql.indexOf('disc AS')), /array_agg\(DISTINCT x\.d\)/);
|
|
685
|
+
// WHO is named (task 1003558): on the map by the feed's own predicate AND Public.
|
|
686
|
+
const isPublic = (pool.calls[1].text.match(/\(p\.id IS NOT NULL[\s\S]*?\) AS is_public/) || [''])[0];
|
|
687
|
+
assert.ok(isPublic.includes(onMapSql('p')), 'names read the feed\'s on-the-map predicate, not a restatement');
|
|
688
|
+
assert.match(isPublic, /AND p\.visibility = 'public'\) AS is_public$/, 'Private and Stealth are counted, never named');
|
|
689
|
+
assert.doesNotMatch(isPublic, /featured/, 'featured only ranks the feed (ADR 0182 D3); it gates no name');
|
|
684
690
|
});
|
|
685
691
|
|
|
686
692
|
await ta('getPublicProfileExtras is fail-closed: null for missing / provisional / hidden accounts', async () => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.779",
|
|
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.779",
|
|
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.779",
|
|
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/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.779'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -237,6 +237,19 @@ await t('getRollupForBuilder attaches recent_ships to publicly-named projects on
|
|
|
237
237
|
assert.ok(!serialized.includes('SECRET-PROJECT'), 'nor does its name (the pre-existing rule this one follows)');
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
await t('works follow the NAME: the one statement that reads recent_ships names Public projects only (task 1003558)', async () => {
|
|
241
|
+
// A second, wider read of the works would let a Private project's titles out while
|
|
242
|
+
// its name stayed counted. So the works and the naming verdict must share a row.
|
|
243
|
+
const pool = mockPool((sql) => (/WITH agg AS/.test(sql)
|
|
244
|
+
? { rows: [{ projects: 0, credits: 0, works_shipped: 0, karma: 0, last_active: null, disciplines: null }] }
|
|
245
|
+
: { rows: [] }));
|
|
246
|
+
await pi.getRollupForBuilder(42, { pool });
|
|
247
|
+
const reads = pool.calls.filter((c) => /recent_ships/.test(c.text));
|
|
248
|
+
assert.equal(reads.length, 1, 'exactly one statement reads the works');
|
|
249
|
+
assert.match(reads[0].text, /AND p\.visibility = 'public'\) AS is_public/, 'and it decides the name on the same row');
|
|
250
|
+
assert.doesNotMatch(reads[0].text, /featured/, 'featured gates no name, so it gates no work either');
|
|
251
|
+
});
|
|
252
|
+
|
|
240
253
|
await t('a project that reported no works reads as an empty list, not null', async () => {
|
|
241
254
|
const pool = mockPool((sql) => {
|
|
242
255
|
if (/WITH agg AS/.test(sql)) return { rows: [{ projects: 1, credits: 0, works_shipped: 0, karma: 0, last_active: null, disciplines: null }] };
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// /scouting by construction. Consent is TWO columns since ADR 0213
|
|
9
9
|
// (profile_state='active' AND terms_accepted_at IS NOT NULL), and the
|
|
10
10
|
// LEGACY fixture is the pre-terms row that proves the second one.
|
|
11
|
-
// 2. getRollupForBuilder aggregates un-fanned and names ONLY projects
|
|
12
|
-
//
|
|
13
|
-
// counted in more_projects, never named.
|
|
11
|
+
// 2. getRollupForBuilder aggregates un-fanned and names ONLY projects that are
|
|
12
|
+
// on the map AND Public (task 1003558) — featured or not; Private, Stealth
|
|
13
|
+
// and door-unknown rows are counted in more_projects, never named.
|
|
14
|
+
// client_id never crosses.
|
|
14
15
|
// 3. getPublicProfileExtras is fail-closed (missing/provisional/hidden/
|
|
15
16
|
// non-numeric → null); getOwnProfileExtras serves the self view anyway.
|
|
16
17
|
// 4. upsertAccount's consent upgrade is one-way (activate flips provisional →
|
|
@@ -53,15 +54,18 @@ try {
|
|
|
53
54
|
const ACTIVE = 90002278, HIDDEN = 90002279, PROV = 90002280, LAZY = 90002281, LEGACY = 90002283;
|
|
54
55
|
const ALL_IDS = [ACTIVE, HIDDEN, PROV, LAZY, LEGACY];
|
|
55
56
|
const PUB_CLIENT = 't1002278-pub', PRIV_CLIENT = 't1002278-priv', LOST_CLIENT = 't1002278-lost';
|
|
56
|
-
const
|
|
57
|
+
const STEALTH_CLIENT = 't1003558-stealth', UNREAD_CLIENT = 't1003558-unread';
|
|
58
|
+
const ALL_CLIENTS = [PUB_CLIENT, PRIV_CLIENT, LOST_CLIENT, STEALTH_CLIENT, UNREAD_CLIENT];
|
|
57
59
|
const PUB_ORIGIN = 'https://t1002278-pub.example', PRIV_ORIGIN = 'https://t1002278-priv.example';
|
|
60
|
+
const STEALTH_ORIGIN = 'https://t1003558-stealth.example', UNREAD_ORIGIN = 'https://t1003558-unread.example';
|
|
61
|
+
const ALL_ORIGINS = [PUB_ORIGIN, PRIV_ORIGIN, STEALTH_ORIGIN, UNREAD_ORIGIN];
|
|
58
62
|
const q = (sql, params) => pool.query(sql, params);
|
|
59
63
|
|
|
60
64
|
async function cleanup() {
|
|
61
65
|
await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [ALL_IDS]).catch(() => {});
|
|
62
66
|
await q(`DELETE FROM platform_identity_project_memberships WHERE github_id = ANY($1::bigint[])`, [ALL_IDS]).catch(() => {});
|
|
63
67
|
await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [ALL_IDS]).catch(() => {});
|
|
64
|
-
await q(`DELETE FROM platform_identity_projects WHERE origin = ANY($1::text[])`, [
|
|
68
|
+
await q(`DELETE FROM platform_identity_projects WHERE origin = ANY($1::text[])`, [ALL_ORIGINS]).catch(() => {});
|
|
65
69
|
await q(`DELETE FROM platform_identity_sso_clients WHERE client_id = ANY($1::text[])`, [ALL_CLIENTS]).catch(() => {});
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -75,12 +79,20 @@ try {
|
|
|
75
79
|
// (012), and the consent floor tests terms_accepted_at (019). Without them a
|
|
76
80
|
// FRESH database answers 42703 "column does not exist" — an infrastructure
|
|
77
81
|
// error dressed up as a boundary failure — instead of a clean assertion.
|
|
82
|
+
// 011 and 020 carry the columns the naming rule reads (description, visibility,
|
|
83
|
+
// the door-known pair); 012_recent_ships the column the rollup SELECTs; 008 and
|
|
84
|
+
// 009 the handle and display_name_source columns upsertAccount writes.
|
|
78
85
|
await q(migrationSql('platform_identity_001_tables.sql'));
|
|
79
86
|
await q(migrationSql('platform_identity_004_projects.sql'));
|
|
80
87
|
await q(migrationSql('platform_identity_005_builder_activity.sql'));
|
|
81
88
|
await q(migrationSql('platform_identity_007_profile_consent.sql'));
|
|
89
|
+
await q(migrationSql('platform_identity_008_handle.sql'));
|
|
90
|
+
await q(migrationSql('platform_identity_009_profile_fields.sql'));
|
|
91
|
+
await q(migrationSql('platform_identity_011_project_visibility.sql'));
|
|
82
92
|
await q(migrationSql('platform_identity_012_account_privacy.sql'));
|
|
93
|
+
await q(migrationSql('platform_identity_012_recent_ships.sql'));
|
|
83
94
|
await q(migrationSql('platform_identity_019_terms_acceptance.sql'));
|
|
95
|
+
await q(migrationSql('platform_identity_020_visibility_pull.sql'));
|
|
84
96
|
await cleanup();
|
|
85
97
|
|
|
86
98
|
// CONSENT IS TWO COLUMNS (ADR 0213, task 1002913): profile_state='active' AND
|
|
@@ -96,27 +108,39 @@ try {
|
|
|
96
108
|
($4, 'legacy-b', 'Legacy', 'active', false, NULL)`,
|
|
97
109
|
[ACTIVE, HIDDEN, PROV, LEGACY],
|
|
98
110
|
);
|
|
99
|
-
//
|
|
100
|
-
//
|
|
111
|
+
// Five reporting projects (task 1003558). All but Lost are active and described;
|
|
112
|
+
// only Pub is on the map AND Public — and it is the one that is NOT featured, so
|
|
113
|
+
// the old `featured` gate would name exactly the wrong rows:
|
|
114
|
+
// Pub public, unfeatured, door assigned → named
|
|
115
|
+
// Priv private, featured, door assigned → counted
|
|
116
|
+
// Stealth stealth, featured, door assigned → counted
|
|
117
|
+
// Unread public, featured, door never read → counted (dark matter)
|
|
118
|
+
// Lost no catalog row at all → counted
|
|
101
119
|
await q(
|
|
102
120
|
`INSERT INTO platform_identity_sso_clients (client_id, secret_hash, name, origin)
|
|
103
|
-
VALUES ($1, 'x', 'Pub Proj', $
|
|
104
|
-
|
|
121
|
+
VALUES ($1, 'x', 'Pub Proj', $6), ($2, 'x', 'Priv Proj', $7), ($3, 'x', 'Lost Proj', NULL),
|
|
122
|
+
($4, 'x', 'Stealth Proj', $8), ($5, 'x', 'Unread Proj', $9)`,
|
|
123
|
+
[...ALL_CLIENTS, ...ALL_ORIGINS],
|
|
105
124
|
);
|
|
106
125
|
await q(
|
|
107
|
-
`INSERT INTO platform_identity_projects (origin, name, featured, status)
|
|
108
|
-
VALUES ($1, 'Pub Proj',
|
|
109
|
-
|
|
126
|
+
`INSERT INTO platform_identity_projects (origin, name, description, featured, status, visibility, visibility_source)
|
|
127
|
+
VALUES ($1, 'Pub Proj', 'described', false, 'active', 'public', 'control-plane'),
|
|
128
|
+
($2, 'Priv Proj', 'described', true, 'active', 'private', 'control-plane'),
|
|
129
|
+
($3, 'Stealth Proj', 'described', true, 'active', 'stealth', 'control-plane'),
|
|
130
|
+
($4, 'Unread Proj', 'described', true, 'active', 'public', NULL)`,
|
|
131
|
+
ALL_ORIGINS,
|
|
110
132
|
);
|
|
111
133
|
await q(
|
|
112
134
|
`INSERT INTO platform_identity_builder_project_activity
|
|
113
135
|
(client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, last_active)
|
|
114
|
-
VALUES ($1, $
|
|
115
|
-
($2, $
|
|
116
|
-
($3, $
|
|
117
|
-
($
|
|
118
|
-
($
|
|
119
|
-
($1, $7,
|
|
136
|
+
VALUES ($1, $6, 100, 5, 3, 'metic', '{engineer,ui}', '2026-01-01T00:00:00Z'),
|
|
137
|
+
($2, $6, 50, 2, 1, 'archon', '{artist}', '2026-02-01T00:00:00Z'),
|
|
138
|
+
($3, $6, 25, 1, 0, NULL, '{}', '2026-03-01T00:00:00Z'),
|
|
139
|
+
($4, $6, 0, 0, 0, 'thetes', '{}', '2025-12-01T00:00:00Z'),
|
|
140
|
+
($5, $6, 0, 0, 0, 'thetes', '{}', '2025-12-01T00:00:00Z'),
|
|
141
|
+
($1, $7, 60, 4, 2, 'metic', '{engineer}', '2026-01-01T00:00:00Z'),
|
|
142
|
+
($1, $8, 10, 1, 0, 'xenos', '{engineer}', '2026-01-01T00:00:00Z'),
|
|
143
|
+
($1, $9, 90, 3, 1, 'metic', '{engineer}', '2026-01-01T00:00:00Z')`,
|
|
120
144
|
[...ALL_CLIENTS, ACTIVE, HIDDEN, PROV, LEGACY],
|
|
121
145
|
);
|
|
122
146
|
|
|
@@ -133,18 +157,20 @@ try {
|
|
|
133
157
|
|
|
134
158
|
// 2 — getRollupForBuilder semantics.
|
|
135
159
|
const r = await pi.getRollupForBuilder(ACTIVE, { pool });
|
|
136
|
-
assert.deepEqual(r.totals, { projects:
|
|
160
|
+
assert.deepEqual(r.totals, { projects: 5, credits: 175, works_shipped: 8, karma: 4 });
|
|
137
161
|
pass++; console.log(' ok - totals aggregate un-fanned across the builder\'s projects');
|
|
138
162
|
assert.deepEqual([...r.disciplines].sort(), ['artist', 'engineer', 'ui']);
|
|
139
163
|
pass++; console.log(' ok - disciplines are the deduped union');
|
|
140
164
|
ok(new Date(r.last_active).toISOString() === '2026-03-01T00:00:00.000Z', 'last_active is the max');
|
|
141
|
-
ok(r.projects.length === 1 && r.projects[0].name === 'Pub Proj', 'only the
|
|
142
|
-
assert.deepEqual(Object.keys(r.projects[0]).sort(), ['name', 'origin', 'rank_held']);
|
|
143
|
-
pass++; console.log(' ok - named rows carry exactly name/origin/rank_held');
|
|
165
|
+
ok(r.projects.length === 1 && r.projects[0].name === 'Pub Proj', 'only the on-the-map Public project is named — unfeatured, and named anyway');
|
|
166
|
+
assert.deepEqual(Object.keys(r.projects[0]).sort(), ['name', 'origin', 'rank_held', 'recent_ships']);
|
|
167
|
+
pass++; console.log(' ok - named rows carry exactly name/origin/rank_held/recent_ships');
|
|
144
168
|
ok(r.projects[0].rank_held === 'metic', 'rank_held is that project\'s reported standing');
|
|
145
|
-
ok(r.more_projects ===
|
|
169
|
+
ok(r.more_projects === 4, 'Private, Stealth, door-unknown and uncatalogued projects are counted, never named');
|
|
146
170
|
ok(!JSON.stringify(r).includes('client_id'), 'client_id never crosses the shape');
|
|
147
|
-
|
|
171
|
+
for (const unnamed of ['Priv Proj', 'Stealth Proj', 'Unread Proj', PRIV_ORIGIN, STEALTH_ORIGIN, UNREAD_ORIGIN]) {
|
|
172
|
+
ok(!JSON.stringify(r).includes(unnamed), `"${unnamed}" never crosses`);
|
|
173
|
+
}
|
|
148
174
|
|
|
149
175
|
// 3 — the port reads.
|
|
150
176
|
ok(await pi.getPublicProfileExtras(HIDDEN, { pool }) === null, 'public read → null for a hidden builder');
|
package/tests/profile_route.mjs
CHANGED
|
@@ -345,16 +345,37 @@ const HIDDEN_LOGIN = 'hidden-DO-NOT-LEAK-r06';
|
|
|
345
345
|
const PROV_LOGIN = 'provisional-DO-NOT-LEAK-r06';
|
|
346
346
|
const PRIVATE_LOGIN = 'private-DO-NOT-LEAK-r06';
|
|
347
347
|
|
|
348
|
-
// TWO projects — one
|
|
349
|
-
//
|
|
348
|
+
// TWO projects, both on the map — one Public and UNFEATURED, one Private and
|
|
349
|
+
// FEATURED — so neither the old `featured` gate nor a door-blind one passes
|
|
350
|
+
// (task 1003558). Distinctive digits so the "no partial numbers" scan below
|
|
351
|
+
// cannot pass by coincidence.
|
|
352
|
+
const ON_MAP = { status: 'active', description: 'described', visibility_source: 'control-plane', visibility_reported_at: null };
|
|
350
353
|
const PROOF_PROJECTS = [
|
|
351
354
|
{ name: 'R06 Public Proj', origin: 'https://r06-pub.example', rank_held: 'metic',
|
|
352
|
-
credits: 60271, tasks_shipped: 4103, karma: 9021,
|
|
355
|
+
credits: 60271, tasks_shipped: 4103, karma: 9021, catalog: { ...ON_MAP, visibility: 'public', featured: false } },
|
|
353
356
|
{ name: SECRET_PROJECT_NAME, origin: 'https://r06-priv.example', rank_held: 'archon',
|
|
354
|
-
credits: 40129, tasks_shipped: 2011, karma: 3007,
|
|
357
|
+
credits: 40129, tasks_shipped: 2011, karma: 3007, catalog: { ...ON_MAP, visibility: 'private', featured: true } },
|
|
355
358
|
];
|
|
356
359
|
const sumOf = (k) => PROOF_PROJECTS.reduce((n, p) => n + p[k], 0);
|
|
357
360
|
|
|
361
|
+
// getRollupForBuilder's `is_public`, evaluated the scouting engine's way: a catalog
|
|
362
|
+
// row is named unless a predicate the SQL actually DECLARES refuses it. Drop
|
|
363
|
+
// `p.visibility = 'public'` from the module and the Private project gets named here.
|
|
364
|
+
function namedByDeclaredSql(sql, project) {
|
|
365
|
+
const expr = (sql.match(/\(p\.id IS NOT NULL[\s\S]*?\) AS is_public/) || [''])[0];
|
|
366
|
+
const c = { name: project.name, ...project.catalog };
|
|
367
|
+
const declared = [
|
|
368
|
+
[/p\.featured/, c.featured === true],
|
|
369
|
+
[/p\.status = 'active'/, c.status === 'active'],
|
|
370
|
+
[/coalesce\(p\.name, ''\)\) <> ''/, String(c.name ?? '').trim() !== ''],
|
|
371
|
+
[/coalesce\(p\.description, ''\)\) <> ''/, String(c.description ?? '').trim() !== ''],
|
|
372
|
+
[/p\.visibility_source = 'control-plane' OR p\.visibility_reported_at IS NOT NULL/,
|
|
373
|
+
c.visibility_source === 'control-plane' || c.visibility_reported_at != null],
|
|
374
|
+
[/p\.visibility = 'public'/, c.visibility === 'public'],
|
|
375
|
+
];
|
|
376
|
+
return expr !== '' && declared.every(([re, holds]) => !re.test(expr) || holds);
|
|
377
|
+
}
|
|
378
|
+
|
|
358
379
|
// The one account row the reads consult. setAccountHideStats mutates it.
|
|
359
380
|
// ADR 0213 (task 1002913): consent is profile_state AND an acceptance of the
|
|
360
381
|
// platform terms, both stamped by the same upsertAccount branch.
|
|
@@ -415,7 +436,8 @@ const engine = {
|
|
|
415
436
|
}
|
|
416
437
|
// getRollupForBuilder's per-project rows (the client_id → origin two-hop).
|
|
417
438
|
if (/platform_identity_sso_clients/.test(sql)) {
|
|
418
|
-
return { rows: PROOF_PROJECTS.map((p) => ({ name: p.name, origin: p.origin, rank_held: p.rank_held,
|
|
439
|
+
return { rows: PROOF_PROJECTS.map((p) => ({ name: p.name, origin: p.origin, rank_held: p.rank_held,
|
|
440
|
+
is_public: namedByDeclaredSql(sql, p), recent_ships: [] })) };
|
|
419
441
|
}
|
|
420
442
|
// getRollupForBuilder's un-fanned aggregate — summed ACROSS both projects.
|
|
421
443
|
if (/AS works_shipped/.test(sql)) {
|
|
@@ -460,9 +482,38 @@ await t('R06.1 a two-project builder\'s COMBINED record shows to another account
|
|
|
460
482
|
assert.equal(cp.totals.works_shipped, sumOf('tasks_shipped'), 'ships are summed across both');
|
|
461
483
|
assert.equal(cp.totals.karma, sumOf('karma'), 'karma is summed across both');
|
|
462
484
|
assert.deepEqual(cp.disciplines, ['engineer', 'ui'], 'the disciplines union spans both projects');
|
|
463
|
-
assert.equal(cp.projects.length, 1, 'only the
|
|
485
|
+
assert.equal(cp.projects.length, 1, 'only the Public project is NAMED — unfeatured, and named anyway');
|
|
464
486
|
assert.equal(cp.projects[0].name, PROOF_PROJECTS[0].name);
|
|
465
|
-
assert.equal(cp.more_projects, 1, 'the
|
|
487
|
+
assert.equal(cp.more_projects, 1, 'the Private one is counted, never named — featured, and unnamed anyway');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await t('R06.1 a name needs the map AND the Public door — every other shape is counted, never named (task 1003558)', async () => {
|
|
491
|
+
const [pub, other] = PROOF_PROJECTS;
|
|
492
|
+
const original = other.catalog;
|
|
493
|
+
const unnamed = {
|
|
494
|
+
private: { ...original, visibility: 'private' },
|
|
495
|
+
stealth: { ...original, visibility: 'stealth' },
|
|
496
|
+
'public, door never read': { ...original, visibility: 'public', visibility_source: null, visibility_reported_at: null },
|
|
497
|
+
'public, no description (dark matter)': { ...original, visibility: 'public', description: ' ' },
|
|
498
|
+
'public, hidden from the catalog': { ...original, visibility: 'public', status: 'hidden' },
|
|
499
|
+
};
|
|
500
|
+
try {
|
|
501
|
+
for (const [label, catalog] of Object.entries(unnamed)) {
|
|
502
|
+
other.catalog = catalog;
|
|
503
|
+
const r = await get(port, '/api/gds/builders/95/profile');
|
|
504
|
+
const cp = r.json.cross_project;
|
|
505
|
+
assert.deepEqual(cp.projects.map((p) => p.name), [pub.name], `${label}: only the Public project is named`);
|
|
506
|
+
assert.equal(cp.more_projects, 1, `${label}: counted`);
|
|
507
|
+
assert.ok(!r.text.includes(SECRET_PROJECT_NAME) && !r.text.includes(other.origin), `${label}: neither its name nor its origin crosses`);
|
|
508
|
+
}
|
|
509
|
+
// The positive control: the same row IS named once Public and on the map, so the
|
|
510
|
+
// refusals above are the door, not a fixture nothing could ever name.
|
|
511
|
+
other.catalog = { ...original, visibility: 'public' };
|
|
512
|
+
const r = await get(port, '/api/gds/builders/95/profile');
|
|
513
|
+
assert.deepEqual(r.json.cross_project.projects.map((p) => p.name), [pub.name, SECRET_PROJECT_NAME]);
|
|
514
|
+
} finally {
|
|
515
|
+
other.catalog = original;
|
|
516
|
+
}
|
|
466
517
|
});
|
|
467
518
|
|
|
468
519
|
await t('R06.1 the record is viewer-INDEPENDENT — another account sees the same body', async () => {
|
|
@@ -463,7 +463,9 @@ test('dark matter never reaches the route at all — the exclusion is in SQL', a
|
|
|
463
463
|
const src = require('node:fs').readFileSync(
|
|
464
464
|
new URL('../modules/platform-identity/projects-feed.js', import.meta.url), 'utf8');
|
|
465
465
|
const fn = (src.match(/async function listPublicProjects[\s\S]*?\n\}/) || [''])[0];
|
|
466
|
-
assert.ok(fn.includes('
|
|
466
|
+
assert.ok(fn.includes('onMapSql()'), 'the gate is applied in the query, not after it');
|
|
467
|
+
const feed = require('../modules/platform-identity/projects-feed.js');
|
|
468
|
+
assert.ok(feed.onMapSql().includes(pi.publishableSql()), 'and on-the-map carries the publish gate');
|
|
467
469
|
});
|
|
468
470
|
|
|
469
471
|
// ── the route's half of the page window (task 1003560 → 1003561) ─────────────
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: builder-box
|
|
3
|
-
description: >-
|
|
4
|
-
Show the builder their OWN dev box — state, address, cost, keys — and get them back on it. Own-scoped; never parks or destroys a box. Triggers: "/builder-box", "is my box up", "wake my box", "what's my sandbox URL".
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
You are showing a builder the status of **their own** dev box and helping them get (back) onto it. This is a read-only, own-scoped helper — it never mutates infrastructure (provision / park / wake / deprovision live on the operator control plane, `node scripts/gds/box.js`, gated by rank).
|
|
8
|
-
|
|
9
|
-
## Background
|
|
10
|
-
|
|
11
|
-
The per-builder box lifecycle (ADR 0031 §2) auto-suspends when idle (snapshot-and-park) and recreates on demand, so a builder's box is often `parked` between sessions — that's normal and cheap, not a problem. **The idle sweep parks; it does not destroy** (task 1002726 — it *did* destroy droplet and snapshot for a long stretch, so a builder who remembers losing work that way is remembering something real that is now fixed). Full reclaim happens only after `BOX_DORMANT_DAYS` (14) parked, and **only after a warning was actually delivered** — an unwarned box is deferred, not reclaimed (task 1002727). The read API:
|
|
12
|
-
- `GET /api/gds/box/me` → the caller's box: `state`, `scope`, `ip`, `hostname`, cost, activity timestamps.
|
|
13
|
-
- `GET /api/gds/box/ssh-key` → the caller's registered SSH keys (fingerprints + labels).
|
|
14
|
-
- `GET /api/gds/box/terminal` → the caller's active box's browser-terminal `url` + basic-auth `credential` (the Chromebook/browser on-ramp, ADR 0038), once the box has published them.
|
|
15
|
-
|
|
16
|
-
**The hands-off on-ramp (#701).** A builder no longer needs an operator to get or wake a box. `POST /api/gds/box/ensure` is the "make my box ready" button: it **auto-provisions** if they have no box, or **wakes** a parked one — by queuing a request the control plane runs within ~1 min (the web server never touches DigitalOcean directly). For a Chromebook, after `ensure` reports `active`, `GET /box/terminal` returns the terminal `url` + `credential` (ADR 0038) to open in the browser.
|
|
17
|
-
|
|
18
|
-
## How to use
|
|
19
|
-
|
|
20
|
-
1. **Fetch the box + keys + the resolved scope** (own-scoped; uses the builder's session token):
|
|
21
|
-
```bash
|
|
22
|
-
node -e 'const{apiCall}=require("./scripts/gds/cli-lib");(async()=>{for(const p of ["/api/gds/box/me","/api/gds/box/ssh-key","/api/gds/box/scope"]){const r=await apiCall("GET",p);console.log(p,r.status,JSON.stringify(r.data));}})()'
|
|
23
|
-
```
|
|
24
|
-
(If the script returns "no Bongos session", run `/builder-setup` first.)
|
|
25
|
-
|
|
26
|
-
**Always report the scope, not just the state** (task 1002731). `/box/scope` returns `{scope, modules, sparse_paths, task_scope}`, and a too-narrow box is otherwise a *silent* failure — the builder sees a working box that is quietly missing the code they need. Read it out:
|
|
27
|
-
- `scope: 'task'` → name the modules: *"your box holds dev-box, hall-ui — and nothing else."*
|
|
28
|
-
- `scope: 'base'` → **say plainly that the box has no `modules/` code at all.** With no active claim that is expected; claiming a task and running `/builder-sync` fixes it.
|
|
29
|
-
- `scope: 'full'` with `modules: []` → the task-1002729 fallback fired: they hold claims, but those claims' goals declare no `scope_modules`, so the box fell back to their rank. Usable, but task-scoping is not actually doing anything — setting the goal's wall would give a tighter box.
|
|
30
|
-
- `scope: 'starter'` → the fixed newcomer slice, not scoped to their claim.
|
|
31
|
-
|
|
32
|
-
2. **Relay it in plain language:**
|
|
33
|
-
- **state = `none`** → they have no box yet. On a Mac/Windows: run `/builder-connect` (registers their SSH key + installs the Desktop file). To actually create the box hands-off, `POST /api/gds/box/ensure` queues the auto-provision (no operator CLI needed). *(The old path — ask an operator to `box.js onboard <login>` — still works but is no longer required.)*
|
|
34
|
-
- **state = `active`** → box is up at `hostname` (`ip`). They can connect from Claude Desktop → Code → Environment → "Example Dev Box". For a browser/Chromebook (ADR 0038), the terminal comes from the web: `GET /api/gds/box/terminal` returns a `url` + a basic-auth `credential` (username `otb`, password = <redacted>; open the url, enter them, then type `claude` in the terminal. No SSH needed.
|
|
35
|
-
- **Live game preview — the sandbox (ADR 0044).** If `/box/terminal` returns a `url` (i.e. the box is on the named tunnel), the builder also has a private live preview of the game running on their own box, at the **same hostname with `term-` swapped for `sandbox-`** — e.g. terminal `https://term-<login>.example.com` → preview `https://sandbox-<login>.example.com`. It serves their working copy of the game (degraded — fully playable, no saved state), so they can review changes in a browser **without shipping to prod**. The loop: edit in the terminal → run **`/builder-stage`** (or `box-preview restart`) → refresh the preview tab. Staging via `/builder-stage` also records the review marker that `/builder-ship`'s sandbox-first gate checks (#927, ADR 0046) — on a dev box, game changes are staged + reviewed on the sandbox before they ship to the main game. `box-preview status` shows whether it's up + the URL; `box-preview logs` tails the server. (No preview on a quick-tunnel box — that path can't carry the game port; it needs the named tunnel.)
|
|
36
|
-
- **state = `parked`** → idle-suspended (cheap, expected). It wakes on demand: `POST /api/gds/box/ensure` queues the wake and it's back in ~1 min (no operator needed). Their data IS safe in the snapshot — say so plainly, and mean it: the idle path preserves the filesystem, and the only thing that deletes the snapshot warns them first with days of lead time.
|
|
37
|
-
- **state = `destroyed`** → the box is genuinely gone, and so is anything uncommitted on it. This is NOT the idle path — it means they closed it themselves, an operator deprovisioned it, or it sat parked past the dormancy window after being warned. `POST /box/ensure` gives them a **fresh** box, not their old one; don't imply otherwise.
|
|
38
|
-
- **state = `provisioning` / `waking`** → in flight; check back in a minute.
|
|
39
|
-
- **state = `error`** → flag it to an operator (don't try to fix infra from here).
|
|
40
|
-
- **SSH keys** → list the fingerprints + labels. If empty, they haven't connected yet → `/builder-connect`. They can remove a stale key with `DELETE /api/gds/box/ssh-key/:id` (own-scoped).
|
|
41
|
-
|
|
42
|
-
3. **Cost framing (if asked):** a box only bills compute while `active`; parked costs ~$0.30/mo (just the snapshot). The number in `compute_cost_usd` is their box's running total.
|
|
43
|
-
|
|
44
|
-
## Constraints
|
|
45
|
-
|
|
46
|
-
- **`POST /box/ensure` is the only mutation a builder may trigger** — and it only *queues* an auto-provision/wake of their **own** box (rank-gated, one open request at a time); the web server never touches DigitalOcean. Never call provision/park/wake/**deprovision** directly — those are operator control-plane commands (`node scripts/gds/box.js`), rank-gated. If the builder wants their box destroyed, that's an operator action.
|
|
47
|
-
- **Own resource only.** `/box/me` and `/box/ssh-key` are scoped to the caller. The full roster (`GET /api/gds/boxes`) is Metic+ (`box.fleet.manage`, metic-floored since ADR 0157) — don't try it for a sub-Metic builder.
|
|
48
|
-
- **Don't print the SSH private key** — these endpoints only ever return public fingerprints, never private material. Keep it that way.
|
|
49
|
-
|
|
50
|
-
## Quirks on the box (verified from ssh sessions)
|
|
51
|
-
|
|
52
|
-
- **`sharp` resolves only from `/workspace`.** Run image-pipeline tooling (art gen, thumbnailing) from `/workspace` — invoking it from another cwd on the box fails to resolve the native binding.
|
|
53
|
-
- **Playwright MCP writes screenshots to `.playwright-mcp/` relative to the server's cwd**, not the caller's — look there first when a screenshot path seems to have gone missing.
|
|
54
|
-
- **The Bash tool's cwd persists between calls on the box.** A bare `cd x` silently shifts every later command in the session; prefer absolute paths over `cd`-then-relative sequences.
|
|
55
|
-
|
|
56
|
-
## Files this skill touches
|
|
57
|
-
|
|
58
|
-
- Calls: `/api/gds/box/me`, `/api/gds/box/ssh-key` (read), `/api/gds/box/terminal` (read), optionally `POST /api/gds/box/ensure` (queue auto-provision/wake) or `DELETE /api/gds/box/ssh-key/:id`
|
|
59
|
-
- Reads: `~/.config/otb/gds-session.json` (the session token, via cli-lib)
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: builder-connect
|
|
3
|
-
description: >-
|
|
4
|
-
Connect Claude Desktop to the builder's dev box: GitHub sign-in, SSH key, connection file, then restart Desktop. Builder side only. Triggers: "/builder-connect", "connect me to the dev box", "set up my dev box", "onboard me to the box".
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
You are guiding a builder through connecting their **own machine's** Claude Desktop to their org dev box. The audience is often non-technical — narrate each step in plain language, and never make them hand-edit files or paste keys around. One command does everything.
|
|
8
|
-
|
|
9
|
-
## Background (the mental model — ADR 0031)
|
|
10
|
-
|
|
11
|
-
Claude Code is three pieces: the **screen** (their device — always local), the **worker** (the dev box — where the repo + work live), and the **brain** (Anthropic's servers). This skill wires the screen to the box. The box itself is provisioned by an operator with `node scripts/gds/box.js onboard <login>`; this skill is the builder's half — it registers their SSH key and installs the Desktop connection file so the box shows up as a one-click "Environment".
|
|
12
|
-
|
|
13
|
-
What the bootstrap does, in order:
|
|
14
|
-
1. **GitHub sign-in** (device flow against Bongos — it prints a short code + a URL to open).
|
|
15
|
-
2. **Creates an SSH key** at `~/.ssh/otb_builder` if absent (the private half never leaves their machine).
|
|
16
|
-
3. **Registers the public key** with Bongos (`POST /box/ssh-key`). The box pulls it into `authorized_keys` itself — no operator SSH round-trip.
|
|
17
|
-
4. **Downloads + installs** the Claude Desktop connection file (`managed-settings.json`) at the OS path.
|
|
18
|
-
|
|
19
|
-
## How to use
|
|
20
|
-
|
|
21
|
-
1. **Decide which command to run.** Both do the same thing; pick by where you're running:
|
|
22
|
-
- **Repo is present locally** (you can see `infra/box-connect.sh`): run the local copy —
|
|
23
|
-
```bash
|
|
24
|
-
bash infra/box-connect.sh
|
|
25
|
-
```
|
|
26
|
-
- **No repo** (a bare laptop with only Claude Desktop): run the hosted bootstrap —
|
|
27
|
-
- macOS / Linux:
|
|
28
|
-
```bash
|
|
29
|
-
curl -fsSL https://example.com/api/gds/box/connect.sh | bash
|
|
30
|
-
```
|
|
31
|
-
- Windows (PowerShell):
|
|
32
|
-
```powershell
|
|
33
|
-
irm https://example.com/api/gds/box/connect.ps1 | iex
|
|
34
|
-
```
|
|
35
|
-
(For staging, swap the host — or set `GDS_API_BASE` before the local script.)
|
|
36
|
-
|
|
37
|
-
2. **Relay the GitHub code + URL verbatim.** The script prints a short user code and a `https://github.com/login/device` URL. The builder must open the URL and enter the code. Do not summarize — they need the literal values. The script also tries to pop the URL open automatically.
|
|
38
|
-
|
|
39
|
-
3. **Wait while it polls.** After they approve in the browser, the script registers the key and installs the connection file. On macOS the install step needs **one `sudo` prompt** (the file lives under `/Library/Application Support/ClaudeCode` — the dir Claude Code actually reads managed settings from; `…/Claude/` is silently ignored).
|
|
40
|
-
|
|
41
|
-
4. **Confirm the finish state and give the next steps:**
|
|
42
|
-
- Tell their operator they're registered (so the operator runs `box.js onboard <login>` to provision the box, if not already done).
|
|
43
|
-
- **Restart Claude Desktop.**
|
|
44
|
-
- Open **Code → Environment → "Example Dev Box"** to connect.
|
|
45
|
-
|
|
46
|
-
5. **Once they're on the box**, the first thing to do there is authenticate the Bongos CLI (`/builder-setup` or `node scripts/gds/setup.js`) so the box can pull rank-scoped source and keep their SSH keys in sync.
|
|
47
|
-
|
|
48
|
-
## Constraints
|
|
49
|
-
|
|
50
|
-
- **Never print or save the SSH private key or Bongos token anywhere except where the script puts them.** The private key stays at `~/.ssh/otb_builder`; the token is held only in memory on the laptop for the two authenticated calls (it is NOT persisted there — the box gets its own session when they authenticate Claude Code on it).
|
|
51
|
-
- **Don't re-generate a key if `~/.ssh/otb_builder` already exists** — the script reuses it. Overwriting would break an existing box connection. (The script handles this; don't override it.)
|
|
52
|
-
- **If `auth_not_configured` comes back**, Bongos has no GitHub OAuth app — stop and point them to an Archon (`blockers/blockers.md#github-oauth-app`). Don't loop.
|
|
53
|
-
- **Memory + credits are server-side** (ADR 0024) — reassure the builder that nothing about which box they land on affects their history.
|
|
54
|
-
|
|
55
|
-
## Files this skill touches
|
|
56
|
-
|
|
57
|
-
- Runs: `infra/box-connect.sh` (or the hosted `GET /api/gds/box/connect.sh` / `.ps1`)
|
|
58
|
-
- Reads/creates: `~/.ssh/otb_builder` (+ `.pub`)
|
|
59
|
-
- Writes (macOS): `/Library/Application Support/ClaudeCode/managed-settings.json`; (Windows): `%ProgramFiles%\ClaudeCode\managed-settings.json` — the dir Claude Code reads managed settings from. **Not** `…/Claude/` or `%ProgramData%\Claude\`, which the app ignores (the box then never appears in the Environment picker).
|
|
60
|
-
- Calls: `/api/gds/auth/device/{start,poll}`, `/api/gds/box/ssh-key`, `/api/gds/box/managed-settings`
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: builder-sync
|
|
3
|
-
description: >-
|
|
4
|
-
Re-fetch the dev box's source now instead of waiting for the ~10-min cron. Runs ON the box. Triggers: "/builder-sync", "sync my box", "re-scope my box", "pull my task's code now", or right after "/builder-claim N".
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
**Script skill (authoritative).** The core action is `node scripts/gds/box-sync.js`. Run it and relay its output verbatim.
|
|
8
|
-
|
|
9
|
-
You are re-running the dev box's source-fetch **on demand** (BV1.R96, goal 1000051 — task-scoped dev box). The box normally re-fetches on a ~10-min cron; this makes it instant.
|
|
10
|
-
|
|
11
|
-
## Why this exists
|
|
12
|
-
|
|
13
|
-
A builder's box holds only the code for their **active claim** (ADR 0148 task-scoping): `GET /box/source-access` returns the sparse-checkout set for the modules the claimed task's goal scopes. The box applies that via `infra/box-source-fetch.sh`, on a cron every ~10 min. So right after `/builder-claim N`, the *new* task's code isn't on the box yet — until the next cron tick.
|
|
14
|
-
|
|
15
|
-
`box-sync.js` re-runs that fetch **now**. Because the fetch re-reads `/box/source-access`, it **re-scopes the box in place** to whatever the builder currently holds a claim on — seconds after claiming, not ~10 minutes later. The cron stays the backstop.
|
|
16
|
-
|
|
17
|
-
## How to use
|
|
18
|
-
|
|
19
|
-
1. **Run it** (on the box):
|
|
20
|
-
```
|
|
21
|
-
node scripts/gds/box-sync.js
|
|
22
|
-
```
|
|
23
|
-
Add `--dry-run` to print what it would run without fetching.
|
|
24
|
-
|
|
25
|
-
2. **Relay the output.** On success it prints `sync: done — your box now holds the code for your active claim.` The sparse-checkout is now scoped to the active claim's modules (plus the base instance).
|
|
26
|
-
|
|
27
|
-
3. **Typical flow:** `/builder-claim N` → `/builder-sync` → the box now has task N's module code; open the working folder and start.
|
|
28
|
-
|
|
29
|
-
## Failure modes
|
|
30
|
-
|
|
31
|
-
- **`no infra/box-source-fetch.sh found`** — you are **not on a dev box** (off a box your source *is* your local checkout, so there's nothing to sync). This is expected on a laptop; the skill exits non-zero and says so.
|
|
32
|
-
- **The fetch itself errors** — the underlying `box-source-fetch.sh` output is passed through verbatim (e.g. a credential/rank issue surfaces there); relay it and, if it's a rank/claim problem, check the active claim with `/builder-start`.
|
|
33
|
-
|
|
34
|
-
## Constraints
|
|
35
|
-
|
|
36
|
-
- **Runs on the box only.** It re-runs the box-side fetch; it does not push, claim, or change scope by itself — scope follows the builder's active claim (change scope by claiming a different task, then sync).
|
|
37
|
-
- **Idempotent + safe.** Re-running is harmless; the cron does the same thing on its own schedule.
|
|
38
|
-
|
|
39
|
-
## Files this skill touches
|
|
40
|
-
|
|
41
|
-
- Runs: `scripts/gds/box-sync.js` → `infra/box-source-fetch.sh` (on the box)
|
|
42
|
-
- Reads (indirectly, via the fetch): `GET /box/source-access`
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: close-box
|
|
3
|
-
description: >-
|
|
4
|
-
Tear down the dev box you are working on, from inside its terminal. Own-scoped — never another builder's box and never any other infrastructure. Triggers: "/close-box", "close my box", "shut down this box", "I'm done with this box", "tear down my dev box".
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
You are closing (deprovisioning) the dev box the builder is currently working on. This is the Claude-side twin of the `close-box` shell command (`infra/close-box.sh`) — same endpoint, for builders who'd rather say "/close-box" inside `claude` than drop to the shell. It is own-scoped and destructive (the box is destroyed), so confirm before firing.
|
|
8
|
-
|
|
9
|
-
## Background
|
|
10
|
-
|
|
11
|
-
A per-builder dev box (ADR 0031 §2) is a DigitalOcean droplet. Closing it enqueues a `deprovision` intent that the control-plane runner executes within ~a minute. The box's disk is gone after that — anything in `/workspace` that was not pushed to its remote is lost. The box is cheap to recreate from the builders' hall ("Request dev box" / `/builder-box`), so closing when done is the right cost hygiene; the idle-suspend sweep would otherwise park it automatically after the idle window.
|
|
12
|
-
|
|
13
|
-
The endpoint:
|
|
14
|
-
- `POST /api/gds/box/close` → enqueues `deprovision` for the **caller's own** box (`auth.requireBuilder`, own resource). Idempotent-ish: if there's no active box it just no-ops.
|
|
15
|
-
|
|
16
|
-
## What this skill does
|
|
17
|
-
|
|
18
|
-
Calls `POST /api/gds/box/close` via the cross-platform helper, after confirming with the builder.
|
|
19
|
-
|
|
20
|
-
## How to use
|
|
21
|
-
|
|
22
|
-
1. **Confirm intent.** Ask plainly: "Close (deprovision) your dev box? Any work in `/workspace` you haven't pushed will be lost." Proceed only on a clear yes. If the builder only wants to stop *using* it for now (not destroy it), tell them they can just close the tab — with session persistence (#893) their work survives and the box auto-parks when idle — and do NOT close it.
|
|
23
|
-
|
|
24
|
-
2. **Fire the close:**
|
|
25
|
-
```
|
|
26
|
-
node scripts/gds/api.js POST /api/gds/box/close
|
|
27
|
-
```
|
|
28
|
-
(The helper resolves the Bongos session token + API base itself; no manual token handling.)
|
|
29
|
-
|
|
30
|
-
3. **Relay the outcome.** On success tell the builder:
|
|
31
|
-
- The box is closing now (a few seconds).
|
|
32
|
-
- They can close the browser tab — and if the builders'-hall tab is still open in the background, it auto-closes the terminal tab for them when it sees the box shut down (`modules/hall-ui/public/box-panel.js`). This skill cannot close the tab itself (it has no browser control).
|
|
33
|
-
- They can provision a fresh box anytime from the hall or with `/builder-box`.
|
|
34
|
-
|
|
35
|
-
## Constraints
|
|
36
|
-
|
|
37
|
-
- **Own box only.** This never deprovisions another builder's box. Operator-scoped lifecycle (provision / park / wake / destroy-any) lives in `node scripts/gds/box.js`, gated by rank — not here.
|
|
38
|
-
- **Don't auto-close without a yes.** It is destructive.
|
|
39
|
-
- **It cannot close the browser tab.** Be honest about that — see the relay step.
|
|
40
|
-
|
|
41
|
-
## Files this skill touches
|
|
42
|
-
|
|
43
|
-
- Reads: `~/.config/otb/gds-session.json`
|
|
44
|
-
- Calls: `POST /api/gds/box/close`
|