@bongos/core 1.19.827 → 1.19.828

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/gds/package-core.js",
5
- "core_version": "1.19.827",
6
- "core_contract": "1.19.827",
7
- "source_commit": "4fe673594c63823c15cf02108dde24ac25c40757",
5
+ "core_version": "1.19.828",
6
+ "core_contract": "1.19.828",
7
+ "source_commit": "1468eede4d69ff51f17aca93df0be9ce3e7c61d4",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-17T13:57:26.543Z",
9
+ "built_at": "2026-09-17T14:06:18.434Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 491,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2236,
14
+ "functional_verbatim": 2241,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2753,
20
- "tree_sha256": "a39afbe8aa551c281c44ae315b31b984c38436ff1e4f632f3a70c67c30d6bb15",
19
+ "file_count": 2758,
20
+ "tree_sha256": "bf97a7d239bc1b9ea00eef896ec4b06c6c598d7d4db056e25bb24066862bd6a3",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2022,12 +2022,12 @@
2022
2022
  {
2023
2023
  "path": "docs/copy-inventory.md",
2024
2024
  "mode": "0000644",
2025
- "sha256": "1e6864ea2bb93e1fb03ec61bb9f462937ff8d3a73645fcda5a3902b88a4093ae"
2025
+ "sha256": "47070b89dd6a26d425e630db6b592b9b99ce73197e39c21b416caa5a12b330df"
2026
2026
  },
2027
2027
  {
2028
2028
  "path": "docs/copy-registry.json",
2029
2029
  "mode": "0000644",
2030
- "sha256": "153a5874fbcc691ce74495bea2ea5cdc321c4e5d61054788abe531f5eab537d5"
2030
+ "sha256": "cceabb45bbe7b9aabe8484777a8567fc9d73f274486475a80b1c04a43152f7c8"
2031
2031
  },
