@bongos/core 1.19.276 → 1.19.278
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +24 -14
- package/.claude/skills/builder-sequence/SKILL.md +16 -1
- package/docs/adr/0182-project-visibility-two-axes.md +168 -0
- package/docs/adr/README.md +1 -0
- package/modules/lifecycle/ship-card.js +71 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/session_boundary_handoff.mjs +184 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.278",
|
|
6
|
+
"core_contract": "1.19.278",
|
|
7
|
+
"source_commit": "d2c8b889323c64a1734fa1e622fe168b46b1116f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-21T17:28:18.709Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 319,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1376,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1717,
|
|
20
|
+
"tree_sha256": "f8955f572f4ca37464ee436a7b53e1b27efcc4d7f142af25ea001a4f42ca390b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
{
|
|
88
88
|
"path": ".claude/skills/builder-sequence/SKILL.md",
|
|
89
89
|
"mode": "0000644",
|
|
90
|
-
"sha256": "
|
|
90
|
+
"sha256": "838648912f0f058c58c42d943282346e2c35a90aea507b0ec136e9b0fbda50b0"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"path": ".claude/skills/builder-setup/SKILL.md",
|
|
@@ -1389,10 +1389,15 @@
|
|
|
1389
1389
|
"mode": "0000644",
|
|
1390
1390
|
"sha256": "26f39ae01ea1768dfbd61789752356ae30364b465117b1e7ddee94fabc7d6bbf"
|
|
1391
1391
|
},
|
|
1392
|
+
{
|
|
1393
|
+
"path": "docs/adr/0182-project-visibility-two-axes.md",
|
|
1394
|
+
"mode": "0000644",
|
|
1395
|
+
"sha256": "119637165d2d6b89f8c00afccd07d9f073629f1502f4b2f2cdc673001b2f8d2a"
|
|
1396
|
+
},
|
|
1392
1397
|
{
|
|
1393
1398
|
"path": "docs/adr/README.md",
|
|
1394
1399
|
"mode": "0000644",
|
|
1395
|
-
"sha256": "
|
|
1400
|
+
"sha256": "56d478e1b5285c1c133225240c65ec23c6b3b4f280cdda853c50b33564cc2910"
|
|
1396
1401
|
},
|
|
1397
1402
|
{
|
|
1398
1403
|
"path": "docs/api-reference.md",
|
|
@@ -3872,7 +3877,7 @@
|
|
|
3872
3877
|
{
|
|
3873
3878
|
"path": "modules/lifecycle/ship-card.js",
|
|
3874
3879
|
"mode": "0000644",
|
|
3875
|
-
"sha256": "
|
|
3880
|
+
"sha256": "c08ee0e89f67a2db7ea049353a12bdd6e5d97c6a7ec9039d19bdd797f66c112e"
|
|
3876
3881
|
},
|
|
3877
3882
|
{
|
|
3878
3883
|
"path": "modules/lifecycle/ship-preflight.js",
|
|
@@ -4487,12 +4492,12 @@
|
|
|
4487
4492
|
{
|
|
4488
4493
|
"path": "package-lock.json",
|
|
4489
4494
|
"mode": "0000644",
|
|
4490
|
-
"sha256": "
|
|
4495
|
+
"sha256": "6c3c704f2de78db933bd38831feaae364a1d1d608a01b7c14d147854950a6cef"
|
|
4491
4496
|
},
|
|
4492
4497
|
{
|
|
4493
4498
|
"path": "package.json",
|
|
4494
4499
|
"mode": "0000644",
|
|
4495
|
-
"sha256": "
|
|
4500
|
+
"sha256": "61504099e0e7e063f76862dfb805405f2743eaa0a3cb389a6b80fad771f35775"
|
|
4496
4501
|
},
|
|
4497
4502
|
{
|
|
4498
4503
|
"path": "public-docs/index.html",
|
|
@@ -6137,7 +6142,7 @@
|
|
|
6137
6142
|
{
|
|
6138
6143
|
"path": "src/module-api.js",
|
|
6139
6144
|
"mode": "0000644",
|
|
6140
|
-
"sha256": "
|
|
6145
|
+
"sha256": "e0b73ea48b0a4c19e931162761dc58e0f97859e35bd92d93335cf610fc2036b0"
|
|
6141
6146
|
},
|
|
6142
6147
|
{
|
|
6143
6148
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8194,6 +8199,11 @@
|
|
|
8194
8199
|
"mode": "0000644",
|
|
8195
8200
|
"sha256": "e76a3baf5fb2cdee08ffe8d80c55630b5ab4f708046f3c63c07c480926ccda31"
|
|
8196
8201
|
},
|
|
8202
|
+
{
|
|
8203
|
+
"path": "tests/session_boundary_handoff.mjs",
|
|
8204
|
+
"mode": "0000644",
|
|
8205
|
+
"sha256": "8789eb59b2c3984500b4b2eb24d1e812e0ce6c970fc6fc940c06a6daf2613ec2"
|
|
8206
|
+
},
|
|
8197
8207
|
{
|
|
8198
8208
|
"path": "tests/session_caps.mjs",
|
|
8199
8209
|
"mode": "0000644",
|
|
@@ -35,7 +35,20 @@ This skill owns the **loop and the halt discipline**. It does not re-derive how
|
|
|
35
35
|
|
|
36
36
|
5. **Ship it** — `/builder-ship`. The task must reach `shipped`, not stall at `completed` or `confirmed`. A ship that does not land has not unblocked anything, so the chain cannot advance; treat it as a halt (see `ship_failed` below).
|
|
37
37
|
|
|
38
|
-
6. **
|
|
38
|
+
6. **Hand the chain off before the next link — the chain gets slower otherwise.** This is the step that makes a long run stay fast, and it is easy to skip because nothing breaks without it.
|
|
39
|
+
|
|
40
|
+
Every turn re-reads the whole conversation, so a chain worked in one window pays more per turn the further down it goes. Measured on a three-task run (2026-08-21): 11.3M cached tokens on the first task, 36.1M on the second, 78.1M on the third. The third task was not twice the work of the second — it re-processed twice the context.
|
|
41
|
+
|
|
42
|
+
A shipped task is the moment almost all of that context turns into dead weight: superseded diffs, resolved test failures, closed grade rounds. **The chain's state is already durable** — dependencies and claims live in the GDS, and `--next` re-derives the plan from them — so a fresh window loses nothing that matters.
|
|
43
|
+
|
|
44
|
+
So, when the ship card shows a **heavy session** (it says so itself: `⚠ heavy session (NNM cached) — run /session-handoff…`), or when you have landed **three or more links** in one window:
|
|
45
|
+
|
|
46
|
+
- Run `/session-handoff`, scoped to the REMAINDER of the chain — the next task id, the chain's tail, and any decision from the links already landed that the rest depends on. Do not summarise the finished work in detail; it is shipped, and the GDS is its record.
|
|
47
|
+
- Hand the user that prompt and say plainly that the chain continues in a fresh session. Then **stop.** Do not claim the next task in this window.
|
|
48
|
+
|
|
49
|
+
`/session-handoff` composes text and changes nothing, so this is safe at any point — and it cannot be automated away: a hook cannot clear a conversation, which is exactly why this step is the runner's job.
|
|
50
|
+
|
|
51
|
+
7. **Otherwise, go back to step 2.** The ship you just landed is what promotes the next task to `ready`.
|
|
39
52
|
|
|
40
53
|
Between passes, report progress in one line — which task landed, what is next, how much of the chain remains. The user is following a long run; they should never have to ask where it is.
|
|
41
54
|
|
|
@@ -73,6 +86,8 @@ node scripts/gds/sequence.js --check <task-id>
|
|
|
73
86
|
|
|
74
87
|
## What NOT to do
|
|
75
88
|
|
|
89
|
+
- **Do not run a long chain to exhaustion in one window** to avoid the handoff in step 6. It feels like progress and it is the single biggest reason a long run drags: the last link can cost several times the first for identical work. Handing off is not an interruption of the chain; it is how the chain stays cheap.
|
|
90
|
+
|
|
76
91
|
- **Do not skip a halted task and continue down the chain.** The owner chose stop-and-report precisely so a blocked predecessor does not silently strand its dependents.
|
|
77
92
|
- **Do not batch-claim.** See "The one rule".
|
|
78
93
|
- **Do not widen a task's `touches[]`** to make the chain flow. File a new task instead (`node scripts/gds/capture.js`).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# ADR 0182 — Project visibility is TWO axes: a derived publish gate and a stored join door
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-21
|
|
5
|
+
- **Task:** [task 1002320](https://cloudbongos.com/builders#/task/1002320) (BONGOS-V1, goal 1000046 — *Project creation — visibility & customization*)
|
|
6
|
+
- **Deciders:** IAMMASTERQUA (Archon), Claude
|
|
7
|
+
- **Confirms R04–R18** — the full accounting is in *Scope* below, including the two that this ADR deliberately does **not** govern.
|
|
8
|
+
- **Bounds — deliberately NOT decided here:** the self-hosted→hub *reporting and trust* mechanics ([task 1002321](https://cloudbongos.com/builders#/task/1002321), R02) · the applications/invite *model* (`docs/specs/<redacted>.md` D2, already decided) · project *authority* (same spec, D1)
|
|
9
|
+
- **Related:** [ADR 0141](<redacted>.md) (hub = identity + relay, authority stays local) · [ADR 0112](<redacted>.md) (`goals.visibility`, the precedent this copies) · [ADR 0108](<redacted>.md) (the instance IS the project) · [ADR 0174](<redacted>.md) (one word, one meaning)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Scope — what "confirms R04–R18" means, task by task
|
|
14
|
+
|
|
15
|
+
The task description says this ADR *"confirms R04–R18"*. That is 15 tasks and they do not
|
|
16
|
+
all relate to it the same way, so they are enumerated rather than gestured at. **Its
|
|
17
|
+
criteria** are `proj-visibility-states`, `proj-join-policy` and `proj-detail-interview`
|
|
18
|
+
(the goal's C1–C3); task 1002320 is linked to all three.
|
|
19
|
+
|
|
20
|
+
**Shape set by this ADR** — these build what it decides, and each should be read against it:
|
|
21
|
+
|
|
22
|
+
| | Task | Gets from this ADR |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| R04 | [1002323](https://cloudbongos.com/builders#/task/1002323) | the 3-value enum + the derived gate + the missing `description` column (D1, D4) |
|
|
25
|
+
| R05 | [1002324](https://cloudbongos.com/builders#/task/1002324) | **moves host** — an instance surface, not a hub one (D5) |
|
|
26
|
+
| R06 | [1002325](https://cloudbongos.com/builders#/task/1002325) | the feed predicate change + **server-side** stealth redaction (D2a, D3) |
|
|
27
|
+
| R07 | [1002326](https://cloudbongos.com/builders#/task/1002326) | which three states render, and that `featured` no longer gates presence (D3) |
|
|
28
|
+
| R08 | [1002327](https://cloudbongos.com/builders#/task/1002327) | the hub row is a cache; **unreported ⇒ fail-closed** (D5) |
|
|
29
|
+
| R09 | [1002328](https://cloudbongos.com/builders#/task/1002328) | the render mapping, and that it may not be trusted to redact (D2a) |
|
|
30
|
+
| R11 | [1002330](https://cloudbongos.com/builders#/task/1002330) | per-state application rules on the already-decided model (D6) |
|
|
31
|
+
| R12 | [1002331](https://cloudbongos.com/builders#/task/1002331) | **visibility is the ceiling; join policy may only narrow** (D6) |
|
|
32
|
+
| R15 | [1002334](https://cloudbongos.com/builders#/task/1002334) | why skipping the interview is a supported default, not a dead end (D4) |
|
|
33
|
+
| R16 | [1002335](https://cloudbongos.com/builders#/task/1002335) | the bar is identity only, and it must name what is missing (D4) |
|
|
34
|
+
|
|
35
|
+
**Proofs of those shapes** — no new decisions, but their assertions are set here:
|
|
36
|
+
R10 ([1002329](https://cloudbongos.com/builders#/task/1002329)) must assert stealth's
|
|
37
|
+
**absent fields, not absent pixels** (D2a);
|
|
38
|
+
R14 ([1002333](https://cloudbongos.com/builders#/task/1002333)) must cover the
|
|
39
|
+
ceiling-vs-narrowing rule (D6);
|
|
40
|
+
R18 ([1002337](https://cloudbongos.com/builders#/task/1002337)) must cover
|
|
41
|
+
skip ⇒ dark-matter-until-published (D4).
|
|
42
|
+
|
|
43
|
+
**NOT governed by this ADR**, stated so the omission is a decision rather than a gap:
|
|
44
|
+
|
|
45
|
+
- **R13** ([1002332](https://cloudbongos.com/builders#/task/1002332)), the owner
|
|
46
|
+
applications review queue — belongs to `docs/specs/<redacted>.md`
|
|
47
|
+
(D1/D2), which already decided the queue, its authority and its storage. This ADR sets
|
|
48
|
+
only which states *accept* an application (D6), never what happens to it afterwards.
|
|
49
|
+
- **R17** ([1002336](https://cloudbongos.com/builders#/task/1002336)), rule-based
|
|
50
|
+
recommendations from the project detail — orthogonal. Recommendations concern module and
|
|
51
|
+
governance presets; nothing about them depends on visibility or publishability.
|
|
52
|
+
|
|
53
|
+
## Context
|
|
54
|
+
|
|
55
|
+
Goal 1000046 seeded 31 tasks, and R04–R18 each say *"per R01"* — this ADR. Nothing in the goal could start until it landed: at claim time it gated **29 open tasks**, the largest single chokepoint in the repo, and it was itself parked at `backlog` with no dependencies of its own.
|
|
56
|
+
|
|
57
|
+
The spec its planning task cites — `docs/specs/<redacted>.md` — **was never written**. This ADR is therefore the design of record, not a summary of one.
|
|
58
|
+
|
|
59
|
+
**What exists today.** `platform_identity_projects` (hub-side, keyed by `origin`) carries `status ('active'|'hidden')`, an Archon-curated `featured` boolean, and `owner_github_id` — explicitly commented *"advisory attribution (not an authority)"*. The public feed reads `featured = true AND status = 'active'`. So today a project's presence on the map is **Archon editorial curation**, and the owner has no control at all. The goal's premise is the inverse: the owner shapes it.
|
|
60
|
+
|
|
61
|
+
**The apparent contradiction that motivated this ADR.** The goal's own criterion `proj-visibility-states` says *"one of **four** states"* (Public/Private/Stealth/Unlisted). R04's description says *"3-state visibility enum … dark-matter is derived … not a stored 4th state"*. Read as one enum these disagree. They do not disagree; they describe different layers, and conflating those layers is the trap this ADR exists to prevent.
|
|
62
|
+
|
|
63
|
+
## Decision 1 — Two independent axes, not one four-valued enum
|
|
64
|
+
|
|
65
|
+
| Axis | Stored? | Set by | Governs |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| **Publishability** | **Derived** | the identity gate (R16) | whether the project is on the map *at all* |
|
|
68
|
+
| **The join door** | Stored: `visibility ∈ (public, private, stealth)` | the project's owner | how it renders, and who may get in |
|
|
69
|
+
|
|
70
|
+
The four user-facing states are **3 × publishable, plus one derived state that collapses all three**:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
publishable = false → DARK MATTER (off the map, un-joinable) — whatever the enum says
|
|
74
|
+
publishable = true → public → planet (identified, anyone may join)
|
|
75
|
+
private → star (identified, no self-join, applications accepted)
|
|
76
|
+
stealth → black hole (ANONYMOUS, no self-join, applications refused)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**The name `visibility` is kept, and it is a mild lie.** The column does not decide visibility — publishability does. We keep the name because `goals.visibility` (ADR 0112) already established it and one word across both planes beats an accurate name used in one place. This paragraph is the mitigation: **anyone reading `visibility = 'public'` and expecting the orb on the map is reading it wrong**, and the publish gate is the thing to check. R04 must carry that sentence to the column comment.
|
|
80
|
+
|
|
81
|
+
## Decision 2 — All three stored states are VISIBLE; only the derived state is invisible
|
|
82
|
+
|
|
83
|
+
This is what makes the 3+1 split honest rather than a workaround: the enum is a *door* policy, and every value of it is on the map. Consequences worth stating because R06/R07/R09 all depend on them:
|
|
84
|
+
|
|
85
|
+
- **`private` means visible-but-closed, exactly as it already does for goals.** ADR 0112: *"a private goal stays VISIBLE in the goal list but non-members can't self-join."* The project plane inherits that meaning unchanged. A private project is a star you can see and ask to join.
|
|
86
|
+
- **`stealth` exposes EXISTENCE but not IDENTITY.** The black hole is the metaphor and it is precise: you see the gravitational effect, not the object. R06/R09 say "stealth is anonymous", and that is a real privacy primitive, not a rendering note.
|
|
87
|
+
- **Dark matter is not a state an owner picks.** It is the absence of a met bar. There is deliberately no way to *choose* it, because "unlisted" as a selectable option would compete with `private` and nobody could say which to pick.
|
|
88
|
+
|
|
89
|
+
### Decision 2a — stealth redaction is SERVER-SIDE, in the feed
|
|
90
|
+
|
|
91
|
+
A stealth project's `name`, `tagline`, `art_url`, and `origin` **must not leave the server** in the public feed. The feed emits the orb (existence, render type, and whatever aggregate the map needs) and nothing identifying.
|
|
92
|
+
|
|
93
|
+
This is called out because the naive implementation is to send the row and let the orbs page hide it — which leaks the whole thing to anyone who opens the network tab, and the orbs page is a **static site outside this repo** (R09), so we cannot enforce it there even in principle. Redaction at the boundary is the only version of this that is true. R06 owns it; R10's proof must assert the absence of the fields, not the absence of pixels.
|
|
94
|
+
|
|
95
|
+
## Decision 3 — Three mechanisms, three owners, one composition rule
|
|
96
|
+
|
|
97
|
+
`status`, `visibility` and `featured` all touch "does this appear", and they are not the same thing:
|
|
98
|
+
|
|
99
|
+
| Field | Owner | Meaning |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `status` | system / Archon | operational lifecycle — a deprovisioned project is `hidden` |
|
|
102
|
+
| `visibility` | the **project owner** | the join door (Decision 1) |
|
|
103
|
+
| `featured` | Archon | editorial promotion |
|
|
104
|
+
|
|
105
|
+
**Composition:** a project is on the map iff `status = 'active' AND publishable`. `featured` may only **rank or promote among projects already on the map** — it can never put an unpublishable or `hidden` project there.
|
|
106
|
+
|
|
107
|
+
That is a behaviour change: today's feed predicate is `featured = true AND status = 'active'`, so **featuring is currently load-bearing for visibility at all**, and an owner setting `public` on an unfeatured project would see nothing happen. R06 replaces the predicate; R07 dogfoods it. Without this decision the owner-control premise of the whole goal silently does not work.
|
|
108
|
+
|
|
109
|
+
## Decision 4 — The publish gate is identity only: `name` + `description`
|
|
110
|
+
|
|
111
|
+
Per the owner interview (recorded on R16): the bar is a name and a description. **Not** tagline, art, modules, governance, or a completed interview.
|
|
112
|
+
|
|
113
|
+
The bar exists to keep empty husks off the map — not to enforce completeness. A higher bar would make "skip the optional detail step" a dead end rather than the supported default the goal explicitly wants (`proj-detail-interview`: *"skipping it produces sensible defaults"*).
|
|
114
|
+
|
|
115
|
+
**The gate must name what is missing, never merely refuse.** A project that cannot be published and is not told why is indistinguishable from a broken one.
|
|
116
|
+
|
|
117
|
+
*Note for R04:* `platform_identity_projects` has `name NOT NULL` but **no `description` column** (only `tagline`). R04 adds it. Do not repurpose `tagline` — it is the card one-liner and a description is the publish bar; collapsing them means editing your card can unpublish your project.
|
|
118
|
+
|
|
119
|
+
## Decision 5 — Visibility is owned by the INSTANCE and mirrored to the hub
|
|
120
|
+
|
|
121
|
+
The hub's `platform_identity_projects` row is a **cache, never the source of truth.**
|
|
122
|
+
|
|
123
|
+
This follows from ADR 0141 §3 and the admin-console spike's D1 (*the instance IS the project; authority lives on the project instance*), and it resolves what otherwise looks like a gap: `owner_github_id` being "advisory, not an authority" is **correct**, not a hole to be filled. The hub never authorizes a visibility change; it accepts a report from the authenticated `sso_client` and stores it.
|
|
124
|
+
|
|
125
|
+
Two consequences that redirect seeded work:
|
|
126
|
+
|
|
127
|
+
1. **R05 (owner set/change visibility) is an INSTANCE surface**, not a hub one. The setting lives in the project's own hall settings, gated by the project's own ranks.
|
|
128
|
+
2. **R08 is the sync, not an afterthought.** A hub row whose visibility was never reported is stale, and stale here means *wrong on the public map*. R08's absent-report default must be **fail-closed: unreported ⇒ treated as not publishable (dark matter)**, so a project that has never spoken is off the map rather than exposed by omission.
|
|
129
|
+
|
|
130
|
+
The *trust* mechanics of that report — how the hub authenticates it, replay, rate — are **R02's** ([task 1002321](https://cloudbongos.com/builders#/task/1002321)), not this ADR's.
|
|
131
|
+
|
|
132
|
+
## Decision 6 — Per-state join rules, and how they compose with the join-policy setting
|
|
133
|
+
|
|
134
|
+
The applications *model* is already decided (`docs/specs/<redacted>.md` D2: extend `access_requests` with `kind ∈ ('application','invite')`, no new table). This ADR adds only the per-state rules on top:
|
|
135
|
+
|
|
136
|
+
| State | Self-join | Applications |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| public / planet | permitted | not needed |
|
|
139
|
+
| private / star | refused | accepted, owner reviews |
|
|
140
|
+
| stealth / black hole | refused | **refused at the door** (the public POST 403s) |
|
|
141
|
+
| dark matter | refused | refused — there is no door to knock on |
|
|
142
|
+
|
|
143
|
+
Stealth's distinction from private is **at the door, not in the queue**: a refused application is never stored, so a stealth project accumulates no queue to leak.
|
|
144
|
+
|
|
145
|
+
**Two knobs could contradict.** [task 1002817](https://cloudbongos.com/builders#/task/1002817) adds a per-project join policy (`open|closed`) at the same door. Rule: **visibility sets the ceiling; join policy may only narrow it.** A `closed` public project accepts no self-joins; an `open` stealth project still refuses everything. The stricter of the two always wins, and neither can widen the other. Without this, the two settings are a coin-flip decided by evaluation order.
|
|
146
|
+
|
|
147
|
+
## Decision 7 — The word `stealth` already means something else; product surfaces win, and the collision is tracked
|
|
148
|
+
|
|
149
|
+
`src/bongos/stealth-gate.js` + `STEALTH_PASSWORD` already mean *an instance-wide pre-launch password wall* — an entire instance made **unreadable**. Project `visibility = 'stealth'` means a project that is **on the public map, anonymously, refusing applications**. These are close to opposites, and ADR 0174 is the standing lesson that one word carrying two meanings costs more than a rename.
|
|
150
|
+
|
|
151
|
+
**Decision:** the *product* meaning (the owner-facing state, whose orb metaphor is locked by the criterion) keeps the word. The infra gate is the one that should be renamed — to `prelaunch` — but that touches a protected path, an env var, and a live instance's configuration, so it is **not** decided here by an ADR about visibility. It is filed as its own task with the owner's call attached.
|
|
152
|
+
|
|
153
|
+
Until then: `stealth` unqualified in product code and copy means the project state. The infra gate is referred to as the **pre-launch gate** in prose even while its identifier still says stealth.
|
|
154
|
+
|
|
155
|
+
## Consequences
|
|
156
|
+
|
|
157
|
+
- R04 lands `visibility` (3-value CHECK, default `public`, precedent `goals.visibility`) **and** the missing `description` column, plus the backfill. Existing rows keep today's behaviour: `visibility='public'`, publishable iff they already have a name and description.
|
|
158
|
+
- R06's feed predicate changes from `featured`-gated to `publishable + active`, with stealth rows redacted server-side. This is the single riskiest change in the goal — it is what actually moves control from Archon curation to the owner — and R10 is its proof.
|
|
159
|
+
- R05 moves host: instance-side settings, not a hub page.
|
|
160
|
+
- R08 defaults fail-closed on an unreported project.
|
|
161
|
+
- 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.
|
|
162
|
+
|
|
163
|
+
## Alternatives rejected
|
|
164
|
+
|
|
165
|
+
- **A stored four-valued enum including `unlisted`.** Rejected: it makes "unlisted" a choice competing with `private`, and it lets a project be simultaneously "publishable" and "unlisted" — two sources of truth for one question, which is the bug the derived gate prevents by construction.
|
|
166
|
+
- **A separate `published` boolean.** Rejected for the same reason: a boolean can disagree with the identity it is supposed to summarise. Derived cannot drift.
|
|
167
|
+
- **Letting `featured` keep gating visibility, with owner visibility layered on top.** Rejected: an owner who sets `public` and appears nowhere until an Archon blesses them has not been given control, and the goal's premise is that they have.
|
|
168
|
+
- **Client-side stealth redaction.** Rejected: the orbs page is a static site outside this repo, so the guarantee would be unenforceable by construction.
|
package/docs/adr/README.md
CHANGED
|
@@ -255,5 +255,6 @@ This keeps the decision history honest and traceable.
|
|
|
255
255
|
| 0179 | [**`rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic** ([task 1003094](https://cloudbongos.com/builders#/task/1003094), goal 1000069). [ADR 0175](<redacted>.md) gave the constitution ONE membership form, `rank:<key>`, resolved by an EXACT SQL match — so `rank:metic` seated metics ONLY (excluding Archons), `rank:xenos` seated NEWCOMERS ONLY rather than "everyone", and "Metic and above" was inexpressible. Everywhere else a rank is a LADDER and authority is cumulative (ADR 0034 inheritance, requireRank('metic','archon'), the floor-derived seed); board membership was the one place it meant "exactly", and nothing said so. Rehearsing R25 on a real Postgres, an Archon ratified `rank:metic` under monarchy — the apply worked perfectly — and the next vote came back 403 not_board_member: **the owner flipped the switch and fell off their own board**, with the board deadlocked at one member because an Archon can still PROPOSE but no longer VOTE. Owner decision: EXPLICIT GRAMMAR. `rank:metic+` (that rank and above), `rank:metic,archon` (union), and **a bare key still means EXACTLY that key** — rejecting the smaller diff (silently redefining the bare form) precisely because it would have changed the meaning of every already-ratified constitution, the monarchy default included, on upgrade: nothing in a government should change meaning without a vote. `+` is defined only on the STANDARD ladder — "and above" about a custom rank with no ladder position is a parse REFUSAL, not a guess, and a refusal seats nobody. The grammar had been written TWICE (a shape regex in the config sanitizer, a parse regex in the resolver, pinned by a property test) — sound for one literal form, unsound the moment a rule needs a LOOKUP, which is the same class of drift that produced the bug; both now call one pure `membership-predicate.js`. Adds the DISENFRANCHISEMENT GUARD: a proposal that would unseat its own proposer is refused before any write, naming the one-character fix, overridable with an explicit `acknowledge_self_removal` (a separate parameter, never a proposal field) because stepping back is a legitimate act. The two DB reads take a set via `= ANY($1)` + DISTINCT (a double-counted member silently raises the consent bar), the structural filters are re-proven against real Postgres (a deactivated builder and a machine principal still never govern), and the hall panel now describes every form in plain language — rendering `rank:metic+` as "the metic+ rank" would be the same bug one layer up. Unblocks task 1003095, which can now write each charter's TRUE predicate rather than wording around a limitation.](<redacted>.md) | government / the board |
|
|
256
256
|
| 0180 | [**Dependency intake & drift policy: baseline-ratchet audit gate, install scripts off, cooldown via patrol, OIDC publish** ([task 1003127](https://cloudbongos.com/builders#/task/1003127), goal 1000076). The red-team baseline audit found 18 advisories (2 high) sitting unwatched in 9 production deps, the `undici ^6.27.0` override pinned INSIDE a vulnerable range while also breaking Node 26 CLIs, and nothing gated what an agent may install — against a 2025 npm threat wave (chalk/debug, Shai-Hulud, Nx) that ran on postinstall scripts and long-lived publish tokens. Four decisions. (1) `scripts/gds/audit-gate.js` runs in the required `unit` check: a high/critical advisory NOT in the committed `config/audit-baseline.json` fails the merge gate; the baseline is **shrink-only pre-gate debt** (10 findings at adoption) — adding an entry to silence a NEW advisory is out of policy. Fail-OPEN on registry/infra errors, fail-CLOSED on findings: the gate catches new advisories, it does not prove registry uptime. Notably a blanket `npm audit fix` cannot even run here (`workspace:` protocol, EUNSUPPORTEDPROTOCOL) — fixes are explicit tasks. (2) `.npmrc ignore-scripts=true`: the tree needs no install-lifecycle scripts — sharp installs prebuilds via optionalDependencies, proven by a script-less `npm ci` + full 430-test suite; a future dep needing one is an ADR-level exception. (3) Update cooldown is PATROL-enforced (task 1003128 flags versions younger than 7 days + stale overrides + action-pin drift) rather than `.npmrc`-enforced, because CI's npm 10 warns on the npm-11 `minimum-release-age` key for every builder; move it into `.npmrc` when the toolchain floor reaches npm ≥ 11.10. (4) Publish auth moves to npm **trusted publishing (OIDC)** — needs a one-time owner registration on npmjs.com (blocker filed); until then the granular 90-day `NPM_TOKEN` is a documented exception.](<redacted>.md) | devsecops / supply chain |
|
|
257
257
|
| 0181 | [**Teardown accepts every state but `torn_down`, and deliberately keeps the address** ([task 1003042](https://cloudbongos.com/builders#/task/1003042), goal 1000058). Walk finding N5: an instance at `status='error'` could never be removed — the owner's teardown answered 409 `not_teardownable` and the archon `force-teardown`, which exists as the escape hatch, answered the SAME refusal, because both read one hand-kept whitelist `['provisioning','active','tearing_down']` that omitted `requested` and `error`. No role had a path out, so errored rows accumulated permanently (instance 15 `swag` from 2026-08-07). The whitelist justified itself as "nothing running to tear down", which is FALSE for `error`: a provision failing mid-run lands there AFTER it may have created the database, taken a port, and written the systemd unit, Caddy block and DNS record — a partial standup is the thing most in need of reclaiming. `TEARDOWN_FROM` is now DERIVED from `INSTANCE_STATES` minus `torn_down`, so a future state is teardownable by DEFAULT — the bug was an allow-list that silently omitted states, and for a failure mode whose signature is "a row with no exit", the safe default is that an exit exists. The runner needed no change (its steps are each `allowFail`); only the web guard refused. The one surviving refusal is now ONE shared helper: the copies had drifted, and force-teardown's positional `res.fail` form put the bare code `not_teardownable` on the wire as the human text. **The `port` and `domain` are deliberately NOT released** — releasing the port alone would be WORSE than the status quo, because the `<slug>.caddy` block survives teardown and proxies the hostname to a PORT NUMBER, so a later instance given that port would answer at the torn-down project's still-live address (verified live: two projects torn down 2026-08-13 still resolved on 2026-08-19). Holding the port is what keeps the address inert at a harmless 502; releasing both is [task 1002897](https://cloudbongos.com/builders#/task/1002897). Retention is also correct for revive, which re-uses the same hostname. Copy was corrected to match: it had promised "the address stops answering", which was never true.](0181-teardown-state-gate-and-address-retention.md) | provisioning / lifecycle |
|
|
258
|
+
| 0182 | [**Project visibility is TWO axes: a derived publish gate and a stored join door** ([task 1002320](https://cloudbongos.com/builders#/task/1002320), goal 1000046). Goal 1000046 seeded 31 tasks each citing "per R01" — this ADR — so nothing in the goal could start until it landed: it gated **29 open tasks**, the largest single chokepoint in the repo, while itself parked at `backlog` with no dependencies. The spec its planning task cites was never written, so this is the design of record. The goal's criterion says **four** states (Public/Private/Stealth/Unlisted) and R04 says **three** stored plus a derived dark-matter; both are right and conflating them is the trap. There are two INDEPENDENT axes: **publishability** (DERIVED from the identity gate — name + description, per the owner interview) decides whether a project is on the map at all, and **the join door** (STORED `visibility ∈ public|private|stealth`, owner-set) decides how it renders and who gets in. Four user-facing states = 3 × publishable + one derived state that collapses all three. The column name `visibility` is a mild lie kept for consistency with `goals.visibility` (ADR 0112) — it does NOT decide visibility, publishability does — and the ADR is the mitigation. All three stored states are VISIBLE; only the derived state is invisible, which is what makes the 3+1 split honest rather than a workaround. `private` inherits ADR 0112's exact meaning (visible-but-closed); `stealth` exposes EXISTENCE but not IDENTITY, and its redaction is **server-side in the feed** — the naive client-side version leaks to anyone opening the network tab, and the orbs page is a static site OUTSIDE this repo (R09) so it could never be enforced there. Three mechanisms touch "does this appear" and are separated: `status` (operational), `visibility` (owner), `featured` (Archon editorial) — on the map iff `status='active' AND publishable`, with `featured` only ranking among those. That is a real behaviour change: today's predicate is `featured=true AND status='active'`, so **featuring is currently load-bearing for visibility at all** and the goal's owner-control premise silently does not work without it. Visibility is owned by the INSTANCE and the hub row is a CACHE (ADR 0141 §3 + the admin-console spike's D1) — so `owner_github_id` being "advisory, not an authority" is CORRECT rather than a gap, R05 moves to an instance surface, and R08's absent-report default is fail-closed (unreported ⇒ dark matter, off the map rather than exposed by omission). Per-state join rules compose with the separate `open|closed` join policy ([task 1002817](https://cloudbongos.com/builders#/task/1002817)) by one rule: **visibility sets the ceiling, join policy may only narrow** — otherwise the two settings are a coin-flip decided by evaluation order. Finally, `stealth` COLLIDES with the existing instance-wide pre-launch password gate (`STEALTH_PASSWORD`, `src/bongos/stealth-gate.js`), which means nearly the opposite (a whole instance made unreadable vs a project publicly on the map, anonymously); per ADR 0174 the product meaning keeps the word and the infra rename is filed separately rather than decided by an ADR about visibility. Rejected: a stored 4-valued enum (makes `unlisted` compete with `private`, and lets publishable+unlisted disagree), a separate `published` boolean (can drift from the identity it summarises; derived cannot), and keeping `featured` as the visibility gate (an owner who sets public and appears nowhere has not been given control).](<redacted>.md) | platform-identity / project visibility |
|
|
258
259
|
|
|
259
260
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -174,6 +174,59 @@ function nextActionFor(stage, { taskId, discipline, sandboxUrl } = {}) {
|
|
|
174
174
|
return { kind: 'prompt', label: 'Continue', cmd: '/builder-start' };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// ---- the session boundary (task 1003150) -----------------------------------
|
|
178
|
+
//
|
|
179
|
+
// A shipped task is the one moment the system KNOWS a session's work is finished,
|
|
180
|
+
// and it is the only moment when almost everything in that session's context
|
|
181
|
+
// becomes dead weight: superseded diffs, resolved test failures, closed grade
|
|
182
|
+
// rounds. Carrying it into the next task costs real time, because every turn
|
|
183
|
+
// re-reads the whole conversation.
|
|
184
|
+
//
|
|
185
|
+
// MEASURED, session of 2026-08-21 — three tasks shipped in one window:
|
|
186
|
+
// task 1003148 262K new + 11.3M cached
|
|
187
|
+
// task 1003065 582K new + 36.1M cached
|
|
188
|
+
// task 1003066 891K new + 78.1M cached
|
|
189
|
+
// The third was not twice the work of the second; it re-processed twice the
|
|
190
|
+
// context. The builder's own report was that everything felt slow, and this was
|
|
191
|
+
// the largest single contributor.
|
|
192
|
+
//
|
|
193
|
+
// WHY THIS LIVES ON THE CARD AND NOT IN A HOOK. A hook cannot clear a
|
|
194
|
+
// conversation, and /session-handoff is read-only by its own spec ("composes text
|
|
195
|
+
// and changes no state") — it has to be invoked. The harness does auto-compact on
|
|
196
|
+
// context PRESSURE, but that fires on size rather than meaning: as likely to
|
|
197
|
+
// compact mid-task, where the detail is load-bearing, as after a ship, where it is
|
|
198
|
+
// not. The ship card is the only component that is already running, already at the
|
|
199
|
+
// boundary, and already printing to the builder — so the nudge costs one line and
|
|
200
|
+
// no new machinery.
|
|
201
|
+
//
|
|
202
|
+
// THE THRESHOLD is deliberately generous. The advice is only useful if it is rare:
|
|
203
|
+
// a line that prints on every ship is wallpaper (the deploy-divergence footer is
|
|
204
|
+
// the cautionary tale — 21 core versions of identical warning that nobody acted
|
|
205
|
+
// on). 25M cached sits between the 11.3M session that felt fine and the 36.1M one
|
|
206
|
+
// that had begun to drag, so a short session never sees it.
|
|
207
|
+
const HEAVY_SESSION_CACHED_TOKENS = 25_000_000;
|
|
208
|
+
|
|
209
|
+
// handoffAdvice — PURE. null when there is nothing to say (the overwhelming
|
|
210
|
+
// majority of ships), else the one-line nudge. Exported so every branch is pinned
|
|
211
|
+
// by a DB-free unit test.
|
|
212
|
+
//
|
|
213
|
+
// Gated on a TERMINAL stage, not merely a big number: mid-task the context is what
|
|
214
|
+
// the work is standing on, and telling someone to reset in the middle of a claim is
|
|
215
|
+
// actively wrong. 'shipped' and 'confirmed' are the two stages where the task is
|
|
216
|
+
// out of the builder's hands.
|
|
217
|
+
function handoffAdvice(input) {
|
|
218
|
+
// Normalise BEFORE destructuring. A default parameter only fires on `undefined`,
|
|
219
|
+
// so an explicit `null` — what a caller spreading an absent object hands over —
|
|
220
|
+
// would throw. This runs at the very end of a successful ship, so a crash here
|
|
221
|
+
// would turn a landed task into a traceback. (Same trap as decideLanding in
|
|
222
|
+
// modules/ideas/routing.js, task 1003065 — it caught me twice in one session.)
|
|
223
|
+
const { stage, cachedTokens } = input || {};
|
|
224
|
+
if (stage !== 'shipped' && stage !== 'confirmed') return null;
|
|
225
|
+
const cached = Number(cachedTokens);
|
|
226
|
+
if (!Number.isFinite(cached) || cached < HEAVY_SESSION_CACHED_TOKENS) return null;
|
|
227
|
+
return `heavy session (${fmtTokens(cached)} cached) — run /session-handoff, then start the next task in a fresh session`;
|
|
228
|
+
}
|
|
229
|
+
|
|
177
230
|
function safeBrand() {
|
|
178
231
|
try {
|
|
179
232
|
const b = branding();
|
|
@@ -219,6 +272,7 @@ function buildCardModel(input = {}) {
|
|
|
219
272
|
|
|
220
273
|
const rows = [];
|
|
221
274
|
const facts = [];
|
|
275
|
+
let handoff = null; // task 1003150 — the session-boundary nudge; null on all but a heavy ship
|
|
222
276
|
const addRow = (k, v) => { if (v != null && v !== '') rows.push({ k, v }); };
|
|
223
277
|
const addFact = (label, value, opts = {}) => { if (value != null && value !== '') facts.push({ label, value, ...opts }); };
|
|
224
278
|
|
|
@@ -319,6 +373,10 @@ function buildCardModel(input = {}) {
|
|
|
319
373
|
|
|
320
374
|
addRow('Value', value); addRow('Changed', changed); addRow('Grade', gradeText);
|
|
321
375
|
addRow('Credits', creditsText); addRow('Usage', usageText); addRow('Deploy', deployText);
|
|
376
|
+
// task 1003150: the session-boundary nudge. Not a row — it qualifies the
|
|
377
|
+
// Next-> action rather than reporting a fact about the task, so both renderers
|
|
378
|
+
// hang it off the action. Usually null.
|
|
379
|
+
handoff = handoffAdvice({ stage, cachedTokens: usage.cachedTokens });
|
|
322
380
|
|
|
323
381
|
addFact('', changed);
|
|
324
382
|
if (gradeText) addFact('grade', gradeText, { cls: gradeCls });
|
|
@@ -345,7 +403,7 @@ function buildCardModel(input = {}) {
|
|
|
345
403
|
notes: input.notes || '',
|
|
346
404
|
description: input.description || '',
|
|
347
405
|
issues,
|
|
348
|
-
rows, facts, action,
|
|
406
|
+
rows, facts, action, handoff,
|
|
349
407
|
};
|
|
350
408
|
}
|
|
351
409
|
|
|
@@ -403,6 +461,9 @@ function renderMonospaceCard(model) {
|
|
|
403
461
|
for (const r of m.rows) kv(r.k, r.v);
|
|
404
462
|
out.push(bar);
|
|
405
463
|
kv('Next →', m.action.kind === 'link' ? (m.action.url || '(sandbox)') : m.action.cmd);
|
|
464
|
+
// task 1003150: aligned under the Next action it qualifies, not as a row — this
|
|
465
|
+
// is guidance about the SESSION, not a fact about the task.
|
|
466
|
+
if (m.handoff) for (const l of wrap(`⚠ ${m.handoff}`, MONO_W - 9)) out.push(' ' + ' '.repeat(9) + l);
|
|
406
467
|
return out.join('\n');
|
|
407
468
|
}
|
|
408
469
|
|
|
@@ -459,6 +520,12 @@ function renderCardHtml(model) {
|
|
|
459
520
|
actionHtml = `<button class="cc-link cc-next" onclick="sendPrompt('${cmd}')">${esc(m.action.label)} ↗</button>`;
|
|
460
521
|
}
|
|
461
522
|
|
|
523
|
+
// task 1003150: the boundary nudge rides beside the action, muted — it is
|
|
524
|
+
// advice, not an alarm, and it must not outshout the Next button.
|
|
525
|
+
const handoffHtml = m.handoff
|
|
526
|
+
? `<span class="cc-sep">·</span><span style="color:var(--color-text-tertiary);">${esc(m.handoff)}</span>`
|
|
527
|
+
: '';
|
|
528
|
+
|
|
462
529
|
const factsSr = m.facts.map((f) => `${f.label ? f.label + ' ' : ''}${f.value}`).join('. ');
|
|
463
530
|
const srText = `${m.cfg.headline(m.taskId)}. ${factsSr}. A collapsible summary expands on click.`;
|
|
464
531
|
|
|
@@ -490,7 +557,7 @@ function renderCardHtml(model) {
|
|
|
490
557
|
${actionHtml}
|
|
491
558
|
</div>
|
|
492
559
|
<div style="display:flex;align-items:baseline;gap:10px;">
|
|
493
|
-
<div style="font-size:13px;color:var(--color-text-secondary);line-height:1.7;flex:1;min-width:0;">${facts}</div>
|
|
560
|
+
<div style="font-size:13px;color:var(--color-text-secondary);line-height:1.7;flex:1;min-width:0;">${facts}${handoffHtml}</div>
|
|
494
561
|
<button class="cc-link cc-sum" aria-label="Toggle task summary" onclick="var s=this.parentElement.nextElementSibling;var o=getComputedStyle(s).display==='none';s.style.display=o?'block':'none';this.querySelector('i').style.transform=o?'rotate(90deg)':'';">Summary <span class="cc-chev"><i class="ti ti-chevron-right" aria-hidden="true"></i></span></button>
|
|
495
562
|
</div>
|
|
496
563
|
<div style="display:none;margin-top:0.6rem;padding-top:0.55rem;border-top:0.5px solid var(--color-border-tertiary);font-size:13px;line-height:1.6;">${summaryBody}</div>
|
|
@@ -538,5 +605,7 @@ module.exports = {
|
|
|
538
605
|
STAGES, TONE, stageFor, buildCardModel, renderMonospaceCard, renderCardHtml, emitCard,
|
|
539
606
|
// exported for unit tests:
|
|
540
607
|
fmtTokens, fmtUsd, fmtInt, fmtMins, nextActionFor, esc, wrap, changedText,
|
|
608
|
+
// task 1003150 — the session-boundary nudge, pure so every branch is pinned.
|
|
609
|
+
handoffAdvice, HEAVY_SESSION_CACHED_TOKENS,
|
|
541
610
|
PIPELINE, FORMS, trailSteps,
|
|
542
611
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.278",
|
|
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.278",
|
|
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.278",
|
|
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
|
@@ -979,8 +979,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
979
979
|
// landed since 1.19.274 with no explicit bump. run 32492414871. (task 1002620)
|
|
980
980
|
// 1.19.276 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
981
981
|
// landed since 1.19.275 with no explicit bump. run 32499257959. (task 1002620)
|
|
982
|
+
// 1.19.277 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
983
|
+
// landed since 1.19.276 with no explicit bump. run 32503053532. (task 1002620)
|
|
984
|
+
// 1.19.278 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
985
|
+
// landed since 1.19.277 with no explicit bump. run 32508270995. (task 1002620)
|
|
982
986
|
// ---------------------------------------------------------------------------
|
|
983
|
-
const CORE_VERSION = '1.19.
|
|
987
|
+
const CORE_VERSION = '1.19.278'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
984
988
|
|
|
985
989
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
986
990
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// tests/session_boundary_handoff.mjs
|
|
2
|
+
//
|
|
3
|
+
// THE SESSION BOUNDARY — task 1003150 (goal 1000053 "Session efficiency").
|
|
4
|
+
//
|
|
5
|
+
// The mirror of that goal's session-START series (1002435-1002438): those
|
|
6
|
+
// front-load what a session can know at open; this stops a session dragging what
|
|
7
|
+
// it no longer needs at close. A shipped task is the one moment the system KNOWS
|
|
8
|
+
// the work is finished, and the moment almost all of that context becomes dead
|
|
9
|
+
// weight.
|
|
10
|
+
//
|
|
11
|
+
// TWO HALVES, both pinned here:
|
|
12
|
+
// 1. ship-card.js handoffAdvice — PURE, so every branch is a real assertion with
|
|
13
|
+
// no card, no DB, no session.
|
|
14
|
+
// 2. .claude/skills/builder-sequence/SKILL.md — a DOC-CONTRACT check. The skill
|
|
15
|
+
// is the only place the between-links handoff can live (a hook cannot clear a
|
|
16
|
+
// conversation), and a prose step with nothing asserting it is exactly the
|
|
17
|
+
// kind of instruction that gets tidied away in a later edit. This is the same
|
|
18
|
+
// posture as the fitness doc checks: assert the RULE is stated, not its wording.
|
|
19
|
+
//
|
|
20
|
+
// WHY A THRESHOLD TEST AT ALL. The failure mode here is not a crash, it is
|
|
21
|
+
// wallpaper: advice that prints on every ship stops being read (the
|
|
22
|
+
// deploy-divergence footer is the in-repo cautionary tale — 21 core versions of
|
|
23
|
+
// identical warning that nobody acted on). So "stays silent on a normal ship" is
|
|
24
|
+
// the assertion that matters most, and it is first.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/session_boundary_handoff.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { makeRunner } from './helpers.mjs';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const card = require('../modules/lifecycle/ship-card.js');
|
|
38
|
+
const { handoffAdvice, HEAVY_SESSION_CACHED_TOKENS, buildCardModel, renderMonospaceCard, renderCardHtml } = card;
|
|
39
|
+
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
// The three real sessions the threshold was calibrated against (2026-08-21).
|
|
43
|
+
const MEASURED = { light: 11.3e6, dragging: 36.1e6, heavy: 78.1e6 };
|
|
44
|
+
|
|
45
|
+
// =========================================================================
|
|
46
|
+
// 1. handoffAdvice — the decision
|
|
47
|
+
// =========================================================================
|
|
48
|
+
|
|
49
|
+
await test('SILENT on an ordinary ship — the whole design rests on this', () => {
|
|
50
|
+
// Advice that fires every time is wallpaper. A short session must never see it.
|
|
51
|
+
for (const cached of [0, 1, 1e6, MEASURED.light, HEAVY_SESSION_CACHED_TOKENS - 1]) {
|
|
52
|
+
assert.equal(handoffAdvice({ stage: 'shipped', cachedTokens: cached }), null,
|
|
53
|
+
`${cached} cached must stay silent`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await test('SPEAKS once the session is genuinely heavy', () => {
|
|
58
|
+
for (const cached of [HEAVY_SESSION_CACHED_TOKENS, MEASURED.dragging, MEASURED.heavy]) {
|
|
59
|
+
const advice = handoffAdvice({ stage: 'shipped', cachedTokens: cached });
|
|
60
|
+
assert.ok(advice, `${cached} cached should advise`);
|
|
61
|
+
assert.match(advice, /session-handoff/, 'and names the command that does something about it');
|
|
62
|
+
assert.match(advice, /fresh session/, 'and says what to do with it');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('the advice quotes the ACTUAL figure, so it is arguable', () => {
|
|
67
|
+
// A builder who disagrees can check the number rather than take it on faith.
|
|
68
|
+
assert.match(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.heavy }), /78\.1M cached/);
|
|
69
|
+
assert.match(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.dragging }), /36\.1M cached/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await test('NEVER mid-task — resetting inside a claim is actively wrong', () => {
|
|
73
|
+
// Mid-task the context is what the work is standing on. Only a stage where the
|
|
74
|
+
// task has left the builder's hands may advise a reset.
|
|
75
|
+
for (const stage of ['claimed', 'review', 'built', 'completed', 'failed']) {
|
|
76
|
+
assert.equal(handoffAdvice({ stage, cachedTokens: MEASURED.heavy }), null,
|
|
77
|
+
`stage '${stage}' must not advise a reset`);
|
|
78
|
+
}
|
|
79
|
+
// ...and the two that may.
|
|
80
|
+
assert.ok(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.heavy }));
|
|
81
|
+
assert.ok(handoffAdvice({ stage: 'confirmed', cachedTokens: MEASURED.heavy }),
|
|
82
|
+
'confirmed counts: the task is out of the builder\'s hands even though it has not landed');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await test('hostile or absent input is silence, never a throw', () => {
|
|
86
|
+
// This runs inside the ship card. A card that CRASHES at the end of a
|
|
87
|
+
// successful ship would turn a landed task into a scary traceback.
|
|
88
|
+
for (const args of [undefined, {}, null, { stage: 'shipped' }, { stage: 'shipped', cachedTokens: null },
|
|
89
|
+
{ stage: 'shipped', cachedTokens: 'lots' }, { stage: 'shipped', cachedTokens: NaN },
|
|
90
|
+
{ stage: 'shipped', cachedTokens: -1 }, { stage: null, cachedTokens: 1e9 }]) {
|
|
91
|
+
assert.doesNotThrow(() => handoffAdvice(args), `${JSON.stringify(args)} must not throw`);
|
|
92
|
+
assert.equal(handoffAdvice(args), null, `${JSON.stringify(args)} ⇒ silence`);
|
|
93
|
+
}
|
|
94
|
+
assert.equal(handoffAdvice(null), null, 'explicit null ⇒ silence, not a crash');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test('the threshold sits between the session that felt fine and the one that dragged', () => {
|
|
98
|
+
// Documents the calibration as an assertion: if someone retunes the constant,
|
|
99
|
+
// this says what the number was chosen to separate.
|
|
100
|
+
assert.ok(HEAVY_SESSION_CACHED_TOKENS > MEASURED.light,
|
|
101
|
+
'the 11.3M session was fine and must stay quiet');
|
|
102
|
+
assert.ok(HEAVY_SESSION_CACHED_TOKENS < MEASURED.dragging,
|
|
103
|
+
'the 36.1M session had begun to drag and should be caught');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// =========================================================================
|
|
107
|
+
// 2. The card — the advice has to actually reach the builder
|
|
108
|
+
// =========================================================================
|
|
109
|
+
|
|
110
|
+
await test('a heavy ship card carries the nudge, in BOTH renderers', () => {
|
|
111
|
+
const model = buildCardModel({
|
|
112
|
+
stage: 'shipped', taskId: 1003150, title: 'A task',
|
|
113
|
+
usage: { cachedTokens: MEASURED.heavy, newTokens: 891e3, costUsd: 49.81 },
|
|
114
|
+
});
|
|
115
|
+
assert.ok(model.handoff, 'it is on the model');
|
|
116
|
+
const mono = renderMonospaceCard(model);
|
|
117
|
+
assert.match(mono, /heavy session/, 'terminal card shows it');
|
|
118
|
+
assert.match(mono, /Next →/, 'and it sits with the Next action it qualifies');
|
|
119
|
+
// Position matters: the nudge must come AFTER Next, as a qualifier, not above it.
|
|
120
|
+
assert.ok(mono.indexOf('Next →') < mono.indexOf('heavy session'), 'below the action, not shouting over it');
|
|
121
|
+
assert.match(renderCardHtml(model), /heavy session/, 'in-feed card shows it too');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await test('a normal ship card is BYTE-IDENTICAL to before the nudge existed', () => {
|
|
125
|
+
// The regression that matters: every ordinary ship must look exactly as it did.
|
|
126
|
+
const usage = { cachedTokens: MEASURED.light, newTokens: 262e3, costUsd: 8.06 };
|
|
127
|
+
const model = buildCardModel({ stage: 'shipped', taskId: 1003148, title: 'A task', usage });
|
|
128
|
+
assert.equal(model.handoff, null);
|
|
129
|
+
const mono = renderMonospaceCard(model);
|
|
130
|
+
assert.doesNotMatch(mono, /heavy session/);
|
|
131
|
+
assert.doesNotMatch(mono, /session-handoff/);
|
|
132
|
+
assert.doesNotMatch(renderCardHtml(model), /heavy session/);
|
|
133
|
+
// A card with no usage data at all (old breadcrumbs) must also stay clean.
|
|
134
|
+
const bare = buildCardModel({ stage: 'shipped', taskId: 1, title: 'A task' });
|
|
135
|
+
assert.equal(bare.handoff, null);
|
|
136
|
+
assert.doesNotMatch(renderMonospaceCard(bare), /heavy session/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await test('the nudge is not a Usage/Deploy ROW — it is guidance, not a task fact', () => {
|
|
140
|
+
const model = buildCardModel({
|
|
141
|
+
stage: 'shipped', taskId: 1, title: 'A task',
|
|
142
|
+
usage: { cachedTokens: MEASURED.heavy },
|
|
143
|
+
});
|
|
144
|
+
assert.equal(model.rows.some((r) => /heavy session/.test(String(r.v))), false,
|
|
145
|
+
'rows report facts about the TASK; this is advice about the SESSION');
|
|
146
|
+
assert.equal(model.facts.some((f) => /heavy session/.test(String(f.value))), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// =========================================================================
|
|
150
|
+
// 3. /builder-sequence — the doc contract
|
|
151
|
+
//
|
|
152
|
+
// A hook cannot clear a conversation and /session-handoff is read-only, so the
|
|
153
|
+
// runner skill is the ONLY place the between-links handoff can live. That makes
|
|
154
|
+
// its prose load-bearing, and load-bearing prose needs a test.
|
|
155
|
+
// =========================================================================
|
|
156
|
+
|
|
157
|
+
const SEQ = fs.readFileSync(path.join(ROOT, '.claude/skills/builder-sequence/SKILL.md'), 'utf8');
|
|
158
|
+
|
|
159
|
+
await test('the sequence runner states the between-links handoff step', () => {
|
|
160
|
+
assert.match(SEQ, /\/session-handoff/, 'it names the command');
|
|
161
|
+
assert.match(SEQ, /fresh session/i, 'and that the chain continues in a fresh one');
|
|
162
|
+
assert.match(SEQ, /heavy session/i, 'and the ship-card signal it keys off');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await test('...and tells the runner to STOP rather than claim the next link', () => {
|
|
166
|
+
// The failure mode: a runner that emits the prompt and then helpfully carries on
|
|
167
|
+
// in the same window has done the paperwork and none of the work.
|
|
168
|
+
assert.match(SEQ, /do not claim the next task in this window/i,
|
|
169
|
+
'the halt must be explicit — emitting a prompt and continuing anyway is the trap');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('...and says WHY, with the measurement, not just what', () => {
|
|
173
|
+
// An unexplained instruction is the first thing dropped in a later edit.
|
|
174
|
+
assert.match(SEQ, /re-?read/i, 'names the mechanism (context re-read per turn)');
|
|
175
|
+
assert.match(SEQ, /11\.3M|36\.1M|78\.1M/, 'carries the measured figures');
|
|
176
|
+
assert.match(SEQ, /durable|GDS/, 'and why a fresh window is safe: the chain state is durable');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await test('the runner is warned against running a chain to exhaustion', () => {
|
|
180
|
+
assert.match(SEQ, /What NOT to do/, 'the section exists');
|
|
181
|
+
assert.match(SEQ, /exhaustion|one window/i, 'and names the anti-pattern this step prevents');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
summary('session_boundary_handoff');
|