@bongos/core 1.24.67 → 1.24.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.24.67",
6
- "core_contract": "1.24.67",
7
- "source_commit": "cb3a73a80b0216905417a6379f4caf4f66a159c2",
5
+ "core_version": "1.24.69",
6
+ "core_contract": "1.24.69",
7
+ "source_commit": "68d624516bb357028ebcde65caf3b4a8fec22239",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-09T20:07:37.283Z",
9
+ "built_at": "2026-11-09T21:00:37.018Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 638,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3904,
14
+ "functional_verbatim": 3905,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4575,
20
- "tree_sha256": "ab4503425d89d4eec00873f68e4ed95088cb713b9816a7a0f3cc218da4f734d1",
19
+ "file_count": 4576,
20
+ "tree_sha256": "dbadf59ddc54d1764c6ad589df54766251762350991b5db66d08fb3283a5f471",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -557,7 +557,7 @@
557
557
  {
558
558
  "path": "docs/adr/0029-status-page-outage-resilience.md",
559
559
  "mode": "0000644",
560
- "sha256": "ec0298ea8652e72c00f90546a57a08a1aca9d0208593bb56bde0a7254aa1e851"
560
+ "sha256": "199fc3e775f32f46a658294bbbe83783a7455cff2b554f8680f4235b1ae0ece1"
561
561
  },