2032
2032
  {
2033
2033
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2552,7 +2552,7 @@
2552
2552
  {
2553
2553
  "path": "docs/module-api-changelog.md",
2554
2554
  "mode": "0000644",
2555
- "sha256": "dc603258b3556e3044710de8bf32a074d12865b74f0e25b93a9944720771962a"
2555
+ "sha256": "1b25950c5cbc4a0495bc7ae5aa052980032732201e87fd4db19c2d00a0497659"
2556
2556
  },
2557
2557
  {
2558
2558
  "path": "docs/modules-contract.md",
@@ -4669,6 +4669,21 @@
4669
4669
  "mode": "0000644",
4670
4670
  "sha256": "cad620c960fb6bb8533d40a1e1e9b841b998b30ff073dabaca8623ee183d142c"
4671
4671
  },
4672
+ {
4673
+ "path": "modules/hall-ui/public/blockers-lib.js",
4674
+ "mode": "0000644",
4675
+ "sha256": "3780d533a99f3418fe1f1c5486258ee105cdbe7320264bfbc08e3a88bb539150"
4676
+ },
4677
+ {
4678
+ "path": "modules/hall-ui/public/blockers.html",
4679
+ "mode": "0000644",
4680
+ "sha256": "ff854ed6800701f2f6b1fa7e908c858206217d0abd3c0b36bbd9bf37237291a7"
4681
+ },
4682
+ {
4683
+ "path": "modules/hall-ui/public/blockers.js",
4684
+ "mode": "0000644",
4685
+ "sha256": "80a9ec728b27e531199e1ea2fb78ef11292ff8f0387e31e598004a5c394c5474"
4686
+ },
4672
4687
  {
4673
4688
  "path": "modules/hall-ui/public/board-lib.js",
4674
4689
  "mode": "0000644",
@@ -4867,7 +4882,7 @@
4867
4882
  {
4868
4883
  "path": "modules/hall-ui/public/goal-inbox.js",
4869
4884
  "mode": "0000644",
4870
- "sha256": "86aefc97c267f860c931ec000ace7a0a4a4e7e56796891eb5c3fed893446dae5"
4885
+ "sha256": "c636f3f437805582e7b160cbf2e615a09d6c0e1851788de3909bcc24b0b97267"
4871
4886
  },
4872
4887
  {
4873
4888
  "path": "modules/hall-ui/public/goals-lib.js",
@@ -5007,7 +5022,7 @@
5007
5022
  {
5008
5023
  "path": "modules/hall-ui/public/index.html",
5009
5024
  "mode": "0000644",
5010
- "sha256": "60fd4302c50d016f3a77abda01aa26cda31a0d68eb04c47790252d7f584cad5a"
5025
+ "sha256": "e307d3baf7619ab0fc768f5dccfb4a311e97e44845bbcd7072d1b36428d265d0"
5011
5026
  },
5012
5027
  {
5013
5028
  "path": "modules/hall-ui/public/index.states.json",
@@ -5297,7 +5312,7 @@
5297
5312
  {
5298
5313
  "path": "modules/hall-ui/public/shell.js",
5299
5314
  "mode": "0000644",
5300
- "sha256": "7a7973a66ad3fc3b14f9216456aebc0fb13fb29d6d1f5b6c07a1e1f1daa8f623"
5315
+ "sha256": "8c2a64d5f797eba3a6fa12384bd1f3b83455ccebbd54cb5b92104a34c9a48f77"
5301
5316
  },
5302
5317
  {
5303
5318
  "path": "modules/hall-ui/public/ship.wav",
@@ -7822,12 +7837,12 @@
7822
7837
  {
7823
7838
  "path": "package-lock.json",
7824
7839
  "mode": "0000644",
7825
- "sha256": "15fdc8e49c1219b3d4f3ebca13c55e530d02b9ad469163a82749d7999d37314d"
7840
+ "sha256": "233324094e9598241150aa565dbbe7ea26d977714a940b5c4e0d595ab42428da"
7826
7841
  },
7827
7842
  {
7828
7843
  "path": "package.json",
7829
7844
  "mode": "0000644",
7830
- "sha256": "c7e6e06cc67ddfd968ac745b125971e1097dfe56b4e7e428c6331ffdc39de325"
7845
+ "sha256": "6e817e4918c137b1d910514c7155c0a22d7c5558b9ba9934892916df07e90b78"
7831
7846
  },
7832
7847
  {
7833
7848
  "path": "public-docs/index.html",
@@ -9747,7 +9762,7 @@
9747
9762
  {
9748
9763
  "path": "src/module-api.js",
9749
9764
  "mode": "0000644",
9750
- "sha256": "e674350e5eca098fd473e43be69fc725f8ec211cb13d5541bfd6c70c6d5133c3"
9765
+ "sha256": "32383a56130d07528e7c21e2867c7285a18e4bf7d4dc1d120e510b0f76e4db7c"
9751
9766
  },
9752
9767
  {
9753
9768
  "path": "src/module-loader/catalog.js",
@@ -10199,6 +10214,16 @@
10199
10214
  "mode": "0000644",
10200
10215
  "sha256": "3a85b9e9bee4258aa64d26ca025d52c605f5f33938d3fdd5a31c393f1ec3f249"
10201
10216
  },
10217
+ {
10218
+ "path": "tests/blockers_hall.mjs",
10219
+ "mode": "0000644",
10220
+ "sha256": "ee7abc023de47cc73846393e81b0e6726e459dec6d20ba6cfc16a2a9fba145d1"
10221
+ },
10222
+ {
10223
+ "path": "tests/blockers_lib.mjs",
10224
+ "mode": "0000644",
10225
+ "sha256": "2b35127281fd8a932386e406b4dd36c6d95575c2f71853bec02d65c4ff904d00"
10226
+ },
10202
10227
  {
10203
10228
  "path": "tests/bongos_box_connect.mjs",
10204
10229
  "mode": "0000644",
@@ -11507,7 +11532,7 @@
11507
11532
  {
11508
11533
  "path": "tests/hall_nav.mjs",
11509
11534
  "mode": "0000644",
11510
- "sha256": "e3aec810d2b893066d262e74e9779cefee981bad516df4eda7e619b61ba86b24"
11535
+ "sha256": "a7edab5e6185582dbfdc039b2217462319543e400a8fe2f89ac8e9c021963a7c"
11511
11536
  },
11512
11537
  {
11513
11538
  "path": "tests/hall_oversight_world.mjs",
@@ -11517,7 +11542,7 @@
11517
11542
  {
11518
11543
  "path": "tests/hall_page_gate_map.mjs",
11519
11544
  "mode": "0000644",
11520
- "sha256": "e8c7bdaaddc80195da94b41cc40695933e2da6f5a8b54c9f0f04899a20b48114"
11545
+ "sha256": "3330c70544f543705183171ee89a96cb2da0d2653875c9502edd894895d96a8b"
11521
11546
  },
11522
11547
  {
11523
11548
  "path": "tests/hall_palette.mjs",
@@ -4,13 +4,13 @@
4
4
  > Deterministic on a given tree — regenerate and commit rather than hand-editing.
5
5
  > Machine-readable twin: [`copy-registry.json`](copy-registry.json).
6
6
 
7
- **1868 strings** across 3 surfaces · 1622 distinct pieces of copy · 1868 certain, 0 heuristic · 170 shown in more than one place.
7
+ **1874 strings** across 3 surfaces · 1627 distinct pieces of copy · 1874 certain, 0 heuristic · 171 shown in more than one place.
8
8
 
9
9
  ## Surfaces
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 86 | 1378 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 89 | 1384 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
15
15
  | `landing` | `modules/public-landing/public` | 5 | 390 |
16
16
 
@@ -76,9 +76,9 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
76
76
  | `07f422cbdb7e` | 2x | ) and capture-resistant by design. | modules/public-landing/public/projects.html:1014, modules/public-landing/public/projects.html:1041 |
77
77
  | `22825067f4c5` | 2x | 1 builder | modules/public-landing/public/index.html:747, modules/public-landing/public/index.html:752 |
78
78
  | `3e5a62bc4887` | 2x | A GitHub username is needed. | modules/hall-ui/public/hall-render.js:377, modules/hall-ui/public/watch.js:311 |
79
- | … | | _130 more in the JSON registry_ | |
79
+ | … | | _131 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1378 strings
81
+ ## `builders-hall` — 1384 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -203,6 +203,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
203
203
  | `f6bb75b08e98` | certain | Back to {…} | `modules/hall-ui/public/task.js:217` |
204
204
  | `f6bb75b08e98` | certain | Back to {…} | `modules/hall-ui/public/task.js:226` |
205
205
  | `e49da8610a21` | certain | Block lifted. | `modules/hall-ui/public/harbor.js:182` |
206
+ | `cadad7e5dc2b` | certain | Blockers — {{worldName}} | `modules/hall-ui/public/blockers.html:6` |
207
+ | `cadad7e5dc2b` | certain | Blockers — {{worldName}} | `modules/hall-ui/public/blockers.html:6` |
206
208
  | `118026595c16` | certain | Board Room | `modules/hall-ui/public/board-room.html:19` |
207
209
  | `118026595c16` | certain | Board Room | `modules/hall-ui/public/board-room.html:19` |
208
210
  | `118026595c16` | certain | Board Room | `modules/hall-ui/public/board-room.html:31` |
@@ -561,8 +563,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
561
563
  | `7ee76a958445` | certain | Join requests | `modules/hall-ui/public/goals-render.js:1402` |
562
564
  | `b7ab5922126b` | certain | Join-requests to approve | `modules/hall-ui/public/goal-inbox.js:117` |
563
565
  | `d305f0df6f25` | certain | Jump to | `modules/hall-ui/public/palette.js:226` |
564
- | `2bfc5a50ce88` | certain | Jump to a page or record | `modules/hall-ui/public/shell.js:461` |
565
- | `eba610d25523` | certain | Jump to… | `modules/hall-ui/public/shell.js:463` |
566
+ | `2bfc5a50ce88` | certain | Jump to a page or record | `modules/hall-ui/public/shell.js:470` |
567
+ | `eba610d25523` | certain | Jump to… | `modules/hall-ui/public/shell.js:472` |
566
568
  | `bc779353aa0c` | certain | Karma — peer recognition and approvals, a separate counter from {…}. | `modules/hall-ui/public/profile.js:94` |
567
569
  | `de0e1590667f` | certain | Keep all {…} {…} — they are the values the code drops in, and the change cannot land without them. | `modules/hall-ui/public/copy-desk.js:442` |
568
570
  | `32d25ef2182d` | certain | Keep box | `modules/hall-ui/public/box-panel.js:164` |
@@ -639,7 +641,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
639
641
  | `31e91ee6ce03` | certain | Name must be {…} characters or fewer. | `modules/hall-ui/public/settings.js:768` |
640
642
  | `5e8199266a8a` | certain | Name unvouched | `modules/hall-ui/public/watch.js:374` |
641
643
  | `3970d299c95a` | certain | Needs work | `modules/hall-ui/public/studio.js:162` |
642
- | `7def635421ea` | certain | Needs your attention | `modules/hall-ui/public/goal-inbox.js:396` |
644
+ | `7def635421ea` | certain | Needs your attention | `modules/hall-ui/public/goal-inbox.js:463` |
643
645
  | `87f09d983f6c` | certain | New category label | `modules/hall-ui/public/project-settings.js:138` |
644
646
  | `d427d9dab01c` | certain | New claims are paused: {…} confirmed {…} can't land yet, so {…} reward is pending. Rebase and re-ship, or rele | `modules/hall-ui/public/hall-render.js:628` |
645
647
  | `7fcc2583c372` | certain | New custom rank | `modules/hall-ui/public/government.html:79` |
@@ -719,10 +721,11 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
719
721
  | `fa818ee444b4` | certain | Only this goal's owner or a manager (or an Archon) may do this. | `modules/hall-ui/public/goals-render.js:1330` |
720
722
  | `fa818ee444b4` | certain | Only this goal's owner or a manager (or an Archon) may do this. | `modules/hall-ui/public/goals-render.js:1346` |
721
723
  | `544011db314a` | certain | Only this goal's owner or a manager (or an Archon) may invite. | `modules/hall-ui/public/goals-render.js:1278` |
724
+ | `ade0e39c65e9` | certain | Open blockers | `modules/hall-ui/public/blockers.html:34` |
722
725
  | `261d123970e8` | certain | Open goal #{…} | `modules/hall-ui/public/roadmap.js:119` |
723
726
  | `5f3e0b179839` | certain | Open in Sessions | `modules/hall-ui/public/task.js:483` |
724
727
  | `9f55e4674ebb` | certain | Open in the copy desk | `modules/hall-ui/public/studio.js:219` |
725
- | `3366b115217b` | certain | Open navigation | `modules/hall-ui/public/shell.js:452` |
728
+ | `3366b115217b` | certain | Open navigation | `modules/hall-ui/public/shell.js:461` |
726
729
  | `0f0b1e62148d` | certain | Open online environment | `modules/hall-ui/public/box-panel.js:421` |
727
730
  | `855277dff074` | certain | Open task #{…} | `modules/hall-ui/public/goals-render.js:401` |
728
731
  | `855277dff074` | certain | Open task #{…} | `modules/hall-ui/public/work.js:1143` |
@@ -920,12 +923,12 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
920
923
  | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/hall-render.js:275` |
921
924
  | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/hall-render.js:346` |
922
925
  | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/hall-render.js:415` |
923
- | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/shell.js:422` |
924
- | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:440` |
925
- | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:597` |
926
+ | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/shell.js:431` |
927
+ | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:449` |
928
+ | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:606` |
926
929
  | `8ad669ab2565` | certain | Sign out everywhere | `modules/hall-ui/public/settings.html:171` |
927
930
  | `ecc12c1dd4de` | certain | Skill voices | `modules/hall-ui/public/settings.html:377` |
928
- | `87cee78ca134` | certain | Skip to content | `modules/hall-ui/public/shell.js:588` |
931
+ | `87cee78ca134` | certain | Skip to content | `modules/hall-ui/public/shell.js:597` |
929
932
  | `688ca7b6e30f` | certain | Slop — reads as machine-written | `modules/hall-ui/public/copy-desk.html:90` |
930
933
  | `82df09a08476` | certain | So the Archon knows who you are | `modules/hall-ui/public/hall-render.js:294` |
931
934
  | `7da6a848cd26` | certain | Someone helped | `modules/hall-ui/public/collab.js:195` |
@@ -951,7 +954,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
951
954
  | `5831de503015` | certain | Studio — {{worldName}} | `modules/hall-ui/public/studio.html:10` |
952
955
  | `f79e1fb8ebd4` | certain | Suggested as you type; leave it on "no goal" and it waits for triage. | `modules/hall-ui/public/ideas.html:88` |
953
956
  | `a1482eff4ead` | certain | Suggested for you | `modules/hall-ui/public/settings-specialities.js:137` |
954
- | `1e40f33483eb` | certain | Switch theme | `modules/hall-ui/public/shell.js:467` |
957
+ | `1e40f33483eb` | certain | Switch theme | `modules/hall-ui/public/shell.js:476` |
955
958
  | `93fa2e97dd35` | certain | System default for this skill | `modules/hall-ui/public/settings.js:74` |
956
959
  | `4894b1d9b0de` | certain | System oversight: people, operations and security. | `modules/hall-ui/public/watch.js:1370` |
957
960
  | `dfc967a63d57` | certain | Task lifecycle from backlog to shipped, showing states and transitions and the edge where credits are awarded. | `modules/hall-ui/public/diagrams.html:51` |
@@ -969,7 +972,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
969
972
  | `2fc66e722e33` | certain | Tasks open once you have read the builder primer. | `modules/hall-ui/public/work.html:43` |
970
973
  | `44579eef0c8d` | certain | Tasks — {{worldName}} | `modules/hall-ui/public/work.html:6` |
971
974
  | `44579eef0c8d` | certain | Tasks — {{worldName}} | `modules/hall-ui/public/work.html:6` |
972
- | `2f4f047dc1b4` | certain | Text size | `modules/hall-ui/public/shell.js:466` |
975
+ | `2f4f047dc1b4` | certain | Text size | `modules/hall-ui/public/shell.js:475` |
973
976
  | `f420429f942e` | certain | That builder no longer exists. | `modules/hall-ui/public/goals-render.js:1275` |
974
977
  | `545c59c1deb7` | certain | That is the session you are using right now. Ending it signs you out of this page. | `modules/hall-ui/public/settings-sessions.js:74` |
975
978
  | `282c3fa2d456` | certain | That is the wording it already has. Change something, or file a flag instead if you only want to mark it. | `modules/hall-ui/public/copy-desk.js:438` |
@@ -1040,6 +1043,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1040
1043
  | `5151cf635644` | certain | They have asked to connect. | `modules/hall-ui/public/profile-connections.js:203` |
1041
1044
  | `10219b02433e` | certain | They join as a member once they accept — you can change that afterwards. Nobody is added until they do. | `modules/hall-ui/public/goals-render.js:1205` |
1042
1045
  | `ccd0ccb80fae` | certain | They joined this goal already. | `modules/hall-ui/public/goals-render.js:1274` |
1046
+ | `27f77408aed0` | certain | Things stuck on the owner — a credential, a purchase, a product call only they can make. Resolving one un-stic | `modules/hall-ui/public/blockers.html:26` |
1043
1047
  | `6e46811189db` | certain | This PR is held by the gate-review check. | `modules/hall-ui/public/gate.js:129` |
1044
1048
  | `b7c288328f79` | certain | This builder hides their cross-project record. | `modules/hall-ui/public/watch.js:417` |
1045
1049
  | `ed313d96b419` | certain | This builder keeps their track record private. | `modules/hall-ui/public/profile.js:294` |
@@ -1303,6 +1307,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1303
1307
  | `36e3f6ba7bf4` | certain | older code | `modules/hall-ui/public/box-panel.js:202` |
1304
1308
  | `759c12b5f983` | certain | oldest first | `modules/hall-ui/public/hall-widgets.js:134` |
1305
1309
  | `c15a63d53697` | certain | on {…} is asking to sign in as | `modules/hall-ui/public/pair.js:53` |
1310
+ | `cbaf58cdbe35` | certain | open the queue | `modules/hall-ui/public/goal-inbox.js:427` |
1306
1311
  | `af3a8e0fe6a8` | certain | open your goals | `modules/hall-ui/public/goal-inbox.js:121` |
1307
1312
  | `4f279d68175f` | certain | open, on the Security tab | `modules/hall-ui/public/watch.html:47` |
1308
1313
  | `7b85e6494596` | certain | opens a new tab | `modules/hall-ui/public/palette.js:190` |
@@ -1442,6 +1447,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1442
1447
  | `b2a04abfead0` | certain | …{…} more awaiting a nod. | `modules/hall-ui/public/goal-inbox.js:264` |
1443
1448
  | `616fb9adf3cf` | certain | …{…} more going quiet. | `modules/hall-ui/public/goal-inbox.js:353` |
1444
1449
  | `e35f11bdc5d7` | certain | …{…} more open ideas — triage clears them one at a time. | `modules/hall-ui/public/builders.js:857` |
1450
+ | `5f1d914ad45e` | certain | …{…} more — | `modules/hall-ui/public/goal-inbox.js:427` |
1445
1451
  | `de8fa5223aee` | certain | ‹ Prev | `modules/hall-ui/public/hall-kit.js:74` |
1446
1452
  | `d4b84b7fbdeb` | certain | ← All goals | `modules/hall-ui/public/goals.html:80` |
1447
1453
  | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:184` |
@@ -3,17 +3,17 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1868,
7
- "unique_ids": 1622,
8
- "certain": 1868,
6
+ "strings": 1874,
7
+ "unique_ids": 1627,
8
+ "certain": 1874,
9
9
  "heuristic": 0,
10
- "duplicate_ids": 170
10
+ "duplicate_ids": 171
11
11
  },
12
12
  "surfaces": {
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
- "files": 86,
16
- "strings": 1378
15
+ "files": 89,
16
+ "strings": 1384
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -248,7 +248,7 @@
248
248
  "modules/hall-ui/public/hall-render.js:275",
249
249
  "modules/hall-ui/public/hall-render.js:346",
250
250
  "modules/hall-ui/public/hall-render.js:415",
251
- "modules/hall-ui/public/shell.js:422"
251
+ "modules/hall-ui/public/shell.js:431"
252
252
  ]
253
253
  },
254
254
  {
@@ -584,6 +584,16 @@
584
584
  "modules/hall-ui/public/task.js:226"
585
585
  ]
586
586
  },
587
+ {
588
+ "id": "cadad7e5dc2b",
589
+ "surface": "builders-hall",
590
+ "text": "Blockers — {{worldName}}",
591
+ "occurrences": 2,
592
+ "at": [
593
+ "modules/hall-ui/public/blockers.html:6",
594
+ "modules/hall-ui/public/blockers.html:6"
595
+ ]
596
+ },
587
597
  {
588
598
  "id": "2b8ad43383de",
589
599
  "surface": "builders-hall",
@@ -1390,8 +1400,8 @@
1390
1400
  "text": "Sign out",
1391
1401
  "occurrences": 2,
1392
1402
  "at": [
1393
- "modules/hall-ui/public/shell.js:440",
1394
- "modules/hall-ui/public/shell.js:597"
1403
+ "modules/hall-ui/public/shell.js:449",
1404
+ "modules/hall-ui/public/shell.js:606"
1395
1405
  ]
1396
1406
  },
1397
1407
  {
@@ -2905,6 +2915,24 @@
2905
2915
  "origin": "js-notice",
2906
2916
  "confidence": "certain"
2907
2917
  },
2918
+ {
2919
+ "id": "cadad7e5dc2b",
2920
+ "surface": "builders-hall",
2921
+ "text": "Blockers — {{worldName}}",
2922
+ "file": "modules/hall-ui/public/blockers.html",
2923
+ "line": 6,
2924
+ "origin": "html-text",
2925
+ "confidence": "certain"
2926
+ },
2927
+ {
2928
+ "id": "cadad7e5dc2b",
2929
+ "surface": "builders-hall",
2930
+ "text": "Blockers — {{worldName}}",
2931
+ "file": "modules/hall-ui/public/blockers.html",
2932
+ "line": 6,
2933
+ "origin": "html-title",
2934
+ "confidence": "certain"
2935
+ },
2908
2936
  {
2909
2937
  "id": "118026595c16",
2910
2938
  "surface": "builders-hall",
@@ -6132,7 +6160,7 @@
6132
6160
  "surface": "builders-hall",
6133
6161
  "text": "Jump to a page or record",
6134
6162
  "file": "modules/hall-ui/public/shell.js",
6135
- "line": 461,
6163
+ "line": 470,
6136
6164
  "origin": "js-markup",
6137
6165
  "confidence": "certain"
6138
6166
  },
@@ -6141,7 +6169,7 @@
6141
6169
  "surface": "builders-hall",
6142
6170
  "text": "Jump to…",
6143
6171
  "file": "modules/hall-ui/public/shell.js",
6144
- "line": 463,
6172
+ "line": 472,
6145
6173
  "origin": "js-markup",
6146
6174
  "confidence": "certain"
6147
6175
  },
@@ -6834,7 +6862,7 @@
6834
6862
  "surface": "builders-hall",
6835
6863
  "text": "Needs your attention",
6836
6864
  "file": "modules/hall-ui/public/goal-inbox.js",
6837
- "line": 396,
6865
+ "line": 463,
6838
6866
  "origin": "js-markup",
6839
6867
  "confidence": "certain"
6840
6868
  },
@@ -7549,6 +7577,15 @@
7549
7577
  "origin": "js-notice",
7550
7578
  "confidence": "certain"
7551
7579
  },
7580
+ {
7581
+ "id": "ade0e39c65e9",
7582
+ "surface": "builders-hall",
7583
+ "text": "Open blockers",
7584
+ "file": "modules/hall-ui/public/blockers.html",
7585
+ "line": 34,
7586
+ "origin": "html-text",
7587
+ "confidence": "certain"
7588
+ },
7552
7589
  {
7553
7590
  "id": "261d123970e8",
7554
7591
  "surface": "builders-hall",
@@ -7581,7 +7618,7 @@
7581
7618
  "surface": "builders-hall",
7582
7619
  "text": "Open navigation",
7583
7620
  "file": "modules/hall-ui/public/shell.js",
7584
- "line": 452,
7621
+ "line": 461,
7585
7622
  "origin": "js-markup",
7586
7623
  "confidence": "certain"
7587
7624
  },
@@ -9363,7 +9400,7 @@
9363
9400
  "surface": "builders-hall",
9364
9401
  "text": "Sign in with GitHub",
9365
9402
  "file": "modules/hall-ui/public/shell.js",
9366
- "line": 422,
9403
+ "line": 431,
9367
9404
  "origin": "js-markup",
9368
9405
  "confidence": "certain"
9369
9406
  },
@@ -9372,7 +9409,7 @@
9372
9409
  "surface": "builders-hall",
9373
9410
  "text": "Sign out",
9374
9411
  "file": "modules/hall-ui/public/shell.js",
9375
- "line": 440,
9412
+ "line": 449,
9376
9413
  "origin": "js-markup",
9377
9414
  "confidence": "certain"
9378
9415
  },
@@ -9381,7 +9418,7 @@
9381
9418
  "surface": "builders-hall",
9382
9419
  "text": "Sign out",
9383
9420
  "file": "modules/hall-ui/public/shell.js",
9384
- "line": 597,
9421
+ "line": 606,
9385
9422
  "origin": "js-markup",
9386
9423
  "confidence": "certain"
9387
9424
  },
@@ -9408,7 +9445,7 @@
9408
9445
  "surface": "builders-hall",
9409
9446
  "text": "Skip to content",
9410
9447
  "file": "modules/hall-ui/public/shell.js",
9411
- "line": 588,
9448
+ "line": 597,
9412
9449
  "origin": "js-text-assign",
9413
9450
  "confidence": "certain"
9414
9451
  },
@@ -9642,7 +9679,7 @@
9642
9679
  "surface": "builders-hall",
9643
9680
  "text": "Switch theme",
9644
9681
  "file": "modules/hall-ui/public/shell.js",
9645
- "line": 467,
9682
+ "line": 476,
9646
9683
  "origin": "js-markup",
9647
9684
  "confidence": "certain"
9648
9685
  },
@@ -9804,7 +9841,7 @@
9804
9841
  "surface": "builders-hall",
9805
9842
  "text": "Text size",
9806
9843
  "file": "modules/hall-ui/public/shell.js",