562
562
  {
563
563
  "path": "docs/adr/0030-builders-hall-live-updates.md",
@@ -3007,7 +3007,7 @@
3007
3007
  {
3008
3008
  "path": "docs/module-api-changelog.md",
3009
3009
  "mode": "0000644",
3010
- "sha256": "b2ea544c7a4c86e2c83d079531d105a0f8ade34933bfe96929c5bd8256dcea5c"
3010
+ "sha256": "65351a8f07adc711a0045deb60cad8493c943702365a855d6400748fbdf2108d"
3011
3011
  },
3012
3012
  {
3013
3013
  "path": "docs/modules-contract.md",
@@ -3127,7 +3127,7 @@
3127
3127
  {
3128
3128
  "path": "docs/recipes/core-release-pipeline.md",
3129
3129
  "mode": "0000644",
3130
- "sha256": "c5840a0cae12bde4eef54a5b2785046fe3bfbb750bb7be7bc3808e4eb3491d0d"
3130
+ "sha256": "09cb01eeb9aa79bca4380b11498c05dd30e4985358e8cb8fdec7497b2a9581b6"
3131
3131
  },
3132
3132
  {
3133
3133
  "path": "docs/recipes/failed-ship-recovery-proof.md",
@@ -3262,7 +3262,7 @@
3262
3262
  {
3263
3263
  "path": "docs/recipes/ui-look-before-you-ship.md",
3264
3264
  "mode": "0000644",
3265
- "sha256": "691bbbf06c9622a91aa8b4f5d40a4be2e7198e9219a6a689bb7dd1ecb1430f94"
3265
+ "sha256": "c78908392eedeed9255a952f959c866cd6282acd6a746258e7761e156f2bcff0"
3266
3266
  },
3267
3267
  {
3268
3268
  "path": "docs/recipes/upgrading-the-core.md",
@@ -8157,7 +8157,7 @@
8157
8157
  {
8158
8158
  "path": "modules/lifecycle/merge-reach.js",
8159
8159
  "mode": "0000644",
8160
- "sha256": "77ee8eb1615feb977701f8f50f14a080b9ebb0cda07dbc5ce0a5061ab2063fb8"
8160
+ "sha256": "422b941660edc139d0353cccb110c8d130827c02200511da372d23456f133e51"
8161
8161
  },
8162
8162
  {
8163
8163
  "path": "modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql",
@@ -11357,7 +11357,7 @@
11357
11357
  {
11358
11358
  "path": "modules/ui-design/kit/README.md",
11359
11359
  "mode": "0000644",
11360
- "sha256": "a95397538d363364347d76a875663ffc9b36eb1cba03475f11bc175e2e2787fc"
11360
+ "sha256": "8fe3b49ba8dc29bd0f923c300978e6650fe27b7937dacef9f14b3f368babf52b"
11361
11361
  },
11362
11362
  {
11363
11363
  "path": "modules/ui-design/kit/check-mock.js",
@@ -11667,7 +11667,7 @@
11667
11667
  {
11668
11668
  "path": "modules/ui-design/kit/lib.js",
11669
11669
  "mode": "0000644",
11670
- "sha256": "e593169a194cd60838193df799dfb3b19ac72e92e2e6beadb3cd28128305abad"
11670
+ "sha256": "70d6c629ee019f96d10726cdc70e0faa2f1a3422a858d70ea9dd0d3cc17002f8"
11671
11671
  },
11672
11672
  {
11673
11673
  "path": "modules/ui-design/kit/png.js",
@@ -11902,12 +11902,12 @@
11902
11902
  {
11903
11903
  "path": "package-lock.json",
11904
11904
  "mode": "0000644",
11905
- "sha256": "02030268a28d3b76dc99198d41bf1de043460b72e8dedde3caa400b65c5ea4bf"
11905
+ "sha256": "7965aa4c685c6bdd12a6710eb50550a823662415b832362a07f41b2b8ac7d2af"
11906
11906
  },
11907
11907
  {
11908
11908
  "path": "package.json",
11909
11909
  "mode": "0000644",
11910
- "sha256": "f18b77185a5d2b924bf459cc5415880fea61d1118f9a358f0691b8d703565120"
11910
+ "sha256": "fb8d2aa51f2449ffe9ba7c21b564fdc36c288f5fd55737f16b245dd60430d780"
11911
11911
  },
11912
11912
  {
11913
11913
  "path": "public-docs/index.html",
@@ -11927,7 +11927,7 @@
11927
11927
  {
11928
11928
  "path": "release-notes.json",
11929
11929
  "mode": "0000644",
11930
- "sha256": "576be7645a892c65c51e1ae3c8a17d7a5ecb2a2505cf9467dfff7a26b7aa5679"
11930
+ "sha256": "325bd7b65c334113a2600e606108a7f2597e9fa0ba045ef76170d3d4907af92c"
11931
11931
  },
11932
11932
  {
11933
11933
  "path": "scripts/bongos/CLAUDE.md",
@@ -12182,7 +12182,7 @@
12182
12182
  {
12183
12183
  "path": "scripts/bongos/claim.js",
12184
12184
  "mode": "0000644",
12185
- "sha256": "bb8b7a0104622d725c7057f04aea6b82f0586f6323a761302afdb3942fb65ae2"
12185
+ "sha256": "377cd8c4aa3bff219bf2d071cdfe0f918a3ceb30b775d8c881f329f36173f1e0"
12186
12186
  },
12187
12187
  {
12188
12188
  "path": "scripts/bongos/claude-materialize.js",
@@ -12292,7 +12292,7 @@
12292
12292
  {
12293
12293
  "path": "scripts/bongos/deploy-divergence.js",
12294
12294
  "mode": "0000644",
12295
- "sha256": "bf18a79242d650a5974b40607dbedcf2d40c9c65d86ddc61cdf19e242cc4f2cb"
12295
+ "sha256": "82b9f7657d6fcd5ed8bd79e4e7104e3b960b649eaf122d6ab871f48b86494ad9"
12296
12296
  },
12297
12297
  {
12298
12298
  "path": "scripts/bongos/deploy-main-audit-gate.sh",
@@ -15867,7 +15867,7 @@
15867
15867
  {
15868
15868
  "path": "src/instance-config.js",
15869
15869
  "mode": "0000644",
15870
- "sha256": "17daf4a38c2e208130fbfe5afe56f548d9bc3bfa9fd57a2ab2dbe459748da811"
15870
+ "sha256": "b123541932f206894a652aebf64a13ed0794a4b44e68936a11d65bf49c7a2fc9"
15871
15871
  },
15872
15872
  {
15873
15873
  "path": "src/link-preview.js",
@@ -15877,7 +15877,7 @@
15877
15877
  {
15878
15878
  "path": "src/module-api.js",
15879
15879
  "mode": "0000644",
15880
- "sha256": "ab5adbd2f126b45470953b8c28545124554f69e04a1ba70494471d888288d509"
15880
+ "sha256": "610ccbeab694b699af5e1a2e5142760bf95ce1c3c8affe30d076d36272390410"
15881
15881
  },
15882
15882
  {
15883
15883
  "path": "src/module-loader/catalog.js",
@@ -16087,7 +16087,7 @@
16087
16087
  {
16088
16088
  "path": "tests/agent_standing_rules.mjs",
16089
16089
  "mode": "0000644",
16090
- "sha256": "f296357646d9eb778a5334b75742ef1172ff87e10b26a5e7eaffe0e8fe983120"
16090
+ "sha256": "08edffc3a673b01249586330d7cb0fbb4f31d7dfaaadc80864191899fedb5545"
16091
16091
  },
16092
16092
  {
16093
16093
  "path": "tests/agents_authoring.mjs",
@@ -16814,6 +16814,11 @@
16814
16814
  "mode": "0000644",
16815
16815
  "sha256": "8b937f463b8f03dba8c67047d8aca9be84a00839f4821e00ee5d31a96a6bead9"
16816
16816
  },
16817
+ {
16818
+ "path": "tests/claim_finish_hint.mjs",
16819
+ "mode": "0000644",
16820
+ "sha256": "a8c2e33617c23d9f6584f2901a7ad9f77f0ed5588b2dd219ba79b3c4c805b4b0"
16821
+ },
16817
16822
  {
16818
16823
  "path": "tests/claim_from_task_and_home.mjs",
16819
16824
  "mode": "0000644",
@@ -17397,7 +17402,7 @@
17397
17402
  {
17398
17403
  "path": "tests/deploy_divergence_line.mjs",
17399
17404
  "mode": "0000644",
17400
- "sha256": "fcf4e6ca24b37e8872ffc1830aafa2dbeb8e28131bec0bebba622114001d3182"
17405
+ "sha256": "b4067239ab71d3aaf3fa7e05fd4a1aaee3938a975970265f3f1679eeab838062"
17401
17406
  },
17402
17407
  {
17403
17408
  "path": "tests/deploy_handoff.mjs",
@@ -19517,7 +19522,7 @@
19517
19522
  {
19518
19523
  "path": "tests/instance_config.mjs",
19519
19524
  "mode": "0000644",
19520
- "sha256": "ba3404f1fd37d33dac3e64ac17a0c71fd8d73a56f7fcef362f5ffd1423136e55"
19525
+ "sha256": "355349970d4a6f903a55bebacdde11eaca4283b22c8ee8266b346a9ecdd8a8ca"
19521
19526
  },
19522
19527
  {
19523
19528
  "path": "tests/instance_db_migrated_on_code_move.mjs",
@@ -21862,7 +21867,7 @@
21862
21867
  {
21863
21868
  "path": "tests/shell_hooks.mjs",
21864
21869
  "mode": "0000644",
21865
- "sha256": "3105a2a2c7a911e1993efd83af537fb2294b332cd5262ab83b41cc720e98a3da"
21870
+ "sha256": "efafe5abb508560bae1287e6e811f9efb907280b1c032e5bb07e6027f6b17add"
21866
21871
  },
21867
21872
  {
21868
21873
  "path": "tests/shell_stat_portability.mjs",
@@ -22267,7 +22272,7 @@
22267
22272
  {
22268
22273
  "path": "tests/status_mirror_sync.mjs",
22269
22274
  "mode": "0000644",
22270
- "sha256": "78d32bb02f7e40885d3c1a0a23209f92531d3eaabe212728d1993af0b7cccaea"
22275
+ "sha256": "9112bd164ed3eaf73826677a646e37819d8400c2f857cb2ff668ae9a8173d5d7"
22271
22276
  },
22272
22277
  {
22273
22278
  "path": "tests/status_page_chrome.mjs",
@@ -22567,12 +22572,12 @@
22567
22572
  {
22568
22573
  "path": "tests/ui_design_kit.mjs",
22569
22574
  "mode": "0000644",
22570
- "sha256": "f5160521be5a2edecfe8997275cbbc65b8cf6ac8aac7cca2e90e23378d4e64f3"
22575
+ "sha256": "a5187e0955766f86c5eafe7ec57b5b7af553883a23342cea03ee29278291a807"
22571
22576
  },
22572
22577
  {
22573
22578
  "path": "tests/ui_design_kit_fit.mjs",
22574
22579
  "mode": "0000644",
22575
- "sha256": "010a7b1427aa28d4807093f2ff31871058b366c46daafb1818cda2eee4f404cc"
22580
+ "sha256": "f72a2535bd11c2bfa2d29f01c05af1b2228de1f559551ab214b42f9b7997d78d"
22576
22581
  },
22577
22582
  {
22578
22583
  "path": "tests/ui_design_skills.mjs",
@@ -1,6 +1,6 @@
1
1
  # 0029 — Status-page outage resilience: measure from outside, host off-box, prevent at the source
2
2
 
3
- - **Status:** Accepted. **Slice 1 (external prober) built this session** under task **[#565](https://example.com/builders#/task/565)**: `.github/workflows/status-probe.yml` + `scripts/status-probe.mjs` + this ADR. **Follow-up B (page off-box) has since SHIPPED** — `status.example.com` is served off the droplet from GitHub Pages (repo `example-owner/example-status`), synced by `.github/workflows/sync-status-mirror.yml`. Incident-backfill (follow-up C) is **not yet built**. **Since removed (task 1004858, 2026-10-25):** the in-process `uptime-poller.js` (nothing started it), the `status-probe.mjs` prober (its workflow was already gone) and the `/public/uptime` feed — the file references below are history.
3
+ - **Status:** Accepted. **Slice 1 (external prober) built this session** under task **[#565](https://example.com/builders#/task/565)**: `.github/workflows/status-probe.yml` + `scripts/status-probe.mjs` + this ADR. **Follow-up B (page off-box) has since SHIPPED** — `status.example.com` is served off the droplet from GitHub Pages (repo `example-owner/example-status`). **No workflow syncs it** (task 1005131): `sync-status-mirror.yml` is not in this repo, so the page updates only when someone runs `node scripts/status-mirror-sync.js --repo-dir <mirror-checkout> --push` by hand. Incident-backfill (follow-up C) is **not yet built**. **Since removed (task 1004858, 2026-10-25):** the in-process `uptime-poller.js` (nothing started it), the `status-probe.mjs` prober (its workflow was already gone) and the `/public/uptime` feed — the file references below are history.
4
4
  - **Date:** 2026-06-03
5
5
  - **Deciders:** Lars Kristensen (Archon), Claude
6
6
  - **Track:** `internal` (the status dashboard / The Watch)
@@ -29,7 +29,7 @@ An **external prober runs on GitHub-hosted runners** — off our DigitalOcean in
29
29
  - `.github/workflows/status-probe.yml` — `*/5` cron + `workflow_dispatch`; publishes the result to an **orphan `status-data` branch** (off-box, web-readable, survives a droplet outage); **fails the job when the site is confirmed down**, which emails the repo owner — the alert.
30
30
 
31
31
  ### 2. Host the page off-box (follow-up B — SHIPPED)
32
- `status.example.com` is now served off the droplet from **GitHub Pages** (repo `example-owner/example-status`), synced by [`.github/workflows/sync-status-mirror.yml`](../../.github/workflows/sync-status-mirror.yml); the page no longer shares fate with the product. Original plan, for the record:
32
+ `status.example.com` is now served off the droplet from **GitHub Pages** (repo `example-owner/example-status`), published by hand with `scripts/status-mirror-sync.js` (no workflow runs it — task 1005131); the page no longer shares fate with the product. Original plan, for the record:
33
33
  Move `public-status/` to free static hosting **separate from the droplet** (Cloudflare Pages or GitHub Pages). The page shell + the **uptime/incident section** are served off-box and read the `status-data` source, so they stay truthful during an outage. The rich GDS panels (treasury, grades, progress) keep loading client-side from `example.com/api/gds/public/*` and **degrade gracefully** to "origin unreachable" when the droplet is down (requires adding CORS to those public routes). Then repoint `status.example.com` DNS at the off-box host.
34
34
 
35
35
  ### 3. Prevent at the source + alert (blocker [#6](https://example.com/builders#/blocker/6))
@@ -3388,5 +3388,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
3388
3388
  landed since 1.24.65 with no explicit bump. run 42686885661. (task 1002620)
3389
3389
  1.24.67 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3390
3390
  landed since 1.24.66 with no explicit bump. run 42689728958. (task 1002620)
3391
+ 1.24.68 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3392
+ landed since 1.24.67 with no explicit bump. run 42692736161. (task 1002620)
3393
+ 1.24.69 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3394
+ landed since 1.24.68 with no explicit bump. run 42698123714. (task 1002620)
3391
3395
  ---------------------------------------------------------------------------
3392
3396
  ```
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Who | Gets a merged change | How to check |
6
6
  |---|---|---|
7
- | **cloudbongos.com** — the projects hub and the builders hall | only when a **published core release** carries it **and the hub's pin moves** — owner-gated, never automatic on merge | its `/version`; the ship card's **Deploy** row reads it: *"live at `<sha>` (includes this merge \| not yet)"* |
7
+ | **cloudbongos.com** — the projects hub and the builders hall | only when a **published core release** carries it **and the hub's pin moves** — its patch-channel subscription moves the pin to a new patch release on its own (task 1005020); a new minor release waits for the owner | its `/version`; the ship card's **Deploy** row reads it: *"live at `<sha>` (includes this merge \| not yet)"* |
8
8
  | **Projects hosted on the platform** | only when a **published core release** carries it (auto-published per merge, [ADR 0161](../adr/0161-publish-on-merge.md)) **and that project's pin moves** — a patch-channel subscription or a core-upgrade | that project's `/version` `coreVersion`; the ship card's **Projects** row states the rule |
9
9
 
10
10
  So a shipped task is live nowhere until a release carries it, and then only on an instance whose pin has moved — the hub included. The release itself waits on a green integration lane: the publish lane's integration gate refuses while `integration` on `main` is red. Never call work live until that instance's `/version` shows it. (This table used to say the hub redeploys from `main` on every merge, within minutes. It never did; on 2026-10-25 the hub had served one build through 200+ merges — task 1004873.) The wording has one source, [`modules/lifecycle/merge-reach.js`](../../modules/lifecycle/merge-reach.js); the ship card, `deploy-divergence.js` and the SessionStart line all print it (task 1004682). `/recall who sees a merge` finds the same rule as learning 1000292, which corrects learning 1000291 (that one still says the hub redeploys on every merge; the API has no way to edit a learning).
@@ -297,7 +297,7 @@ If live is **at or past** `<VER>`, stop and pick again. On 2026-09-17 a paste pr
297
297
 
298
298
  What each piece is for — each guards a failure that actually happened:
299
299
 
300
- - **`BONGOS_INSTANCE_ROOT`, not `CLOUDBONGOS_INSTANCE_ROOT`.** The legacy spelling still works but prints a deprecation warning three times per run and stops being read in core 1.21; the new spelling is verified in this exact line (2026-09-17, twice).
300
+ - **`BONGOS_INSTANCE_ROOT`, not `CLOUDBONGOS_INSTANCE_ROOT`.** The legacy spelling still works but prints a deprecation warning three times per run (it is still read, but will stop being read in a future core release); the new spelling is verified in this exact line (2026-09-17, twice).
301
301
  - **The token lives in root-only `/etc/cloudbongos/npm.env`, and is handed DOWN to `lars`.** This is the step that used to be documented wrongly, so read it before improvising. `NPM_TOKEN` is **not** in lars’s environment, **not** in the interactive profile, and **not** in `/home/lars/.npmrc` — all three probed empty (2026-08-17). An earlier version of this runbook grepped `~/.npmrc` and therefore aborted on its own `[ -n "$NPM_TOKEN" ]` guard every time. Worse, it could not have worked even if that file were populated: the project `.npmrc` says `_authToken=<redacted>, which **overrides** lars’s user `~/.npmrc`, so the variable is the only channel. Hence the shape above: `set -a; . /etc/cloudbongos/npm.env; set +a` reads it as **root** (mode 600), and `--preserve-env=NPM_TOKEN` hands it to the unprivileged `lars` — exactly what systemd does for the sweep unit (`User=lars` + `EnvironmentFile=`). Without it npm falls back to the **public** registry, 404s on the private `@bongos/core`, and rolls back cleanly having taught you nothing.
302
302
  - **Never put the token in the command line — `sudo` logs the full argv (task 1003218).** The shape above is safe *because* the token is read INSIDE the quoted script, so only the `grep` reaches argv. The tempting variant is not:
303
303
  ```
@@ -342,7 +342,7 @@ node scripts/bongos/deploy-divergence.js --url https://<instance-host>
342
342
  It reports the two gaps **separately**, because they have different fixes:
343
343
 
344
344
  - **UNRELEASED** — commits on `main` past the last `CORE_VERSION` bump. *No instance, the hub included, can take these at any pin* — no published version contains them yet. The publish lane (ADR 0161) normally carries a merge within minutes; a gap that stays means the lane is disarmed, red, or refused by its integration gate. The SessionStart line also says whether `main`'s latest merge is in a release yet.
345
- - **UNDEPLOYED** — the live instance's `coreVersion` is behind `main`'s. A release exists it has not picked up: its pin has not moved (gate 3). On the hub that move is owner-gated.
345
+ - **UNDEPLOYED** — the live instance's `coreVersion` is behind `main`'s. A release exists it has not picked up: its pin has not moved (gate 3). On the hub the patch-channel sweep makes that move on its own; a gap that stays means the sweep is held (artist gate, failed health check) or the release is a new minor, which waits for the owner.
346
346
 
347
347
  Exit codes: `0` aligned, `1` diverged, `2` undetermined.
348
348
 
@@ -49,6 +49,7 @@ node modules/ui-design/kit/render.js --page / --style expedition # the sa
49
49
  | `text-fit` | a word of text more than 1px past its box (the padding box of its nearest non-inline ancestor), or an SVG `<text>` past the smallest `<rect>` in its own `<g>` that holds its start — a scroller, `text-overflow: ellipsis` and a visually-hidden label are skipped ([ADR 0329](../adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md)) |
50
50
  | `sibling-overlap` | two in-flow, block-level children of one parent that both carry text overlapping by more than 2px each way — absolute, fixed, sticky and negative-margin layering is deliberate and skipped |
51
51
  | `empty-graphic` | a shown `<use>` that paints nothing (its referenced SVG failed to parse, or the id is missing) or an `<img>` that did not decode or renders 0×0 — the logo that silently vanishes; a sprite sheet's own `<symbol>`/`<defs>`, a hidden graphic and a lazy image not yet loaded are skipped (task 1005104) |
52
+ | `covered-content` | on-screen text a painted layer sits on top of — hit-tested at the middle of each text run with pointer-events forced on, so a `pointer-events: none` veil counts: an ancestor's positioned `::before`/`::after` (the hall's fixed `body::after` plate veil over content not lifted above it), or another element that paints at half alpha or more. A fixed or sticky bar, a masked or clipped shape, a glow that fades to transparent, a transparent stretched-link overlay, the scrim behind an open modal and a visually-hidden label are skipped (task 1005132) |
52
53
  | `one-header-main-h1` | not exactly one *rendered* `<header>`, `<main>` and `<h1>` |
53
54
  | `controls-24px` | a visible control under 24px on either side (`ignoreControls` for the skip link and the like) |
54
55
  | `top-bar-80px` | a `<header>` taller than 80px |
@@ -1,14 +1,16 @@
1
1
  // modules/lifecycle/merge-reach.js — who sees a merge, and when. The ONE source of
2
- // the sentence (task 1004682; corrected by task 1004873).
2
+ // the sentence (task 1004682; corrected by tasks 1004873 and 1005020).
3
3
  //
4
4
  // One path, for every instance — the hub included:
5
5
  // 1. A merge to main is first carried by a PUBLISHED core release. The publish
6
6
  // lane cuts one per merge (ADR 0161), but only while its integration gate is
7
7
  // green; a red integration lane stalls every release behind it.
8
- // 2. An instance gets that release only when its PIN moves. On the hub
9
- // (cloudbongos.com: the projects hub and the builders hall) that is
10
- // owner-gated; on a hosted project it is a patch-channel subscription or a
11
- // core-upgrade.
8
+ // 2. An instance gets that release only when its PIN moves. The hub
9
+ // (cloudbongos.com: the projects hub and the builders hall) is subscribed to
10
+ // the patch channel, so its update sweep moves the pin to a new patch release
11
+ // on its own — no person needed; only a new MINOR release (which the patch
12
+ // channel never takes) waits for the owner's Hall → Deploy. On a hosted
13
+ // project it is a patch-channel subscription or a core-upgrade.
12
14
  //
13
15
  // History: this file used to say the hub "redeploys from main on every merge,
14
16
  // within minutes". It never did — the hub pins a published core like any other
@@ -16,7 +18,9 @@
16
18
  // while sessions told the owner that work was live (task 1004873). Before that,
17
19
  // deploy-divergence.js printed "Merging to main deploys nothing" (ADR 0137), false
18
20
  // in the other direction. The ship card, the divergence report and the SessionStart
19
- // line all read their wording from here, so they cannot drift apart again.
21
+ // line all read their wording from here, so they cannot drift apart again. Then
22
+ // task 1004873's wording called the hub's pin move "owner-gated", which was false
23
+ // for patches: cloudbongos.com moved to core 1.24.12 with no person (task 1005015).
20
24
  //
21
25
  // PURE and dependency-free: callers pass an already-sanitised reading (host, short
22
26
  // sha, inclusion), never a raw network response — some of this text is injected
@@ -26,8 +30,11 @@
26
30
 
27
31
  const HOSTED_LINE = 'Hosted projects: only when a published core release carries the change and their pin moves (patch-channel subscription or a core-upgrade).';
28
32
 
33
+ // How the hub's pin moves (task 1005020): by itself for a patch release, by the owner for a minor.
34
+ const HUB_PIN = 'its patch-channel subscription moves that pin on its own; a new minor release waits for the owner';
35
+
29
36
  function hubRule(host) {
30
- return `${host || 'The hub'} gets a merge only when a published core release carries it and its pin moves (owner-gated)`;
37
+ return `${host || 'The hub'} gets a merge only when a published core release carries it and its pin moves (${HUB_PIN})`;
31
38
  }
32
39
 
33
40
  // includes: true (the live build carries this merge), false (not yet), null (cannot tell).
@@ -36,7 +43,7 @@ function hubLine({ host = null, liveShort = null, includes = null } = {}) {
36
43
  const state = includes === true ? 'includes this merge'
37
44
  : includes === false ? 'not yet'
38
45
  : 'cannot tell yet whether it includes this merge';
39
- return `${host || 'The hub'}: live at ${liveShort} (${state}) — it gets a merge only when a published core release carries it and its pin moves (owner-gated).`;
46
+ return `${host || 'The hub'}: live at ${liveShort} (${state}) — it gets a merge only when a published core release carries it and its pin moves (${HUB_PIN}).`;
40
47
  }
41
48
 
42
49
  // Is main's latest merge in a published release yet? `tipShort` is a short sha,
@@ -12,7 +12,7 @@ node modules/ui-design/kit/contrast.js --pack [look] | --url <url> [--glyphs sel
12
12
 
13
13
  A **look** is a branding pack from the module's style library (`modules/ui-design/styles/<name>/`, task 1003323): `STYLE=<name>` lays it over the instance's own pack through `GDS_BRANDING_FILE`, so any surface renders under any look with no page edit.
14
14
 
15
- Two of the floors are about fit ([task 1004203](https://cloudbongos.com/builders#/task/1004203), [ADR 0329](../../../docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md)): `text-fit` (text past its box, SVG text past its card) and `sibling-overlap` (text-bearing siblings on top of each other). `scripts/bongos/render-check.js` runs just those two over the pages a branch changed, and `/ship-check` shows them as an advisory row. A third in-page floor, `empty-graphic` ([task 1005104](https://cloudbongos.com/builders#/task/1005104)), reports a shown `<use>` or `<img>` that paints nothing — a referenced SVG that failed to parse leaves no console error and no failed request, so the logo vanished under ALL CLEAN.
15
+ Two of the floors are about fit ([task 1004203](https://cloudbongos.com/builders#/task/1004203), [ADR 0329](../../../docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md)): `text-fit` (text past its box, SVG text past its card) and `sibling-overlap` (text-bearing siblings on top of each other). `scripts/bongos/render-check.js` runs just those two over the pages a branch changed, and `/ship-check` shows them as an advisory row. A third in-page floor, `empty-graphic` ([task 1005104](https://cloudbongos.com/builders#/task/1005104)), reports a shown `<use>` or `<img>` that paints nothing — a referenced SVG that failed to parse leaves no console error and no failed request, so the logo vanished under ALL CLEAN. A fourth, `covered-content` ([task 1005132](https://cloudbongos.com/builders#/task/1005132)), reports on-screen text a painted layer sits on top of — the hall specimen read near-black in dark under the fixed `body::after` veil while every other floor passed.
16
16
 
17
17
  A surface describes itself in two JSON files beside it — `<page>.states.json` and `<page>.probes.json` — in the vocabularies `lib.js` defines and validates. A page served at the surface root (the hall's Home, `/builders/`) reads `index.states.json`. A file (or one state) may carry `"storage": { "session": {…}, "local": {…} }`, written into the browser before the page's own scripts run, so a first-visit question renders as a returning visitor sees it: every hall file seeds `hall-craft-asked` so the mode picker stays out of the shots ([task 1005080](https://cloudbongos.com/builders#/task/1005080)). A hall state that needs real data (a goal's detail, the Settings sections) passes against the hall-preview harness with `--base-in`/`--base-out`, one render at a time. The full loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). Exit codes everywhere: 0 clean · 1 problems · 2 a broken file or usage · 3 no browser.
18
18
 
@@ -29,6 +29,7 @@ const FLOORS = Object.freeze([
29
29
  'text-fit', // no text runs past its box (HTML) or its card rect (SVG)
30
30
  'sibling-overlap', // no two in-flow, block-level, text-bearing siblings overlap
31
31
  'empty-graphic', // no shown <use> or <img> that paints nothing (a referenced SVG that failed to parse)
32
+ 'covered-content', // no on-screen text sits under a painted layer (a veil, a plate, an overlay)
32
33
  'one-header-main-h1', // exactly one <header>, one <main>, one <h1>
33
34
  'controls-24px', // every visible control clears 24px on both sides
34
35
  'top-bar-80px', // the page's <header> is at most 80px tall
@@ -307,6 +308,56 @@ function factsInPage(spec) {
307
308
  else if (r.width < 1 || r.height < 1) emptyGraphics.push(`${where(im)} → "${ref(im)}" renders ${Math.round(r.width)}x${Math.round(r.height)}`);
308
309
  });
309
310
  out.emptyGraphics = emptyGraphics;
311
+ // The covered-content floor (task 1005132): the hall specimen read near-black in dark because a fixed
312
+ // body::after veil painted OVER content that was not lifted above it, and every other floor passed. Hit-test the
313
+ // middle of each on-screen text run with pointer-events forced on (a veil is pointer-events: none, which hit
314
+ // testing would otherwise look straight through): a hit outside the text's own box is a layer on top of it, and
315
+ // it counts when that layer paints — an ancestor's positioned ::before/::after, or (with no modal open) another
316
+ // element or its pseudo. A transparent stretched-link overlay and the scrim behind an open dialog are skipped.
317
+ const alpha = (c) => { const m = String(c).match(/rgba?\(([^)]*)\)/); if (!m) return c === 'transparent' ? 0 : 1; const n = m[1].split(/[\s,/]+/).filter(Boolean); return n.length > 3 ? parseFloat(n[3]) * (/%/.test(n[3]) ? 0.01 : 1) : 1; };
318
+ // Opaque enough to hide text: a background colour, or a gradient every stop of which, at half alpha or more —
319
+ // a glow or halo that fades to transparent is not a cover. An image (url()) is not judged, and neither is a
320
+ // masked or clipped layer, whose box is bigger than what it paints (the cosmos orb's shaped disc).
321
+ const solid = (cs) => {
322
+ if ((cs.maskImage || cs.webkitMaskImage || 'none') !== 'none' || (cs.clipPath || 'none') !== 'none') return false;
323
+ const stops = /gradient\(/.test(cs.backgroundImage) ? cs.backgroundImage.match(/rgba?\([^)]*\)|transparent/g) || [] : [];
324
+ return +cs.opacity * Math.max(alpha(cs.backgroundColor), stops.length ? Math.min(...stops.map(alpha)) : 0) >= 0.5;
325
+ };
326
+ const paints = (cs, pseudo) => !!cs && (!pseudo || (cs.content !== 'none' && cs.content !== 'normal' && /absolute|fixed/.test(cs.position))) && cs.display !== 'none' && cs.visibility !== 'hidden' && solid(cs);
327
+ // A fixed or sticky bar that content scrolls under is layered on purpose.
328
+ const pinned = (hit, box) => { for (let a = hit; a && !a.contains(box); a = a.parentElement) if (/fixed|sticky/.test(getComputedStyle(a).position)) return true; return false; };
329
+ const layerOf = (hit, box) => {
330
+ const name = hit === document.body ? 'body' : hit === de ? 'html' : where(hit);
331
+ for (const ps of ['::after', '::before']) if (paints(getComputedStyle(hit, ps), true)) return name + ps;
332
+ return !hit.contains(box) && !pinned(hit, box) && paints(getComputedStyle(hit), false) ? name : null;
333
+ };
334
+ const modal = qa('dialog[open], [aria-modal="true"]').some(vis);
335
+ const coveredText = [], probed = new Set();
336
+ const force = document.createElement('style');
337
+ force.textContent = '*, *::before, *::after { pointer-events: auto !important; }';
338
+ de.appendChild(force);
339
+ try {
340
+ const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
341
+ for (let n = tw.nextNode(); n && probed.size < 300; n = tw.nextNode()) {
342
+ const el = n.parentElement;
343
+ if (!n.nodeValue.trim() || !el || NO_TEXT.test(el.tagName) || el.closest('svg') || !vis(el)) continue;
344
+ let box = el;
345
+ while (box && /^(inline|contents)$/.test(getComputedStyle(box).display)) box = box.parentElement;
346
+ if (!box || probed.has(box) || box.clientWidth < 4 || box.clientHeight < 4) continue; // < 4: a visually-hidden label
347
+ const rg = document.createRange(); rg.selectNodeContents(n);
348
+ const r = [...rg.getClientRects()].find((x) => x.width > 0 && x.height > 0);
349
+ if (!r) continue;
350
+ const x = r.left + r.width / 2, y = r.top + r.height / 2;
351
+ if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) continue;
352
+ probed.add(box);
353
+ const hit = document.elementFromPoint(x, y);
354
+ if (!hit || hit === el || el.contains(hit) || box.contains(hit)) continue;
355
+ if (modal && !hit.contains(box)) continue;
356
+ const layer = layerOf(hit, box);
357
+ if (layer) coveredText.push(`${where(box)} "${quote(n.nodeValue)}" under ${layer}`);
358
+ }
359
+ } finally { force.remove(); }
360
+ out.covered = coveredText;
310
361
  return out;
311
362
  }
312
363
  // Pure: compare collected facts against an expectation. Returns problem strings.
@@ -345,6 +396,7 @@ function auditFloors(facts, { width, mode } = {}) {
345
396
  if (facts.textFit && facts.textFit.length) p.push(`text-fit: ${some(facts.textFit)}`);
346
397
  if (facts.overlaps && facts.overlaps.length) p.push(`sibling-overlap: ${some(facts.overlaps)}`);
347
398
  if (facts.emptyGraphics && facts.emptyGraphics.length) p.push(`empty-graphic: ${some(facts.emptyGraphics)}`);
399
+ if (facts.covered && facts.covered.length) p.push(`covered-content: ${some(facts.covered)}`);
348
400
  if (facts.headers !== 1 || facts.mains !== 1 || facts.h1s !== 1) p.push(`one-header-main-h1: header ${facts.headers} main ${facts.mains} h1 ${facts.h1s}`);
349
401
  if (facts.smallControls && facts.smallControls.length) p.push(`controls-24px: ${facts.smallControls.join(' | ')}`);
350
402
  if (facts.bar && facts.bar.h > 80) p.push(`top-bar-80px: header is ${facts.bar.h}px`);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.67",
3
+ "version": "1.24.69",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.67",
9
+ "version": "1.24.69",
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.24.67",
3
+ "version": "1.24.69",
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",
@@ -11405,5 +11405,37 @@
11405
11405
  "id": "1004383",
11406
11406
  "text": "The CI check that guards every merge is now protected, so a builder below Metic can no longer quietly switch its checks off. Protecting the credit-minting file was held back for the owner to decide, because it would lock the w"
11407
11407
  }
11408
+ ],
11409
+ "1.24.68": [
11410
+ {
11411
+ "id": "1005132",
11412
+ "text": "The design checker now catches text hidden under a dark overlay (the bug that made the components page unreadable in dark mode), instead of reporting it clean."
11413
+ }
11414
+ ],
11415
+ "1.24.69": [
11416
+ {
11417
+ "id": "1005113",
11418
+ "text": "In a project that installs Bongos from npm (like toast), the claim card now tells you to finish with 'npx bongos ship', a command that exists there, instead of a script path that doesn't."
11419
+ },
11420
+ {
11421
+ "id": "1005131",
11422
+ "text": "The status-page docs no longer claim an automatic sync that does not exist. They now say plainly that the off-site status page updates only when someone publishes it by hand, and a test keeps that claim honest."
11423
+ },
11424
+ {
11425
+ "id": "1005022",
11426
+ "text": "The 'you may have made a mistake' nudge no longer fires on automatic messages from other sessions or CI — only on a person's own corrections."
11427
+ },
11428
+ {
11429
+ "id": "1005020",
11430
+ "text": "Status messages now say plainly that cloudbongos.com picks up small (patch) releases on its own — no one has to move it by hand. Only a bigger (minor) release waits for the owner."
11431
+ },
11432
+ {
11433
+ "id": "1005042",
11434
+ "text": "The old-style settings warning no longer promises a removal date that already passed; it now says honestly the old names still work for now."
11435
+ },
11436
+ {
11437
+ "id": "1004949",
11438
+ "text": "When a shell command was blocked by one hook and another hook was missing, the warning about the missing hook vanished. Now both messages show."
11439
+ }
11408
11440
  ]
11409
11441
  }
@@ -763,10 +763,7 @@ async function main() {
763
763
  // best-effort and never block the claim.
764
764
  const shownLearningKeys = await printRelevantLearnings(task);
765
765
  await printClaimRecall(task, { excludeKeys: shownLearningKeys });
766
- console.log('\nWhen you finish:');
767
- console.log(` node scripts/bongos/ship.js ${task.id} --notes "..." --summary "..."`);
768
- console.log('Or to abandon:');
769
- console.log(` node scripts/bongos/release.js ${task.id}`);
766
+ for (const line of finishHintLines(task.id)) console.log(line);
770
767
  // V3.R13 (#237): post-claim nudge — for a newcomer, this becomes
771
768
  // "Next: Ship thy first work" so the path forward is visible. Reuses the
772
769
  // /me fetched above for the context pack (no second round-trip).
@@ -802,6 +799,20 @@ async function main() {
802
799
 
803
800
  // Only auto-run when invoked directly (`node claim.js …`); a `require()` (the
804
801
  // unit test) just imports formatClaimFailure without executing the claim flow.
802
+ // task 1005113 - claim card finish: the "When you finish" hint names the command that
803
+ // exists where this claim ran. A project that installs the core from npm has no
804
+ // scripts/bongos/ of its own — the CLI ships in node_modules/@bongos/core and runs as
805
+ // `npx bongos <verb>` — so a node path there points at nothing. isInstalledCore() is the
806
+ // one positive "this core sits under node_modules" check fitness already uses.
807
+ function finishHintLines(taskId, { installed } = {}) {
808
+ if (installed === undefined) {
809
+ try { installed = require('./fitness-lib').isInstalledCore(); } catch { installed = false; }
810
+ }
811
+ const ship = installed ? `npx bongos ship ${taskId}` : `node scripts/bongos/ship.js ${taskId}`;
812
+ const release = installed ? `npx bongos release ${taskId}` : `node scripts/bongos/release.js ${taskId}`;
813
+ return ['\nWhen you finish:', ` ${ship} --notes "..." --summary "..."`, 'Or to abandon:', ` ${release}`];
814
+ }
815
+
805
816
  if (require.main === module) {
806
817
  main().catch((err) => {
807
818
  console.error('fatal:', err);
@@ -809,4 +820,4 @@ if (require.main === module) {
809
820
  });
810
821
  }
811
822
 
812
- module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills, printRoleLine, worktreeDetectionNote, liveStrandLeases, QUEUE_GATED_EXIT };
823
+ module.exports = { finishHintLines, formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills, printRoleLine, worktreeDetectionNote, liveStrandLeases, QUEUE_GATED_EXIT };
@@ -5,7 +5,8 @@
5
5
  // modules/lifecycle/merge-reach.js): every instance, the hub included, gets a merge
6
6
  // only when a published core release carries it AND that instance's pin moves. The
7
7
  // publish lane cuts a release per merge (ADR 0161) only while the integration lane
8
- // is green; on the hub (cloudbongos.com) the pin move is owner-gated.
8
+ // is green; on the hub (cloudbongos.com) its patch-channel subscription moves the pin
9
+ // on its own (task 1005020), and only a new minor release waits for the owner.
9
10
  // Two blanket claims have been wrong here: "Merging to main deploys nothing" (ADR
10
11
  // 0137, false since ADR 0161) and "the hub redeploys from main on every merge"
11
12
  // (never true — the hub pins a published core). Sessions repeated each to the owner.
@@ -335,8 +336,9 @@ function report(r, { quiet = false } = {}) {
335
336
  }
336
337
  if (r.behind) {
337
338
  say(` ⚠ UNDEPLOYED — live is ${r.live.coreVersion}, main declares ${r.onMain}.`);
338
- say(' A release exists that this instance has not picked up: its pin has not moved. On the hub that move is');
339
- say(' owner-gated; on a hosted project it is its update channel or a core-upgrade (docs/recipes/upgrading-the-core.md).');
339
+ say(' A release exists that this instance has not picked up: its pin has not moved. On the hub its patch-channel');
340
+ say(' sweep moves it on its own (a new minor waits for the owner); on a hosted project it is its update channel or a');
341
+ say(' core-upgrade (docs/recipes/upgrading-the-core.md). A gap that stays means the sweep is held or failing.');
340
342
  }
341
343
  if (r.schemaBehind) {
342
344
  const n = r.live.schemaPending;
@@ -46,12 +46,10 @@ const LEGACY_DIRS = ['otb'];
46
46
  // test that it FIRES rather than one that merely tolerates it.
47
47
  const LEGACY_ENV_PREFIXES = ['CLOUDBONGOS', 'OTB', 'GDS', 'PMS'];
48
48
 
49
- // The core release in which the legacy prefixes above stop being read. Named in
50
- // every deprecation warning so an operator learns the deadline from the warning
51
- // itself rather than from a changelog they will not read. This lands in 1.19.x,
52
- // so 1.21 leaves a full minor of overlap; task 1003706 is the task that both
53
- // removes the fallback and moves this constant's meaning to "already gone".
54
- const LEGACY_ENV_SUNSET_RELEASE = '1.21';
49
+ // The deprecation warning names no removal release (task 1005042 - legacy env
50
+ // warning): it promised 1.21, the retirement (task 1003706 - C3d) was abandoned,
51
+ // and 1.24 still read the old names. Name a real release only in the diff that
52
+ // deletes the fallback.
55
53
 
56
54
  // Lazy require (ADR 0108 §1 made this a two-way edge: branding.js now requires
57
55
  // resolveCoreRoot()/resolveInstanceRoot() from this module, so a top-level
@@ -267,7 +265,7 @@ function warnLegacyEnv({ legacyName, canonicalName, shadowed, differs, log = con
267
265
  : shadowed
268
266
  ? `${canonicalName} is also set and wins`
269
267
  : `rename it to ${canonicalName}`;
270
- log.warn(`[bongos] deprecated env ${legacyName} — ${fate}. The legacy spelling stops being read in core ${LEGACY_ENV_SUNSET_RELEASE}.`);
268
+ log.warn(`[bongos] deprecated env ${legacyName} — ${fate}. The legacy spelling is still read for now, but will stop being read in a future core release.`);
271
269
  return true;
272
270
  }
273
271
 
@@ -282,11 +280,10 @@ function warnPackPrefix({ prefix, name, log = console } = {}) {
282
280
  return true;
283
281
  }
284
282
 
285
- // NOT exported, deliberately: warnLegacyEnv, LEGACY_ENV_SUNSET_RELEASE and the
286
- // ledger are internals. resolveEnv's injectable `warn` is the whole test seam a
283
+ // NOT exported, deliberately: warnLegacyEnv and the ledger are internals. resolveEnv's injectable `warn` is the whole test seam a
287
284
  // caller needs, and a test of the DEFAULT path captures console.warn — which
288
285
  // exercises the real wiring rather than a hand-called helper. Exporting them
289
- // would also add three untraceable entries to the knip dead-code ratchet.
286
+ // would also add untraceable entries to the knip dead-code ratchet.
290
287
 
291
288
  // resolveCoreRoot() / resolveInstanceRoot() — the ADR 0108 §1 configurable-root
292
289
  // resolvers, extending this module's established zero-risk pattern from
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.67'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.69'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -112,6 +112,20 @@ t('the app\'s own status messages never trigger the slop check (task 1005023)',
112
112
  assert.ok(conductor.evaluateTriggers({ prompt: "that's wrong, it's already live" }).some((x) => x.id === 'slop-check'), 'a real correction still fires');
113
113
  });
114
114
 
115
+ t('side-session events, cross-session messages and CI events never trigger the slop check (task 1005022)', () => {
116
+ const words = "It's already live, not true, actually it works, you said it was slop";
117
+ for (const notice of [
118
+ `<side-sessions-event>Session bug-x ended a turn: ${words}</side-sessions-event>`,
119
+ `<cross-session-message from="local_abc">${words}</cross-session-message>`,
120
+ `<teammate-message teammate_id="t1">${words}</teammate-message>`,
121
+ `<ci-monitor-event pr="12">${words}</ci-monitor-event>`,
122
+ ` \n<channel source="discord">${words}</channel>`,
123
+ ]) {
124
+ assert.ok(!conductor.evaluateTriggers({ prompt: notice }).some((x) => x.id === 'slop-check'), notice);
125
+ }
126
+ assert.ok(conductor.evaluateTriggers({ prompt: "<b>that's wrong</b>, it's already live" }).some((x) => x.id === 'slop-check'), 'a person typing a tag still gets checked');
127
+ });
128
+
115
129
  t('ordinary prompts do not trigger the slop check', () => {
116
130
  for (const p of ['add a dark mode toggle', 'run the tests', 'what can I work on', 'make the button wider']) {
117
131
  assert.ok(!conductor.evaluateTriggers({ prompt: p }).some((x) => x.id === 'slop-check'), p);
@@ -0,0 +1,50 @@
1
+ // tests/claim_finish_hint.mjs
2
+ //
3
+ // task 1005113 - claim card finish: the claim card's "When you finish" hint printed
4
+ // `node scripts/bongos/ship.js N` everywhere. In a project that installs the core from npm
5
+ // (toast), that path is not in the project — the CLI ships inside node_modules/@bongos/core
6
+ // and runs as `npx bongos ship N`. finishHintLines() picks the command for where the core
7
+ // is: a core checkout keeps the node path; an installed core gets the npx verbs.
8
+ //
9
+ // Run: node tests/claim_finish_hint.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { createRequire } from 'node:module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const { finishHintLines } = require('../scripts/bongos/claim.js');
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+ function t(name, fn) {
20
+ try { fn(); passed++; console.log(` PASS ${name}`); }
21
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
22
+ }
23
+
24
+ t('installed core: ship + release hints use npx bongos, never a scripts/ path', () => {
25
+ const text = finishHintLines('1005113', { installed: true }).join('\n');
26
+ assert.match(text, /npx bongos ship 1005113 --notes "\.\.\." --summary "\.\.\."/);
27
+ assert.match(text, /npx bongos release 1005113/);
28
+ assert.doesNotMatch(text, /scripts\//);
29
+ });
30
+
31
+ t('core checkout: hints keep the node scripts/bongos path', () => {
32
+ const text = finishHintLines('42', { installed: false }).join('\n');
33
+ assert.match(text, /node scripts\/bongos\/ship\.js 42 --notes/);
34
+ assert.match(text, /node scripts\/bongos\/release\.js 42/);
35
+ assert.doesNotMatch(text, /scripts\/gds\//);
36
+ });
37
+
38
+ t('hint keeps its headings', () => {
39
+ const lines = finishHintLines('7', { installed: true });
40
+ assert.equal(lines[0], '\nWhen you finish:');
41
+ assert.ok(lines.includes('Or to abandon:'));
42
+ });
43
+
44
+ t('defaults to detecting the core here (this checkout is not installed)', () => {
45
+ const text = finishHintLines('9').join('\n');
46
+ assert.match(text, /node scripts\/bongos\/ship\.js 9/);
47
+ });
48
+
49
+ console.log(`\n${passed} passed, ${failed} failed`);
50
+ if (failed) process.exit(1);
@@ -79,7 +79,11 @@ test('the line says who sees a merge, not a blanket rule in either direction', (
79
79
  const s = line(behind);
80
80
  assert.doesNotMatch(s, /deploys nothing/);
81
81
  assert.doesNotMatch(s, /redeploys from main|on every merge|within minutes/);
82
- assert.match(s, /cloudbongos[.]com gets a merge only when a published core release carries it and its pin moves [(]owner-gated[)]/);
82
+ assert.match(s, /cloudbongos[.]com gets a merge only when a published core release carries it and its pin moves/);
83
+ // task 1005020: the hub's pin is NOT owner-gated for a patch release — its patch-channel
84
+ // subscription moved cloudbongos.com to core 1.24.12 with no person (task 1005015).
85
+ assert.doesNotMatch(s, /owner-gated[)]/, 'a patch move on the hub needs no person');
86
+ assert.match(s, /its patch-channel subscription moves that pin on its own/);
83
87
  assert.match(s, /hosted projects: only when a published core release carries the change and their pin moves/);
84
88
  assert.match(s, /Never call work live until that instance's \/version shows it/);
85
89
  });
@@ -2,6 +2,7 @@
2
2
  // Pure: every input is injected, so no env mutation / disk dependence.
3
3
  import assert from 'node:assert/strict';
4
4
  import { test } from 'node:test';
5
+ import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import ic from '../src/instance-config.js';
@@ -108,7 +109,37 @@ test('resolveEnv: ONLY the old spelling set — resolves, and warns naming old,
108
109
  assert.equal(lines.length, 1);
109
110
  assert.match(lines[0], /CLOUDBONGOS_C3A_RENDER_PROBE/, 'names the old spelling');
110
111
  assert.match(lines[0], /BONGOS_C3A_RENDER_PROBE/, 'names the new spelling');
111
- assert.match(lines[0], /stops being read in core 1\.21/, 'names the release it stops working in');
112
+ // Task 1005042 - legacy env warning: the line used to promise "stops being read
113
+ // in core 1.21" long after 1.21 passed with the spelling still read. Whatever it
114
+ // says about removal must be TRUE against the version actually running: it may
115
+ // name a future release, never the current one or a past one.
116
+ assert.deepEqual(stalePromisedReleases(lines[0], runningCore()), [],
117
+ `the warning names a removal release that is not in the future: ${lines[0]}`);
118
+ assert.match(lines[0], /still read/i, 'says plainly that the old spelling still works today');
119
+ assert.match(lines[0], /future core release/i, 'says it will go, without inventing a date nobody has committed to');
120
+ });
121
+
122
+ // Every "core X.Y" a line names that is NOT after the running release — i.e. a
123
+ // removal promise that is already broken, since the running core still reads the
124
+ // spelling. Empty = the line is honest about dates.
125
+ function runningCore() {
126
+ const coreSrc = fs.readFileSync(new URL('../src/module-api.js', import.meta.url), 'utf8');
127
+ return coreSrc.match(/^const CORE_VERSION = '(\d+)\.(\d+)\.\d+'/m).slice(1).map(Number);
128
+ }
129
+ function stalePromisedReleases(line, [runMaj, runMin]) {
130
+ return [...line.matchAll(/core (\d+)\.(\d+)/g)]
131
+ .map((m) => [Number(m[1]), Number(m[2])])
132
+ .filter(([maj, min]) => !(maj > runMaj || (maj === runMaj && min > runMin)))
133
+ .map(([maj, min]) => `${maj}.${min}`);
134
+ }
135
+
136
+ test('stalePromisedReleases: catches the passed-release warning task 1005042 fixed, accepts a future one', () => {
137
+ const old = '[bongos] deprecated env CLOUDBONGOS_X — rename it to BONGOS_X. The legacy spelling stops being read in core 1.21.';
138
+ assert.deepEqual(stalePromisedReleases(old, [1, 24]), ['1.21'], 'the original line names a release already passed');
139
+ assert.deepEqual(stalePromisedReleases('stops being read in core 1.24.', [1, 24]), ['1.24'], 'the running release is not a future one');
140
+ assert.deepEqual(stalePromisedReleases('stops being read in core 1.25.', [1, 24]), [], 'a genuinely future release is honest');
141
+ assert.deepEqual(stalePromisedReleases('stops being read in core 2.0.', [1, 24]), [], 'a future major is honest');
142
+ assert.ok(runningCore()[0] >= 1, 'CORE_VERSION is readable from src/module-api.js');
112
143
  });
113
144
 
114
145
  test('resolveEnv: ONLY the new spelling set — resolves, and is SILENT', () => {
@@ -165,6 +165,13 @@ t('a missing hook is visible (exit 1, one line) and never blocks; the others sti
165
165
  assert.match(r.stderr, /no-such-hook\.js/);
166
166
  const b = together(['no-such-hook.js', 'git-guard.js'], shell('Bash', 'git stash push -m x'));
167
167
  assert.equal(b.status, 2, 'a real block still blocks beside a missing hook');
168
+ assert.match(b.stderr, /no-such-hook\.js/, 'the missing-hook line survives the block (task 1004949)');
169
+ assert.match(b.stderr, /git-guard: blocked/, 'and the block reason is still there');
170
+ });
171
+
172
+ t('merge: a code-1 diagnostic is joined into the block stderr, in roster order (task 1004949)', () => {
173
+ const m = dispatcher.merge([{ code: 1, stderr: 'missing\n' }, { code: 2, stderr: 'blocked\n' }, { code: 0, stdout: '' }]);
174
+ assert.deepEqual(m, { code: 2, stdout: '', stderr: 'missing\nblocked\n' });
168
175
  });
169
176
 
170
177
  t('only plain sibling names load — no paths, not itself', () => {
@@ -8,7 +8,7 @@
8
8
  // exited 2. The `require.main === module` guard is what makes the transform testable.
9
9
  import assert from 'node:assert/strict';
10
10
  import { test } from 'node:test';
11
- import { readFileSync } from 'node:fs';
11
+ import { readFileSync, readdirSync } from 'node:fs';
12
12
  import mirror from '../scripts/status-mirror-sync.js';
13
13
 
14
14
  const { buildMirrorIndex, injectOnce, brandTokens } = mirror;
@@ -120,3 +120,24 @@ test('the publisher source hardcodes no host identity (the ratchet this slice in
120
120
  const hit = stripped.match(/example|example-owner|example world/i);
121
121
  assert.equal(hit, null, `scripts/status-mirror-sync.js hardcodes the host identity "${hit && hit[0]}" — resolve it from branding() instead`);
122
122
  });
123
+
124
+ test('no doc claims a sync workflow runs the mirror publish while none exists (task 1005131)', () => {
125
+ // The publish is run by hand: nothing in .github/workflows/ calls this script. A doc
126
+ // saying "synced by sync-status-mirror.yml" (or calling the sync CI-driven) sends the
127
+ // reader waiting for an automatic update that never comes, so the live page goes stale.
128
+ const wfDir = new URL('../.github/workflows/', import.meta.url);
129
+ const workflows = readdirSync(wfDir).map((f) => readFileSync(new URL(f, wfDir), 'utf8'));
130
+ const automated = workflows.some((w) => /status-mirror-sync\.js/.test(w));
131
+ if (automated) return; // a real workflow now runs it — the claim would be true
132
+ const docs = [
133
+ 'scripts/status-mirror-sync.js',
134
+ 'docs/adr/0029-status-page-outage-resilience.md',
135
+ 'docs/architecture.md',
136
+ 'docs/recipes/ops-gotchas.md',
137
+ ];
138
+ for (const rel of docs) {
139
+ const text = readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8');
140
+ const hit = text.match(/synced by[^\n]{0,40}sync-status-mirror\.yml|(?<!NOT )CI-driven/i);
141
+ assert.equal(hit, null, `${rel} claims an automatic mirror sync ("${hit && hit[0]}"), but no workflow runs scripts/status-mirror-sync.js`);
142
+ }
143
+ });
@@ -27,7 +27,7 @@ const readJson = (rel) => JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8
27
27
 
28
28
  // ── the contract ──────────────────────────────────────────────────────────────
29
29
  test('the platform floors are a pinned list — a floor cannot vanish quietly', () => {
30
- assert.deepEqual([...lib.FLOORS], ['console-errors', 'failed-requests', 'horizontal-overflow', 'text-fit', 'sibling-overlap', 'empty-graphic', 'one-header-main-h1', 'controls-24px', 'top-bar-80px', 'reduced-motion']);
30
+ assert.deepEqual([...lib.FLOORS], ['console-errors', 'failed-requests', 'horizontal-overflow', 'text-fit', 'sibling-overlap', 'empty-graphic', 'covered-content', 'one-header-main-h1', 'controls-24px', 'top-bar-80px', 'reduced-motion']);
31
31
  assert.deepEqual(lib.EXIT, { CLEAN: 0, PROBLEMS: 1, USAGE: 2, NO_BROWSER: 3 });
32
32
  assert.deepEqual(Object.keys(lib.SIZES), ['320', '390', '1024', '1280', '1440']);
33
33
  assert.deepEqual([...lib.DEFAULT_SIZES], [1440, 390, 320], 'render.js renders these unless --sizes names 1280');
@@ -103,6 +103,9 @@ test('auditFloors names each floor a render breaks, and says nothing about a cle
103
103
  // task 1005104: a logo whose referenced SVG failed to parse is a floor problem, not ALL CLEAN
104
104
  assert.deepEqual(lib.auditFloors({ ...clean(), emptyGraphics: [] }, { width: 1440, mode: 'dark' }), []);
105
105
  assert.deepEqual(lib.auditFloors({ ...clean(), emptyGraphics: ['a.brand > svg > use → "/logo.svg#mark" paints nothing'] }, { width: 1440, mode: 'dark' }), ['empty-graphic: a.brand > svg > use → "/logo.svg#mark" paints nothing']);
106
+ // task 1005132: text under a painted veil is a floor problem, not ALL CLEAN
107
+ assert.deepEqual(lib.auditFloors({ ...clean(), covered: [] }, { width: 1440, mode: 'dark' }), []);
108
+ assert.deepEqual(lib.auditFloors({ ...clean(), covered: ['main.spec-main > p.spec-lede "Each shared component" under body::after'] }, { width: 1440, mode: 'dark' }), ['covered-content: main.spec-main > p.spec-lede "Each shared component" under body::after']);
106
109
  });
107
110
 
108
111
  test('expectProblems compares every expectation the vocabulary allows', () => {
@@ -103,3 +103,58 @@ test('the empty-graphic floor reports a <use> or <img> that paints nothing, and
103
103
  const problems = lib.auditFloors(facts, { width: 1280 });
104
104
  assert.ok(problems.some((p) => p.startsWith('empty-graphic: ') && p.includes('#logo-gone')), problems.join('\n'));
105
105
  });
106
+
107
+ // task 1005132: the hall specimen read near-black in dark because style.css's fixed body::after veil painted OVER
108
+ // content that was not lifted above it (task 1005137 fixed the page) — and the render said ALL CLEAN. The
109
+ // covered-content floor reports text a painted layer sits on top of; text lifted above the veil, a transparent
110
+ // stretched-link overlay and the scrim behind an open dialog stay silent.
111
+ const VEIL = `body::after { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; background-image: linear-gradient(rgba(0,0,0,.82), rgba(0,0,0,.93)); }`;
112
+ const covered = async (html) => {
113
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
114
+ const page = await ctx.newPage();
115
+ await page.setContent(html);
116
+ const facts = await page.evaluate(lib.factsInPage, {});
117
+ await ctx.close();
118
+ return facts;
119
+ };
120
+
121
+ test('the covered-content floor reports text a painted veil sits over, and not text lifted above it', { skip: !browser && 'no system Edge or Chrome' }, async () => {
122
+ const facts = await covered(`<!doctype html><html><head><style>${VEIL} .lift { position: relative; z-index: 1; }</style></head><body>
123
+ <header class="lift"><h1>lifted</h1></header><main><p id="under">this paragraph sits under the veil</p></main></body></html>`);
124
+ assert.deepEqual(facts.covered.map((c) => c.split(' "')[0]), ['#under'], facts.covered.join('\n'));
125
+ assert.match(facts.covered[0], /^#under "this paragraph sits under the v.*" under body::after$/);
126
+ assert.ok(lib.auditFloors(facts, { width: 1280 }).some((p) => p.startsWith('covered-content: #under')));
127
+ });
128
+
129
+ test('a transparent stretched link, an open dialog\'s scrim and an off-screen paragraph are not covered content', { skip: !browser && 'no system Edge or Chrome' }, async () => {
130
+ const facts = await covered(`<!doctype html><html><head><style>
131
+ .card { position: relative; width: 300px; } .card a::after { content: ""; position: absolute; inset: 0; }
132
+ .scrim { position: fixed; inset: 0; background: rgba(0,0,0,.6); }
133
+ dialog { z-index: 2; }
134
+ </style></head><body><header></header><main><h1>cards</h1>
135
+ <div class="card"><h2><a href="/x">the card's link</a></h2><p>the card's text under a transparent overlay</p></div>
136
+ <p>a paragraph behind the open dialog's scrim</p>
137
+ <p style="margin-top: 2000px">far below the fold</p>
138
+ <div class="scrim"></div><div role="dialog" aria-modal="true" style="position: fixed; top: 10px; left: 10px; z-index: 3; background: white">the dialog</div>
139
+ </main></body></html>`);
140
+ assert.deepEqual(facts.covered, [], facts.covered.join('\n'));
141
+ });
142
+
143
+ test('a fixed top bar over scrolled text, a glow that fades to transparent and a visually-hidden heading are not covered content', { skip: !browser && 'no system Edge or Chrome' }, async () => {
144
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
145
+ const page = await ctx.newPage();
146
+ await page.setContent(`<!doctype html><html><head><style>
147
+ header { position: fixed; top: 0; left: 0; right: 0; height: 60px; background: #111; z-index: 5; }
148
+ .orb { position: relative; width: 120px; } .orb .disc::before { content: ""; position: absolute; inset: -40px; background-image: radial-gradient(rgba(255,160,80,.6), transparent 70%); }
149
+ .vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
150
+ .sky { position: absolute; inset: 0; background: #000; z-index: 2; }
151
+ </style></head><body><header>the bar</header><main><div class="sky"></div><h1 class="vh">the project map</h1>
152
+ <div class="orb" style="position: relative; z-index: 3; margin-top: 80px"><span class="disc">●</span><div class="nm">Demo world</div></div>
153
+ <div class="orb" style="position: relative; z-index: 3"><span class="shape" style="position: relative; display: block; height: 20px"><i style="position: absolute; left: 0; top: 0; width: 120px; height: 80px; background: #c84; -webkit-mask-image: linear-gradient(black 20px, transparent 20px); mask-image: linear-gradient(black 20px, transparent 20px)"></i></span><div class="nm">a masked shape's name</div></div>
154
+ <p style="position: relative; z-index: 3">a paragraph the page scrolls under the bar</p><div style="height: 2000px"></div>
155
+ </main></body></html>`);
156
+ await page.evaluate(() => window.scrollTo(0, 120));
157
+ const facts = await page.evaluate(lib.factsInPage, {});
158
+ await ctx.close();
159
+ assert.deepEqual(facts.covered, [], facts.covered.join('\n'));
160
+ });