9807
- "line": 466,
9844
+ "line": 475,
9808
9845
  "origin": "js-markup",
9809
9846
  "confidence": "certain"
9810
9847
  },
@@ -10438,6 +10475,15 @@
10438
10475
  "origin": "js-notice",
10439
10476
  "confidence": "certain"
10440
10477
  },
10478
+ {
10479
+ "id": "27f77408aed0",
10480
+ "surface": "builders-hall",
10481
+ "text": "Things stuck on the owner — a credential, a purchase, a product call only they can make. Resolving one un-sticks every task waiting on it automatically.",
10482
+ "file": "modules/hall-ui/public/blockers.html",
10483
+ "line": 26,
10484
+ "origin": "html-text",
10485
+ "confidence": "certain"
10486
+ },
10441
10487
  {
10442
10488
  "id": "6e46811189db",
10443
10489
  "surface": "builders-hall",
@@ -12805,6 +12851,15 @@
12805
12851
  "origin": "js-markup",
12806
12852
  "confidence": "certain"
12807
12853
  },
12854
+ {
12855
+ "id": "cbaf58cdbe35",
12856
+ "surface": "builders-hall",
12857
+ "text": "open the queue",
12858
+ "file": "modules/hall-ui/public/goal-inbox.js",
12859
+ "line": 427,
12860
+ "origin": "js-markup",
12861
+ "confidence": "certain"
12862
+ },
12808
12863
  {
12809
12864
  "id": "af3a8e0fe6a8",
12810
12865
  "surface": "builders-hall",
@@ -14056,6 +14111,15 @@
14056
14111
  "origin": "js-markup",
14057
14112
  "confidence": "certain"
14058
14113
  },
14114
+ {
14115
+ "id": "5f1d914ad45e",
14116
+ "surface": "builders-hall",
14117
+ "text": "…{…} more —",
14118
+ "file": "modules/hall-ui/public/goal-inbox.js",
14119
+ "line": 427,
14120
+ "origin": "js-markup",
14121
+ "confidence": "certain"
14122
+ },
14059
14123
  {
14060
14124
  "id": "de8fa5223aee",
14061
14125
  "surface": "builders-hall",
@@ -2128,5 +2128,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2128
2128
  landed since 1.19.825 with no explicit bump. run 35228618989. (task 1002620)
2129
2129
  1.19.827 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2130
2130
  landed since 1.19.826 with no explicit bump. run 35230339528. (task 1002620)
2131
+ 1.19.828 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2132
+ landed since 1.19.827 with no explicit bump. run 35231300810. (task 1002620)
2131
2133
  ---------------------------------------------------------------------------
2132
2134
  ```
@@ -0,0 +1,110 @@
1
+ // blockers-lib.js — the ONE blocker-row renderer (task 1003455, goal 1000089).
2
+ //
3
+ // GET /blockers (modules/ideas/routes/blockers.js) had zero consumers — not the
4
+ // hall, not status-ui — and the only blocker a builder could ever see was
5
+ // GET /blockers/:id, reachable only by typing #/blocker/N into the address bar.
6
+ // Owner decision 2026-08-30: BOTH surfaces, from ONE code path — the open-blocker
7
+ // queue gets a real page (modules/hall-ui/public/blockers.js), and open blockers
8
+ // also become line items in the existing Needs-attention inbox
9
+ // (modules/hall-ui/public/goal-inbox.js), because hall-ui/CLAUDE.md's placement
10
+ // rule says an action awaiting a human is an inbox line, never a new Home card.
11
+ //
12
+ // This file is the shared middle: the goals-lib.js / goalRenderFacts precedent —
13
+ // ONE facts source that both callers render from their own DOM shape, so the two
14
+ // surfaces cannot drift apart the way ~10 hand-rolled pagers did before the kit.
15
+ // The two callers do not share a visual family (the inbox nests inside
16
+ // .ginbox__item, the page's rows are the oversight family's .ov-row), so — unlike
17
+ // goalRenderFacts, whose two callers share one family and one fixed class — the
18
+ // class names are the caller's own, passed in `opts`; only the FACTS (title, the
19
+ // filed date, the note, which tasks it blocks) are shared.
20
+ //
21
+ // Pure: no window.OTB, no DOM, no fetch — Node-require-safe like goals-lib.js /
22
+ // hall-kit.js, so a test can run the real arithmetic instead of agreeing with a
23
+ // copy of it.
24
+ (function (root, factory) {
25
+ var mod = factory();
26
+ if (typeof module !== 'undefined' && module.exports) module.exports = mod;
27
+ else root.OTBBlockersLib = mod;
28
+ }(typeof self !== 'undefined' ? self : this, function () {
29
+ 'use strict';
30
+
31
+ function esc(s) {
32
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
33
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
34
+ });
35
+ }
36
+
37
+ function fmtDate(iso) {
38
+ if (!iso) return '';
39
+ var d = new Date(iso);
40
+ if (isNaN(d.getTime())) return '';
41
+ return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });
42
+ }
43
+
44
+ // A blocker's body_md is the owner's own words, quoted the way goal-inbox.js's
45
+ // invitation/join rows already quote a note (the SAME convention, not a new
46
+ // one). It can run to a paragraph, and the row is a summary, so it is clipped —
47
+ // CSS line-clamp handles visual overflow on both surfaces, this just keeps one
48
+ // enormous blocker from making the row's own text node absurd.
49
+ function noteHtml(bodyMd, noteClass) {
50
+ var t = String(bodyMd || '').trim();
51
+ if (!t) return '';
52
+ var clipped = t.length > 240 ? t.slice(0, 239).replace(/\s+$/, '') + '…' : t;
53
+ return '<p class="' + esc(noteClass) + '">“' + esc(clipped) + '”</p>';
54
+ }
55
+
56
+ // `taskHref(id)` is the caller's own link builder (window.OTBTaskDetail.href on
57
+ // a live page) — kept OUT of this pure module the way hall-kit.js stays
58
+ // date-agnostic: the lib never learns what a task URL looks like.
59
+ function blockingLine(b, taskHref) {
60
+ var ids = Array.isArray(b.blocking_task_ids) ? b.blocking_task_ids : [];
61
+ if (!ids.length) return 'Blocking nothing yet';
62
+ var links = ids.map(function (id) {
63
+ var label = '#' + id;
64
+ return taskHref ? '<a href="' + esc(taskHref(id)) + '">' + esc(label) + '</a>' : esc(label);
65
+ }).join(', ');
66
+ var noun = ids.length === 1 ? 'task' : 'tasks';
67
+ return 'Blocking ' + ids.length + ' ' + noun + ': ' + links;
68
+ }
69
+
70
+ // blockerFacts(b, opts) — the shared render. `opts`:
71
+ // titleClass (default 'ov-row__title')
72
+ // noteClass (default 'ov-row__sub')
73
+ // whenClass (default 'ov-row__when')
74
+ // blockerHref(id) — the title's own link (both surfaces point at the SAME
75
+ // #/blocker/N overlay task-detail.js already renders —
76
+ // the "reachable only by typing the URL" bug this task
77
+ // fixes is exactly that overlay having no door INTO it).
78
+ // taskHref(id) — see blockingLine above.
79
+ // Returns { titleHtml, noteHtml, factsHtml } — fragments, not a wrapped row;
80
+ // each caller nests them in its OWN <li> (goal-inbox.js's .ginbox__item,
81
+ // blockers.js's .ov-row), the goalRenderFacts idiom.
82
+ function blockerFacts(b, opts) {
83
+ opts = opts || {};
84
+ var titleClass = opts.titleClass || 'ov-row__title';
85
+ var noteClass = opts.noteClass || 'ov-row__sub';
86
+ var whenClass = opts.whenClass || 'ov-row__when';
87
+ var blockerHref = opts.blockerHref || null;
88
+ var taskHref = opts.taskHref || null;
89
+
90
+ var titleText = esc(b.title || ('Blocker #' + b.id));
91
+ var titleHtml = blockerHref
92
+ ? '<a class="' + esc(titleClass) + '" href="' + esc(blockerHref(b.id)) + '">' + titleText + '</a>'
93
+ : '<span class="' + esc(titleClass) + '">' + titleText + '</span>';
94
+
95
+ // ONE fact line, `·`-joined — the watch.js convention (its box/report rows
96
+ // join multiple facts into a single .ov-row__when span the same way) rather
97
+ // than several sibling spans, which need a flex-gapped parent to space
98
+ // themselves and the attention-inbox context (.ginbox__sub) is not one.
99
+ var facts = 'Filed ' + esc(fmtDate(b.created_at)) + ' · ' + blockingLine(b, taskHref);
100
+ var factsHtml = '<span class="' + esc(whenClass) + '">' + facts + '</span>';
101
+
102
+ return {
103
+ titleHtml: titleHtml,
104
+ noteHtml: noteHtml(b.body_md, noteClass),
105
+ factsHtml: factsHtml,
106
+ };
107
+ }
108
+
109
+ return { esc: esc, fmtDate: fmtDate, blockingLine: blockingLine, blockerFacts: blockerFacts };
110
+ }));
@@ -0,0 +1,65 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Blockers — {{worldName}}</title>
7
+ <meta name="description" content="What is stuck on the owner right now: open blockers and the work they hold up.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
+ <!-- task 1003455: the row shape (title + facts) is the oversight family's,
13
+ the same archetype collab/copy-desk/watch/gate already ride. -->
14
+ <link rel="stylesheet" href="/builders/oversight.css">
15
+ </head>
16
+ <body data-page="blockers" data-page-title="Blockers">
17
+
18
+ <div class="app">
19
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
20
+ <div class="app-scrim" id="app-scrim" hidden></div>
21
+ <div class="app-body">
22
+ <header class="app-topbar" id="app-topbar"></header>
23
+ <div class="app-content">
24
+ <div class="page-head page-head--board">
25
+ <h1 class="page-head__h">Blockers</h1>
26
+ <p class="page-head__sub" id="blockers-sub">Things stuck on the owner — a credential, a purchase, a product call only they can make. Resolving one un-sticks every task waiting on it automatically.</p>
27
+ </div>
28
+
29
+ <main id="hall-main">
30
+ <!-- task 1003455 (goal 1000089): the queue GET /blockers had no page for
31
+ — the only door in used to be typing #/blocker/N into the address
32
+ bar. This is the read half; filing/resolving/linking is task 1003456. -->
33
+ <section class="scroll" id="blockers-scroll" aria-labelledby="blockers-h">
34
+ <h2 id="blockers-h" class="scroll__h">Open blockers<span class="count-pill" id="blockers-count" hidden></span></h2>
35
+ <div id="blockers-body">
36
+ <p class="profile__placeholder">Loading…</p>
37
+ </div>
38
+ </section>
39
+ </main>
40
+
41
+ <footer class="app-foot" id="app-foot"></footer>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
47
+
48
+ <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
49
+ chrome exists before the page script runs. -->
50
+ <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
51
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
52
+ <script src="/builders/palette.js" defer></script>
53
+ <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
54
+ branding pack's currency label, loaded BEFORE blockers.js. -->
55
+ <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
56
+ <script src="/builders/brand-apply.js"></script>
57
+ <script src="/builders/hall-kit.js"></script>
58
+ <!-- task-detail.js still owns the #/blocker/N deep-link overlay every row's
59
+ title opens into — this page needs its link-builder (OTBTaskDetail.href)
60
+ for the "blocking N tasks" facts line, but not the overlay's own render. -->
61
+ <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
62
+ <script src="/builders/blockers-lib.js"></script>
63
+ <script src="/builders/blockers.js"></script>
64
+ </body>
65
+ </html>
@@ -0,0 +1,88 @@
1
+ // blockers.js — the Blockers page: the full open-blocker queue (task 1003455,
2
+ // goal 1000089).
3
+ //
4
+ // GET /blockers (modules/ideas/routes/blockers.js) is PUBLIC and had ZERO hall
5
+ // consumers before this task — the only blocker a builder could ever see was the
6
+ // #/blocker/N overlay task-detail.js already renders, reachable only by typing
7
+ // the id into the address bar. Owner decision 2026-08-30: BOTH surfaces, from
8
+ // ONE code path — this page is the full queue (a long list has nowhere else to
9
+ // live), and goal-inbox.js's attention-inbox slice is the capped summary. Both
10
+ // render from blockers-lib.js's blockerFacts(), so the two cannot drift apart
11
+ // the way ~10 hand-rolled pagers did before the kit (hall-ui/CLAUDE.md).
12
+ //
13
+ // Read-only: filing, resolving and linking a blocker to a task are task 1003456.
14
+ // Deliberately UNGATED (tests/hall_page_gate_map.mjs) — its one data source is
15
+ // public, the roster/drachmae precedent: a page must not be gated tighter than
16
+ // the API it fronts.
17
+ (function () {
18
+ 'use strict';
19
+ var kit = window.OTBKit;
20
+ var lib = window.OTBBlockersLib;
21
+ var API = '/api/bongos';
22
+ var body = document.getElementById('blockers-body');
23
+ var countPill = document.getElementById('blockers-count');
24
+ var sub = document.getElementById('blockers-sub');
25
+
26
+ var taskHref = function (id) {
27
+ return window.OTBTaskDetail ? window.OTBTaskDetail.href(id) : ('/task/' + encodeURIComponent(id));
28
+ };
29
+ var blockerHref = function (id) { return '#/blocker/' + encodeURIComponent(id); };
30
+
31
+ var all = [];
32
+
33
+ function rowHtml(b) {
34
+ var facts = lib.blockerFacts(b, {
35
+ titleClass: 'ov-row__title',
36
+ noteClass: 'ov-row__sub',
37
+ whenClass: 'ov-row__when',
38
+ blockerHref: blockerHref,
39
+ taskHref: taskHref,
40
+ });
41
+ return '<li class="ov-row" data-id="' + lib.esc(String(b.id)) + '">'
42
+ + '<div class="ov-row__main">'
43
+ + facts.titleHtml
44
+ + facts.noteHtml
45
+ + '<div class="ov-row__facts">' + facts.factsHtml + '</div>'
46
+ + '</div>'
47
+ + '</li>';
48
+ }
49
+
50
+ var pager = kit.makePager('blockers', 'hall-blockers-per-page', { defaultPerPage: 25 });
51
+ var listEl = null;
52
+
53
+ function rerender() {
54
+ var slice = kit.paginate(all, pager);
55
+ listEl.innerHTML = slice.length
56
+ ? slice.map(rowHtml).join('')
57
+ : kit.emptyStateHtml('Nothing is stuck on the owner right now.');
58
+ }
59
+
60
+ function mountBlockers(rows) {
61
+ all = Array.isArray(rows) ? rows : [];
62
+ if (countPill) { countPill.textContent = String(all.length); countPill.hidden = all.length === 0; }
63
+ if (sub) {
64
+ sub.textContent = all.length
65
+ ? (all.length === 1 ? 'One thing' : all.length + ' things') + ' stuck on the owner — a credential, a '
66
+ + 'purchase, a product call only they can make. Resolving one un-sticks every task waiting on it automatically.'
67
+ : 'Nothing is stuck on the owner right now.';
68
+ }
69
+ body.innerHTML = '<ul class="ov-rows" id="blockers-list"></ul>' + kit.pagerBarHtml(pager.prefix, { label: 'blockers' });
70
+ listEl = document.getElementById('blockers-list');
71
+ kit.wirePager(pager, rerender);
72
+ rerender();
73
+ }
74
+
75
+ function notice(msg) {
76
+ body.innerHTML = '';
77
+ body.className = 'note';
78
+ body.textContent = msg;
79
+ }
80
+
81
+ fetch(API + '/blockers?limit=200', { headers: { Accept: 'application/json' } })
82
+ .then(function (r) {
83
+ if (!r.ok) throw new Error('http ' + r.status);
84
+ return r.json();
85
+ })
86
+ .then(function (data) { mountBlockers(data && data.blockers); })
87
+ .catch(function () { notice('Couldn’t load the blocker queue right now.'); });
88
+ }());
@@ -384,6 +384,73 @@
384
384
  });
385
385
  }
386
386
 
387
+ // ---- open blockers (task 1003455, goal 1000089) ---------------------------
388
+ //
389
+ // GET /blockers is public and had ZERO hall consumers — the only blocker a
390
+ // builder could ever see was the #/blocker/N overlay task-detail.js already
391
+ // renders, reachable only by typing the id into the address bar. This slice
392
+ // + the /blockers page (blockers.js) are ONE code path: both render from
393
+ // blockers-lib.js's blockerFacts(), loaded before this file (index.html).
394
+ const MAX_BLOCKER_ROWS = 5;
395
+
396
+ function blockerRowHtml(b) {
397
+ const lib = window.OTBBlockersLib;
398
+ const facts = lib.blockerFacts(b, {
399
+ titleClass: 'ginbox__goal',
400
+ noteClass: 'ginbox__note',
401
+ whenClass: 'ginbox__sub',
402
+ // The SAME overlay task-detail.js has rendered since task 1003066's
403
+ // predecessor — this slice is what gives it its first door in.
404
+ blockerHref: (id) => `#/blocker/${encodeURIComponent(id)}`,
405
+ taskHref: (id) => (window.OTBTaskDetail ? window.OTBTaskDetail.href(id) : `/task/${encodeURIComponent(id)}`),
406
+ });
407
+ return `<li class="ginbox__item">
408
+ <div class="ginbox__body">
409
+ ${facts.titleHtml}
410
+ ${facts.factsHtml}
411
+ ${facts.noteHtml}
412
+ </div>
413
+ </li>`;
414
+ }
415
+
416
+ function renderBlockers(data) {
417
+ const box = host && host.querySelector('#attention-blockers');
418
+ if (!box) return;
419
+ const rows = (data && Array.isArray(data.blockers)) ? data.blockers : [];
420
+ if (!rows.length) {
421
+ box.innerHTML = '';
422
+ window.OTBAttention?.refresh();
423
+ return;
424
+ }
425
+ const total = (data && data.page && Number.isFinite(data.page.total)) ? data.page.total : rows.length;
426
+ const more = total > rows.length
427
+ ? `<p class="ginbox__pending">…${total - rows.length} more — <a href="/blockers">open the queue</a>.</p>`
428
+ : '';
429
+ box.innerHTML = '<h3 class="ginbox__sec-h">Stuck on the owner '
430
+ + `<span class="count-pill">${total}</span></h3>`
431
+ + `<ul class="ginbox">${rows.map(blockerRowHtml).join('')}</ul>${more}`;
432
+ window.OTBAttention?.refresh();
433
+ }
434
+
435
+ // task 1003448's "Home bounded reads" rule: ask the server for exactly the
436
+ // rows the card will show (limit=MAX_BLOCKER_ROWS) and read `page.total` back,
437
+ // rather than fetching every open blocker and slicing it client-side — the
438
+ // Next-up / triage-card bug this file's own CLAUDE.md names as the shape to
439
+ // avoid.
440
+ function loadBlockers() {
441
+ return getJSON(`${API}/blockers?limit=${MAX_BLOCKER_ROWS}`)
442
+ .then((data) => renderBlockers(data))
443
+ .catch((err) => {
444
+ const box = host && host.querySelector('#attention-blockers');
445
+ if (box) box.innerHTML = '';
446
+ window.OTBAttention?.refresh();
447
+ if (err.code !== 'auth-needed' && err.code !== 'not-found') {
448
+ console.warn('[hall] blockers feed unavailable — the "Stuck on the owner" section is empty '
449
+ + 'because the request failed, NOT because nothing is open:', err);
450
+ }
451
+ });
452
+ }
453
+
387
454
  // task 2169: this widget IS the "Needs your attention" card — ONE capped
388
455
  // summary hosting every actionable item class: goal invitations +
389
456
  // join-requests (this file) and ideas awaiting triage (builders.js renders
@@ -417,6 +484,12 @@
417
484
  // triage. Its rows carry the shared .ginbox__item class, so refresh()
418
485
  // counts them with no change to the selector below.
419
486
  '<div id="attention-nod"></div>' +
487
+ // task 1003455 (goal 1000089): open blockers — things stuck on the owner
488
+ // (modules/ideas/blockers.js). AFTER nod, BEFORE rot: like every slice
489
+ // above it, a blocker is genuinely waiting on a human, but unlike them it
490
+ // is project-wide rather than addressed to this viewer, so it sits with
491
+ // rot's standing-question tail rather than the personal asks at the top.
492
+ '<div id="attention-blockers"></div>' +
420
493
  // task 1003279 (goal 1000072): work going quiet — a goal or task untouched
421
494
  // past the project's rot timer, with the owner's four verbs on it. LAST in
422
495
  // the card on purpose: every slice above is somebody waiting on YOU right
@@ -426,6 +499,7 @@
426
499
  host.addEventListener('click', onClick);
427
500
  load();
428
501
  loadNodQueue();
502
+ loadBlockers();
429
503
  loadRot();
430
504
  // task 1002802 (P06): the profile slice lives in its own file (builders.js is
431
505
  // over the size budget); this card owns its div, so it boots it here.
@@ -121,6 +121,10 @@
121
121
  <script src="/builders/hall-widgets.js?v=2026-06-27-r75" defer></script>
122
122
  <!-- BV1.R78 (task 1694): the Goals widget registers into the same registry. -->
123
123
  <script src="/builders/goals.js?v=2026-07-01-goals" defer></script>
124
+ <!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
125
+ both goal-inbox.js's attention-inbox slice below and the /blockers page.
126
+ Loaded BEFORE goal-inbox.js, which calls it while rendering. -->
127
+ <script src="/builders/blockers-lib.js" defer></script>
124
128
  <!-- ADR 0112 (task 1954): the home-page goal-membership inbox — invitations to
125
129
  accept + join-requests to approve. Self-registers; hidden when empty. -->
126
130
  <script src="/builders/goal-inbox.js?v=2026-07-10-goal-inbox" defer></script>
@@ -165,6 +165,15 @@
165
165
  // active-nav detection (#nav-work), the 'work' gate case, and the server
166
166
  // page-gate all keep working — only the visible label and order change.
167
167
  { id: 'work', label: 'Tasks', icon: 'work', href: '/work', gate: 'signed' },
168
+ // task 1003455 (goal 1000089): the open-blocker queue — what's stuck on the
169
+ // owner. Lives with the project's own state (Roadmap/Goals/Tasks), not You
170
+ // or Community, because its subject is the project, not a person. Its one
171
+ // data source (GET /blockers) is deliberately public, like Roadmap's — but
172
+ // the NAV FLAG stays 'signed' on the roster/drachmae precedent: an ungated
173
+ // page can still gate cosmetically wider in the sidebar than the API it
174
+ // fronts, showing the link to signed-in builders while a stranger who
175
+ // already has the URL can still open it.
176
+ { id: 'blockers', label: 'Blockers', icon: 'admin', href: '/blockers', gate: 'signed' },
168
177
  // task 1003744 (goal 1000111): the Board Room lives HERE, in the project's
169
178
  // own group, and gates on the atom its page gates on.
170
179
  //
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.827",
3
+ "version": "1.19.828",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.827",
9
+ "version": "1.19.828",
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.827",
3
+ "version": "1.19.828",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.827'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.828'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,107 @@
1
+ // tests/blockers_hall.mjs — task 1003455 (goal 1000089): "Blockers become
2
+ // visible: the queue page and the attention slice, from one code path."
3
+ //
4
+ // GET /blockers had zero hall consumers before this task. Done-when: the queue
5
+ // renders on a real page from GET /blockers, the attention inbox shows open
6
+ // blockers as line items, BOTH read the same renderer (blockers-lib.js, pinned
7
+ // separately in tests/blockers_lib.mjs), and the page's list is paged through
8
+ // OTBKit rather than hand-rolled. This file pins the WIRING — that both
9
+ // consumers actually call the shared lib and the real API, and that the page
10
+ // uses the kit's pager rather than reimplementing slicing.
11
+ //
12
+ // Source-regex style, matching tests/rot_inbox.mjs's checks on the same file —
13
+ // pure text assertions over the real client source, no DOM/vm harness needed for
14
+ // what this pins (presence + call shape, not runtime behaviour).
15
+ //
16
+ // Run: node tests/blockers_hall.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { test } from 'node:test';
20
+ import { readFileSync } from 'node:fs';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { dirname, join } from 'node:path';
23
+
24
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const HALL = join(ROOT, 'modules/hall-ui/public');
26
+ const GOAL_INBOX_SRC = readFileSync(join(HALL, 'goal-inbox.js'), 'utf8');
27
+ const BLOCKERS_JS_SRC = readFileSync(join(HALL, 'blockers.js'), 'utf8');
28
+ const BLOCKERS_HTML_SRC = readFileSync(join(HALL, 'blockers.html'), 'utf8');
29
+ const INDEX_HTML_SRC = readFileSync(join(HALL, 'index.html'), 'utf8');
30
+
31
+ // ---- the attention-inbox slice (goal-inbox.js) -----------------------------
32
+
33
+ test('goal-inbox.js carries a blockers slice div in the SAME needs-attention card', () => {
34
+ assert.match(GOAL_INBOX_SRC, /<div id="attention-blockers"><\/div>/,
35
+ 'a second Home card would violate the placement rule (hall-ui/CLAUDE.md): an action awaiting a human is an inbox line, never a new card');
36
+ });
37
+
38
+ test('goal-inbox.js reads the real GET /blockers route, not a stand-in', () => {
39
+ assert.match(GOAL_INBOX_SRC, /getJSON\(`\$\{API\}\/blockers/);
40
+ });
41
+
42
+ test('goal-inbox.js asks the server for a BOUNDED read and reports the total, not fetch-all-then-slice', () => {
43
+ // The "Home bounded reads" rule this module's own CLAUDE.md names: a capped
44
+ // Home card must ask for exactly what it shows and get a total back, never
45
+ // download the whole feed and .slice() it client-side.
46
+ assert.match(GOAL_INBOX_SRC, /\/blockers\?limit=\$\{MAX_BLOCKER_ROWS\}/);
47
+ assert.match(GOAL_INBOX_SRC, /data\.page\.total/);
48
+ });
49
+
50
+ test('goal-inbox.js renders blocker rows through the SHARED lib, not a re-implementation', () => {
51
+ assert.match(GOAL_INBOX_SRC, /window\.OTBBlockersLib/);
52
+ assert.match(GOAL_INBOX_SRC, /lib\.blockerFacts\(/);
53
+ });
54
+
55
+ test('the blocker row carries .ginbox__item, so the card\'s count-pill and visibility include it', () => {
56
+ assert.match(GOAL_INBOX_SRC, /function blockerRowHtml[\s\S]*?class="ginbox__item"/);
57
+ });
58
+
59
+ test('the mount() boot sequence actually calls loadBlockers()', () => {
60
+ assert.match(GOAL_INBOX_SRC, /loadNodQueue\(\);\s*\n\s*loadBlockers\(\);\s*\n\s*loadRot\(\);/);
61
+ });
62
+
63
+ // ---- the full queue page (blockers.html + blockers.js) ---------------------
64
+
65
+ test('blockers.js reads the real GET /blockers route, not a stand-in', () => {
66
+ assert.match(BLOCKERS_JS_SRC, /API \+ '\/blockers/);
67
+ });
68
+
69
+ test('blockers.js renders rows through the SAME shared lib the attention slice uses', () => {
70
+ assert.match(BLOCKERS_JS_SRC, /window\.OTBBlockersLib/);
71
+ assert.match(BLOCKERS_JS_SRC, /lib\.blockerFacts\(/);
72
+ });
73
+
74
+ test('blockers.js pages through OTBKit — makePager/paginate/wirePager/pagerBarHtml — not by hand', () => {
75
+ assert.match(BLOCKERS_JS_SRC, /kit\.makePager\(/);
76
+ assert.match(BLOCKERS_JS_SRC, /kit\.paginate\(/);
77
+ assert.match(BLOCKERS_JS_SRC, /kit\.wirePager\(/);
78
+ assert.match(BLOCKERS_JS_SRC, /kit\.pagerBarHtml\(/);
79
+ // The negative half: no hand-rolled slice math (start/end arithmetic) sitting
80
+ // beside the kit call, which is what "paged through OTBKit rather than
81
+ // hand-rolled" would otherwise silently tolerate.
82
+ assert.doesNotMatch(BLOCKERS_JS_SRC, /\.slice\(\s*\(?\s*pager\.page/);
83
+ });
84
+
85
+ test('blockers.html loads blockers-lib.js BEFORE blockers.js', () => {
86
+ const libAt = BLOCKERS_HTML_SRC.indexOf('src="/builders/blockers-lib.js"');
87
+ const pageAt = BLOCKERS_HTML_SRC.indexOf('src="/builders/blockers.js"');
88
+ assert.ok(libAt > -1 && pageAt > -1, 'both scripts must be present');
89
+ assert.ok(libAt < pageAt, 'blockers.js calls window.OTBBlockersLib at load time');
90
+ });
91
+
92
+ test('index.html loads blockers-lib.js BEFORE goal-inbox.js', () => {
93
+ // The literal <script src> attribute, not a bare filename mention — a
94
+ // surrounding comment is free to name either file in either order (and does:
95
+ // profile-nudge.js's own comment mentions "goal-inbox.js" ahead of the real
96
+ // goal-inbox.js <script> tag), so only the src strings say what actually loads
97
+ // first.
98
+ const libAt = INDEX_HTML_SRC.indexOf('src="/builders/blockers-lib.js"');
99
+ const inboxAt = INDEX_HTML_SRC.indexOf('src="/builders/goal-inbox.js');
100
+ assert.ok(libAt > -1 && inboxAt > -1, 'both scripts must be present');
101
+ assert.ok(libAt < inboxAt);
102
+ });
103
+
104
+ test('every title link opens the SAME #/blocker/N overlay task-detail.js already renders', () => {
105
+ assert.match(GOAL_INBOX_SRC, /`#\/blocker\/\$\{encodeURIComponent\(id\)\}`/);
106
+ assert.match(BLOCKERS_JS_SRC, /'#\/blocker\/' \+ encodeURIComponent\(id\)/);
107
+ });
@@ -0,0 +1,102 @@
1
+ // tests/blockers_lib.mjs — the shared blocker-row renderer (blockers-lib.js,
2
+ // task 1003455, goal 1000089).
3
+ //
4
+ // Pins that the lib is genuinely pure (no window.OTB, no DOM) so it loads
5
+ // standalone via require() the way goals-lib.js loads standalone in a vm — and
6
+ // that its output is what both callers (goal-inbox.js's attention-inbox slice,
7
+ // blockers.js's full-queue page) actually depend on: a title link into the
8
+ // EXISTING #/blocker/N overlay (task-detail.js), a quoted note, and a `·`-joined
9
+ // facts line naming which tasks a blocker holds up.
10
+ //
11
+ // Run: node tests/blockers_lib.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { test } from 'node:test';
15
+ import { createRequire } from 'node:module';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const lib = require('../modules/hall-ui/public/blockers-lib.js');
19
+
20
+ test('loads standalone via require() — no window, no DOM (Node-require-safe like hall-kit.js)', () => {
21
+ assert.ok(lib, 'module.exports must be set');
22
+ for (const k of ['esc', 'fmtDate', 'blockingLine', 'blockerFacts']) {
23
+ assert.ok(k in lib, `missing export: ${k}`);
24
+ }
25
+ });
26
+
27
+ test('blockerFacts: title links into the SAME #/blocker/N overlay task-detail.js renders', () => {
28
+ const b = { id: 42, title: 'Need the owner’s AWS card on file', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [] };
29
+ const { titleHtml } = lib.blockerFacts(b, { blockerHref: (id) => `#/blocker/${id}` });
30
+ assert.match(titleHtml, /href="#\/blocker\/42"/);
31
+ assert.match(titleHtml, /Need the owner/);
32
+ });
33
+
34
+ test('blockerFacts: with no blockerHref, the title renders as plain text, not a dead link', () => {
35
+ const b = { id: 42, title: 'Something stuck', created_at: null, blocking_task_ids: [] };
36
+ const { titleHtml } = lib.blockerFacts(b);
37
+ assert.doesNotMatch(titleHtml, /<a /);
38
+ assert.match(titleHtml, /<span/);
39
+ });
40
+
41
+ test('blockerFacts: the caller-supplied classes land on the right fragments', () => {
42
+ const b = { id: 1, title: 'x', body_md: 'a reason', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [] };
43
+ const { titleHtml, noteHtml, factsHtml } = lib.blockerFacts(b, {
44
+ titleClass: 'ginbox__goal', noteClass: 'ginbox__note', whenClass: 'ginbox__sub',
45
+ });
46
+ assert.match(titleHtml, /class="ginbox__goal"/);
47
+ assert.match(noteHtml, /class="ginbox__note"/);
48
+ assert.match(factsHtml, /class="ginbox__sub"/);
49
+ });
50
+
51
+ test('blockingLine: no linked tasks reads honestly, not as a false "0 tasks"', () => {
52
+ assert.equal(lib.blockingLine({ blocking_task_ids: [] }, null), 'Blocking nothing yet');
53
+ assert.equal(lib.blockingLine({}, null), 'Blocking nothing yet');
54
+ });
55
+
56
+ test('blockingLine: singular/plural noun agreement, and every id becomes a link when taskHref is given', () => {
57
+ const one = lib.blockingLine({ blocking_task_ids: [5] }, (id) => `/task/${id}`);
58
+ assert.equal(one, 'Blocking 1 task: <a href="/task/5">#5</a>');
59
+ const many = lib.blockingLine({ blocking_task_ids: [5, 6] }, (id) => `/task/${id}`);
60
+ assert.equal(many, 'Blocking 2 tasks: <a href="/task/5">#5</a>, <a href="/task/6">#6</a>');
61
+ });
62
+
63
+ test('blockingLine: with no taskHref the ids still render, as plain text — never a dead link', () => {
64
+ const line = lib.blockingLine({ blocking_task_ids: [5] }, null);
65
+ assert.equal(line, 'Blocking 1 task: #5');
66
+ assert.doesNotMatch(line, /<a /);
67
+ });
68
+
69
+ test('blockerFacts: the facts line is ONE `·`-joined span, not several ungapped siblings', () => {
70
+ // The bug this shape avoids: two adjacent .ginbox__sub spans have no flex gap
71
+ // between them (unlike .ov-row__facts, which is a flex container) and would
72
+ // render Filed-date and Blocking-line running together with no separator.
73
+ const b = { id: 1, title: 'x', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [7] };
74
+ const { factsHtml } = lib.blockerFacts(b, { taskHref: (id) => `/task/${id}` });
75
+ assert.equal((factsHtml.match(/<span/g) || []).length, 1, 'exactly one wrapping span');
76
+ assert.match(factsHtml, /Filed .* · Blocking 1 task/);
77
+ });
78
+
79
+ test('blockerFacts: body_md becomes a quoted note; an empty one renders nothing', () => {
80
+ const withNote = lib.blockerFacts({ id: 1, title: 'x', body_md: 'reason', blocking_task_ids: [] });
81
+ assert.match(withNote.noteHtml, /<p class="ov-row__sub">“reason”<\/p>/);
82
+ const noNote = lib.blockerFacts({ id: 1, title: 'x', body_md: ' ', blocking_task_ids: [] });
83
+ assert.equal(noNote.noteHtml, '');
84
+ });
85
+
86
+ test('blockerFacts: a very long body_md is clipped, so one enormous blocker cannot blow up the row', () => {
87
+ const long = 'x'.repeat(500);
88
+ const { noteHtml } = lib.blockerFacts({ id: 1, title: 't', body_md: long, blocking_task_ids: [] });
89
+ assert.ok(noteHtml.length < 500, 'clipped well below the source length');
90
+ assert.match(noteHtml, /…”<\/p>$/);
91
+ });
92
+
93
+ test('blockerFacts: HTML in a title/body/task id is escaped, not injected', () => {
94
+ const b = {
95
+ id: 1, title: '<script>alert(1)</script>', body_md: '<img onerror=alert(1)>',
96
+ created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [1],
97
+ };
98
+ const { titleHtml, noteHtml, factsHtml } = lib.blockerFacts(b, { taskHref: (id) => `/task/"><script>${id}` });
99
+ assert.doesNotMatch(titleHtml, /<script>/);
100
+ assert.doesNotMatch(noteHtml, /<img /);
101
+ assert.doesNotMatch(factsHtml, /<script>/);
102
+ });
@@ -255,7 +255,10 @@ test('sidebar groups read by audience, in order', () => {
255
255
  // (2026-09-09) was to move it: a sitting is work the project does, beside Goals
256
256
  // and Tasks, not one of Government's monitoring surfaces. The item is invisible
257
257
  // to anyone without the atom, so most builders never see it here.
258
- assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'board-room', 'ideas', 'copy-desk', 'studio'],
258
+ // task 1003455 (goal 1000089): 'blockers' joins the same group, after Tasks —
259
+ // its subject is the project (what's stuck on the owner), not a person or a
260
+ // craft, and its one data source (GET /blockers) is public like Roadmap's.
261
+ assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'blockers', 'board-room', 'ideas', 'copy-desk', 'studio'],
259
262
  'the unlabeled group is the project itself (task 1002425 order)');
260
263
  assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
261
264
  // People (task 1002577): the /scouting directory graduated into the hall.
@@ -129,6 +129,14 @@ const ORACLE = {
129
129
  '/builders/drachmae': null,
130
130
  '/builders/profile': null,
131
131
 
132
+ // task 1003455: the blocker queue. GET /blockers is deliberately public (the
133
+ // status dashboard is meant to read it too — "anyone landing on /builders or
134
+ // the status dashboard can see what's stuck on the owner", the route's own
135
+ // handler comment), so the page must not be gated tighter than the API it
136
+ // fronts — the same rule that puts /roster and /drachmae here. The nav link
137
+ // is still cosmetically 'signed', same as those two.
138
+ '/builders/blockers': null,
139
+
132
140
  // The bare detail slugs, gated by task 1003629 — the decision idea 1000907
133
141
  // (task 1003448) asked for. They resolve to the same task.html / idea.html
134
142
  // shells their /:id forms do, so they now sit on the same tier: the id-less