@bongos/core 1.19.832 → 1.19.834

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.832",
6
- "core_contract": "1.19.832",
7
- "source_commit": "c8e1d760c29ed41a5d66af6b1c4f9936a7fd0f32",
5
+ "core_version": "1.19.834",
6
+ "core_contract": "1.19.834",
7
+ "source_commit": "c7f5d1e6cbf225b7ce9e59bf6f3154125ab3f15b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-17T15:29:22.894Z",
9
+ "built_at": "2026-09-17T16:08:57.677Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 492,
12
+ "docs_redacted": 493,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2244,
14
+ "functional_verbatim": 2248,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2762,
20
- "tree_sha256": "0795b0e4670053ed5c638dac4719252988b0dbfd0d863dafef3df5b466a11f45",
19
+ "file_count": 2767,
20
+ "tree_sha256": "2981d7c12dcad3c4b86f0b609213d11eed3a70b0e9aa62518c1adc9535ee79fb",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1989,10 +1989,15 @@
1989
1989
  "mode": "0000644",
1990
1990
  "sha256": "7c5d4c49dc98d37a8a41aa0cddd158c3a46a60cdb4e79b018d68c6b5222b7e79"
1991
1991
  },
1992
+ {
1993
+ "path": "docs/adr/0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md",
1994
+ "mode": "0000644",
1995
+ "sha256": "02f7e793bdf3665cf371f273dda6b84701918dc8fd031fac2893a19ed9f0bd9a"
1996
+ },
1992
1997
  {
1993
1998
  "path": "docs/adr/README.md",
1994
1999
  "mode": "0000644",
1995
- "sha256": "8af8b8c5ea9b368bb790280eadd167b99ed9f864350e101ba18a4b0ec8f5ed5e"
2000
+ "sha256": "7fec861b916708b1292c3b69c76d2277368ffbaa10cbf0eab3e60b7512afab44"
1996
2001
  },
1997
2002
  {
1998
2003
  "path": "docs/api-reference.md",
@@ -2007,7 +2012,7 @@
2007
2012
  {
2008
2013
  "path": "docs/architecture.md",
2009
2014
  "mode": "0000644",
2010
- "sha256": "d8a7777f2ebac97a6fe57324a31fe3747fc629feee725a7b3e27fe23e046d69a"
2015
+ "sha256": "02a8f7f07465f7059bc888a367793812c78d689f2335d333529712833c452116"
2011
2016
  },
2012
2017
  {
2013
2018
  "path": "docs/branding-contract.md",
@@ -2557,7 +2562,7 @@
2557
2562
  {
2558
2563
  "path": "docs/module-api-changelog.md",
2559
2564
  "mode": "0000644",
2560
- "sha256": "02d7156a9ee2a022e71835b03c120059c44b36cf9358085bf2c2d1fb91a72bd4"
2565
+ "sha256": "ccb66f38c502ddcefa3b6f3ba78a2e52e9e997c82ad5453fdd1408c1997b8aaa"
2561
2566
  },
2562
2567
  {
2563
2568
  "path": "docs/modules-contract.md",
@@ -5534,6 +5539,11 @@
5534
5539
  "mode": "0000644",
5535
5540
  "sha256": "acc9cf557aabc3fe10efc8c7272162cf288428e8077a24f5df06648e30881337"
5536
5541
  },
5542
+ {
5543
+ "path": "modules/lifecycle/check-rollup.js",
5544
+ "mode": "0000644",
5545
+ "sha256": "3a69defc4e2157c104c9a95d2b67f4b68053c0a0e049683ddd9eb970d3f69871"
5546
+ },
5537
5547
  {
5538
5548
  "path": "modules/lifecycle/claim-eligibility.js",
5539
5549
  "mode": "0000644",
@@ -5657,7 +5667,7 @@
5657
5667
  {
5658
5668
  "path": "modules/lifecycle/github-push.js",
5659
5669
  "mode": "0000644",
5660
- "sha256": "4c8887bafc8c60cd31b90f27edac898f8c93ebf9a17607e4e744720947cc052d"
5670
+ "sha256": "f9c22388c70ba8476a6dba8cd072d92abc44da79c296a4685abab81e24edf6ed"
5661
5671
  },
5662
5672
  {
5663
5673
  "path": "modules/lifecycle/goal-advisory.js",
@@ -5887,7 +5897,7 @@
5887
5897
  {
5888
5898
  "path": "modules/lifecycle/routes/tasks.js",
5889
5899
  "mode": "0000644",
5890
- "sha256": "06948b8df847145848dec75dcb3c525cecd24948064eae19ae524e0498576b2c"
5900
+ "sha256": "22a55af5d5c8453a5c27351a74b03bd635638ea177f599763513752f88f7a224"
5891
5901
  },
5892
5902
  {
5893
5903
  "path": "modules/lifecycle/routes/version-route-authz.js",
@@ -7187,7 +7197,7 @@
7187
7197
  {
7188
7198
  "path": "modules/security/artifact-scan/quarantine.js",
7189
7199
  "mode": "0000644",
7190
- "sha256": "9ac4695e571efdda2b991ff42d4635c9b493383733819c8c97ca93f1066220a1"
7200
+ "sha256": "af3f54455c88b9299a1eb4132709071cf7c92707ece4a947c9e18db86db8f7f1"
7191
7201
  },
7192
7202
  {
7193
7203
  "path": "modules/security/artifact-scan/rebuild-spec.js",
@@ -7202,7 +7212,7 @@
7202
7212
  {
7203
7213
  "path": "modules/security/artifact-scan/sanitize.js",
7204
7214
  "mode": "0000644",
7205
- "sha256": "861abc56de494363d4420d8e5826eeda01ba6ba79f113deceea094db4d9ab6a8"
7215
+ "sha256": "e70485e457a496bc518fc8b26dfaa0ceca1768158cfb95407668c659a6f2b24b"
7206
7216
  },
7207
7217
  {
7208
7218
  "path": "modules/security/migrations/security_001_tool_scans.sql",
@@ -7257,7 +7267,7 @@
7257
7267
  {
7258
7268
  "path": "modules/security/tests/mirror.mjs",
7259
7269
  "mode": "0000644",
7260
- "sha256": "b1bceda3d21897133db69f22d02fc91ed9671c536432d32959409637bfaf3bdb"
7270
+ "sha256": "fbded0d3f137a62c9f6255297b2aa5752e0dc2a6175ce0e344d461cf59bc53b1"
7261
7271
  },
7262
7272
  {
7263
7273
  "path": "modules/security/tests/sanitize_rebuild.mjs",
@@ -7842,12 +7852,12 @@
7842
7852
  {
7843
7853
  "path": "package-lock.json",
7844
7854
  "mode": "0000644",
7845
- "sha256": "f833d910fdbed07c2c2a6a1597d5d6315780e4633c6015bde77629b7682f8c8d"
7855
+ "sha256": "b91cd05ca37aecf647568e064603860f0bb149fb79cd1147c3700aa4adf495c5"
7846
7856
  },
7847
7857
  {
7848
7858
  "path": "package.json",
7849
7859
  "mode": "0000644",
7850
- "sha256": "6fd9cd7067b463895344e6de0710ce6eda471f96d659161c56f9ce3cc9c0edb1"
7860
+ "sha256": "5111d54c082eb876389b62997fb898a8bdb195fa4e91091992a8f092a13e4b35"
7851
7861
  },
7852
7862
  {
7853
7863
  "path": "public-docs/index.html",
@@ -8387,7 +8397,7 @@
8387
8397
  {
8388
8398
  "path": "scripts/gds/fitness.js",
8389
8399
  "mode": "0000644",
8390
- "sha256": "aa1a26c0b2871cd6e49b619254ce2b11d477780da8f4b037c38cb16b0ccd68fc"
8400
+ "sha256": "42c49bcab43be3367f05b99f4dfded590edf89ae10c1a3df2d9312b6e0d33fc9"
8391
8401
  },
8392
8402
  {
8393
8403
  "path": "scripts/gds/gate-review.js",
@@ -8627,7 +8637,7 @@
8627
8637
  {
8628
8638
  "path": "scripts/gds/migration-namespace.js",
8629
8639
  "mode": "0000644",
8630
- "sha256": "f90c5e21f17cb613d86de1bad8561783d6b823f54efe2fa8dc16e909f0dbc77c"
8640
+ "sha256": "d5a4715d0e062a2fa9aabdfa19cf668521907df421f4a3532dbfe3d26c9cab11"
8631
8641
  },
8632
8642
  {
8633
8643
  "path": "scripts/gds/mirror-lineage-check.js",
@@ -8807,7 +8817,7 @@
8807
8817
  {
8808
8818
  "path": "scripts/gds/push-path-brief.js",
8809
8819
  "mode": "0000644",
8810
- "sha256": "1ab68c74dc0c4de8dac3b7b6932719e27b5b7bccabb688e64b2284662049f1f7"
8820
+ "sha256": "3e4d1f99b3edd2883ba63fecfd3718f26b6e53f2255f5669ab7f5e4afe3f9c34"
8811
8821
  },
8812
8822
  {
8813
8823
  "path": "scripts/gds/read-session-export.js",
@@ -8892,7 +8902,7 @@
8892
8902
  {
8893
8903
  "path": "scripts/gds/run-unit-tests.js",
8894
8904
  "mode": "0000644",
8895
- "sha256": "1bbffb825eae2d4253a0f2562609fc158c0307e6120bc83fd659890cdbcff2d6"
8905
+ "sha256": "c5fb602f48d48ca31f5f5290d3624050267ec9bdf4b0c2988e3ff7d59e523bff"
8896
8906
  },
8897
8907
  {
8898
8908
  "path": "scripts/gds/sandbox-stage.js",
@@ -9147,7 +9157,7 @@
9147
9157
  {
9148
9158
  "path": "scripts/gds/ship-land.js",
9149
9159
  "mode": "0000644",
9150
- "sha256": "b47a09039f3d35e06284e6d9a8f15c96a6a66039c735ad96a825b48fd0364a9d"
9160
+ "sha256": "0ae08db904a8777c7b6402ab2064d0ee092cf014a63a4c5421582d5aa998e7c3"
9151
9161
  },
9152
9162
  {
9153
9163
  "path": "scripts/gds/ship-memory-sync.js",
@@ -9767,7 +9777,7 @@
9767
9777
  {
9768
9778
  "path": "src/module-api.js",
9769
9779
  "mode": "0000644",
9770
- "sha256": "8e0d57c0e24d78b95565b3c81fdae6ca50647ad6f84ea337eed213109594056e"
9780
+ "sha256": "cc7c4888b81e294f2ea2a0439e1ca80b6a6cc64a3a68e0c71889aed74efc887c"
9771
9781
  },
9772
9782
  {
9773
9783
  "path": "src/module-loader/catalog.js",
@@ -10027,7 +10037,7 @@
10027
10037
  {
10028
10038
  "path": "tests/artifact_staleness.mjs",
10029
10039
  "mode": "0000644",
10030
- "sha256": "8eaeb56d805615a65f30a62e32dd4d180f1b61d7ac084725bc9d9bde8a1d67d5"
10040
+ "sha256": "95156a6c54388afca8894ca6eb16d2472ede78e2ae3cf34ff00942c4551548c3"
10031
10041
  },
10032
10042
  {
10033
10043
  "path": "tests/artist_gate.mjs",
@@ -11062,7 +11072,7 @@
11062
11072
  {
11063
11073
  "path": "tests/github_push_merge_driver.mjs",
11064
11074
  "mode": "0000644",
11065
- "sha256": "850113ed91babc9eb54dc22231f194d283fd3f94cee9deef1e6e0bf6f3a969c2"
11075
+ "sha256": "c4783b6e8171f96f4a83520cfc4335a313dbfce4702cccea6972e520d6f08deb"
11066
11076
  },
11067
11077
  {
11068
11078
  "path": "tests/github_push_stderr_truncation.mjs",
@@ -11392,7 +11402,7 @@
11392
11402
  {
11393
11403
  "path": "tests/government_require_permission.mjs",
11394
11404
  "mode": "0000644",
11395
- "sha256": "95a5215153c5d98f4fe9ecd3d36ca1ce76d9c486abef8f69dfca0ce3d5cfb006"
11405
+ "sha256": "aef9704aa928c09762a16f6f497f5e8ddf1b61b64daa0e1ac00912be5660c38a"
11396
11406
  },
11397
11407
  {
11398
11408
  "path": "tests/government_resolver.mjs",
@@ -12097,7 +12107,7 @@
12097
12107
  {
12098
12108
  "path": "tests/migration_namespace.mjs",
12099
12109
  "mode": "0000644",
12100
- "sha256": "511b7b0129abc708b1cf687f1813affff522cf42e8a70bcff8839f073f566059"
12110
+ "sha256": "42266f01d6ce88fdcdfddf45c2a11bb4599a87ea05b603d41120674900a59c95"
12101
12111
  },
12102
12112
  {
12103
12113
  "path": "tests/migrations_forward_safe.mjs",
@@ -12554,6 +12564,11 @@
12554
12564
  "mode": "0000644",
12555
12565
  "sha256": "5095bb2c890d89337c819fc3fc07fe26507f0bd78607b2b49239bccf23419aa3"
12556
12566
  },
12567
+ {
12568
+ "path": "tests/promote_deps_gate.mjs",
12569
+ "mode": "0000644",
12570
+ "sha256": "36b77a87a3c67fe326e2592428e40ca09fe79a4e6fa54a6e297b4d86b010ebbb"
12571
+ },
12557
12572
  {
12558
12573
  "path": "tests/promote_goal_id.mjs",
12559
12574
  "mode": "0000644",
@@ -12757,7 +12772,12 @@
12757
12772
  {
12758
12773
  "path": "tests/push_path_brief.mjs",
12759
12774
  "mode": "0000644",
12760
- "sha256": "aea6f9ec053bd9477288025b58ed76c166c2d59a24d7ea3481edadba90113887"
12775
+ "sha256": "96d52fb8d0ea02ad7f4dcec59a5637c82055f603be5352e27221410614ea92c2"
12776
+ },
12777
+ {
12778
+ "path": "tests/quarantine_canonical.mjs",
12779
+ "mode": "0000644",
12780
+ "sha256": "d8a2376c5ff96d77378626c9b4d1c32660e8412afb2531cf93085564890ffdef"
12761
12781
  },
12762
12782
  {
12763
12783
  "path": "tests/rank_floor_wiring.mjs",
@@ -12982,7 +13002,7 @@
12982
13002
  {
12983
13003
  "path": "tests/search_index_poller.mjs",
12984
13004
  "mode": "0000644",
12985
- "sha256": "f665d4f0a9ccfb5b92564dfda39d13d6d8ed3146cde3fe2e05bb9f9aec029558"
13005
+ "sha256": "5015bb705395336000e60ef957a48376ad7267bfeb74e3727213684dd30632e7"
12986
13006
  },
12987
13007
  {
12988
13008
  "path": "tests/search_ingest.mjs",
@@ -13002,7 +13022,7 @@
13002
13022
  {
13003
13023
  "path": "tests/search_isolation.mjs",
13004
13024
  "mode": "0000644",
13005
- "sha256": "869d47dc66f8e8a9913bbb875983796225209b3abc3c360c67eaa8e4a0fd8145"
13025
+ "sha256": "225bebacb2660252bae17dd2274aac4ad1e322348480a781ee13f6b1060714b4"
13006
13026
  },
13007
13027
  {
13008
13028
  "path": "tests/search_route.mjs",
@@ -13649,6 +13669,11 @@
13649
13669
  "mode": "0000644",
13650
13670
  "sha256": "a2ab80b7fb38695bb65c617eff1c39cfbe17403b1586cb1b20e3dee50e5a23fa"
13651
13671
  },
13672
+ {
13673
+ "path": "tests/unit_report_status.mjs",
13674
+ "mode": "0000644",
13675
+ "sha256": "cd3586158da6fdae3320352cc783db39f53d00bfc90d8798ab5c8a696f85f524"
13676
+ },
13652
13677
  {
13653
13678
  "path": "tests/unit_runner_serial.mjs",
13654
13679
  "mode": "0000644",
@@ -0,0 +1,66 @@
1
+ # ADR 0301 — A red PR names its failing tests in a commit status, and says so when it cannot see
2
+
3
+ **Status:** accepted · **Date:** 2026-09-17 · **Task:** [task 1003988](https://cloudbongos.com/builders#/task/1003988) (goal 1000095 — BONGOS-V2)
4
+
5
+ **Related:** [ADR 0055](0055-server-mediated-branch-publish.md) (a builder box holds no GitHub credential; the server answers for GitHub) · [ADR 0031](0031-cloud-dev-environments-for-builders.md) §6 (the box's clone token is one-shot) · [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (shipping lands on CI checks alone) · [ADR 0082](0082-server-side-merge-conflict-auto-resolution.md) (the stale-generated-file heal)
6
+
7
+ ## Context
8
+
9
+ On 2026-09-17 one task's pull request took **nine red CI runs in four hours**: the first eight
10
+ failed the same `security` test, the ninth a different one. The builder's session was not being
11
+ careless. It was blind:
12
+
13
+ - `GET /tasks/:id/publish-status` reported `rollup_state: FAILURE` with `checks: []`. The Cloud
14
+ Bongos GitHub App has no `Checks: read`, so GitHub answers the rollup **state** and nulls every
15
+ `CheckRun` node (blocker 1000151). The code already tolerated that — and then dropped the holes
16
+ silently, so an empty list read as "nothing failed".
17
+ - `mergeBlockReason` then called the red PR `blocked_required_review_or_pending_check`, which is
18
+ an instruction to *wait*.
19
+ - A box has no `gh`, no credential, and the repository is private, so there is no other door.
20
+
21
+ A task parked at `confirmed` also shuts that builder's claim gate, so one unreadable red check
22
+ stopped all of their work. A non-technical builder has no independent way to sanity-check the
23
+ agent's reading of any of this.
24
+
25
+ ## Decision
26
+
27
+ 1. **Say when the list is blind.** `getMergeState` counts the nulled nodes and returns
28
+ `checks_unreadable`. A red rollup with no readable failing check is reported as
29
+ `failing_check_unreadable`, never as a review/pending wait. A `PENDING` rollup keeps the old
30
+ reading, because there a wait is the truth.
31
+ 2. **Report the failing tests through a channel the App can already read.** A `StatusContext`
32
+ survives the missing permission where a `CheckRun` does not (it is why `gate-author-trust` was
33
+ the one entry that ever appeared). The `unit` workflow gains a `report` job that posts a
34
+ `unit-report` commit status whose description is `N failed: <names>`; the server returns it as
35
+ `failing_tests`.
36
+ 3. **The write-scoped token never sits beside PR code.** `report` is a separate job: it holds
37
+ `statuses: write`, checks out nothing, and receives the failure line as a job output through
38
+ `env`. The `unit` job keeps `contents: read`.
39
+ 4. **Treat the description as untrusted.** The PR controls the workflow that writes it, and the
40
+ text is handed to an agent, so `failing_tests` is reduced to test-list vocabulary and the raw
41
+ description never rides out on `checks[]`.
42
+
43
+ ## Alternatives rejected
44
+
45
+ - **Only ask the owner for `Checks: read`.** Still the right grant, and blocker 1000151 stays
46
+ open for it — but every new instance starts without it, and a fix that needs an owner action
47
+ on each install is the interruption this work exists to remove. With the grant, `checks[]`
48
+ fills in and (1) and (2) simply agree with it.
49
+ - **Post the status from the `unit` job.** One job fewer, but it puts a status-writing token in
50
+ the job that runs PR-controlled tests. Gate contexts are commit statuses too.
51
+ - **A PR comment.** Readable without the permission, but it accumulates, notifies people, and is
52
+ not part of the head commit's rollup the server already reads.
53
+ - **Make the poll bail on red.** The reconciler can still heal a stale generated file and turn the
54
+ PR green, so `pollServerPublish` prints the reason once and keeps its window.
55
+
56
+ ## Consequences
57
+
58
+ - `failing_check:unit-report` matches the reconciler's existing `^failing_check:` test, so the
59
+ ADR 0082 stale-generated-file heal can now fire on an instance without `Checks: read`. It was
60
+ designed to; it never could.
61
+ - `report` is not a required check and never fails — a status that could not be posted (a fork's
62
+ read-only token) is a warning. While it runs, a green PR reads `UNSTABLE` for a few seconds;
63
+ GitHub's own auto-merge ignores non-required contexts, and the server's `mergeIfGreen` catches
64
+ it on the next sweep.
65
+ - The CI log still prints only a node:test file's summary counts, not its assertion. Naming the
66
+ test is what makes a local reproduction possible; printing the assertion is separate work.
@@ -392,3 +392,4 @@ This keeps the decision history honest and traceable.
392
392
  | 0298 | [**The grader gets a reader lens: advisory, scoped to copy-bearing diffs, and it waits on a written voice** ([task 1003937](https://cloudbongos.com/builders#/task/1003937), goal 1000095 — Working area 6; owner decision 2026-09-17). Every artist review the cascade files opened with *“REVIEW THE GRADER … coach it”*, and it could not be followed: the v4 panel runs narc, quality, hacker and efficiency — completion, code quality, security, runtime cost — and not one of them reads a sentence for whether it is true, clear, in the project’s voice or worth a person’s attention. Four consecutive reviews hit the same wall and all four graded ships deservedly PASSED on the axes that were measured; the gate is not lenient, it is not pointed at this. `grader-prompt.js` already injects the craft’s written responsibility, but only when `discipline === ‘artist’`, so the unjudged case is copy shipped by someone whose craft is not the copy. **Decision: a fifth ADVISORY `reader` worker, on three settled terms.** It runs only where `modules/lifecycle/cascade.js`’s existing `hasCopyOrVisualWork` says the ship is copy-bearing — the same predicate the after-the-fact review uses, so the two can never disagree about coverage, and every other ship costs exactly what it costs today. It records `issues[]` and a `copy_score` and can never verdict `fail` or drag the panel to `concerns`, because [ADR 0241](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md) put the artist’s judgement on the DEPLOY and off the ship path; gating would need that ADR amended first. And it is NOT BUILT until the instance writes a voice guide — today the whole written standard is one dash rule in `DESIGN.md`, a tagline, a registry of which 1,860 strings exist, and one responsibility sentence, so a lens built now would record taste and call it a finding. Until the reader exists, STEP 1 asks the artist to WRITE DOWN what the grade could not see rather than to coach a lens that is not listening — followable today, and the corpus both the guide and the lens get written from. Nothing in the rubric, the active panel or any existing worker changes.](0298-the-reader-lens-is-advisory-and-waits-on-a-voice-guide.md) | grading / the artist’s loop / voice |
393
393
  | 0299 | [**The nod queue stays scoped to routed filings; the backlog walk is the widening** ([task 1003748](https://cloudbongos.com/builders#/task/1003748), goal 1000089). `listRoutedAwaitingNod` filters `origin='route'`, and on 2026-09-08 showed 11 of 232 backlog rows. Re-measured after `/backlog-review` (task 1003746) shipped: 115 backlog rows = 109 dep-gated + 2 spikes (not a person's decision) + 1 stranded + 3 awaiting a nod, all four walked by `/backlog-review`. **Decision: do not widen the card.** The omitted population already has a walkable review with the dependency predicate any widening needed; the copy says *reported* work, so the name is honest; a card slice would be a second copy of the walk's four-bucket partition. Revisit if the un-routed nod bucket outgrows a daily walk (then a count + link, derived from the same partition) or if the walk stops being run.](0299-the-nod-queue-stays-scoped-to-routed-filings.md) | ideas / backlog / attention |
394
394
  | 0300 | [**A missing protected-surface registry is a broken install, not a module opt-out** ([task 1003662](https://cloudbongos.com/builders#/task/1003662), goal 1000091). Filed as a bug with a remedy attached: `require('./module-scope-map')` throws when `modules/government/protected-surfaces.json` is absent, so make it degrade to an empty protected set + a warning, matching `resolveOptional`. The throw reproduces, and the rest is wrong four times over. **The throw is not in the named file** — `module-scope-map.js` has no throw and no file read; it requires `permission-path-check.js`, whose `loadRegistry()` throws at module scope. **It is a written contract, not an oversight** — that file's header says FAIL-CLOSED in capitals and already pre-answers the layering worry (the read is `fs`, never `require`, so ADR 0083's one-way rule holds; static data, not code; `route-rank-check.js` reads `modules/*/routes/*.js` the same way). **Disabling the module cannot cause it** — enablement is a runtime flag, never a file deletion, and the read never consults it. **"Not yet materialized" cannot either** — no `files[]` allowlist, so `npm pack --dry-run` puts the 13,274-byte registry in the tarball; per ADR 0108 §1 you cannot have `@bongos/core` without it. The `resolveOptional` precedent does not transfer: that is the module **seam/port** resolver for a missing runtime *capability*, whereas this is the static data the boundary is made of, read where there is no server and no DB (a git hook, a CI job) — borrowing it converts "the boundary is unreadable" into "there is no boundary". **Decision: fail-closed stays, no behavior change, the remedy is rejected — and the one genuine defect is fixed: the message now names the cause and the fix** (reinstall the core; do not empty the set), because stating a refusal without a remedy is exactly how a reader concludes the module is optional and files this task. The asymmetry is the point: a false "nothing is protected" is silent, permanent and grants authority; a false "this install is broken" is loud and grants nothing. Revisit only if a core-only checkout without `modules/` becomes legitimate — and then the question is *where the registry lives*, not whether it is optional.](0300-a-missing-protected-surface-registry-is-a-broken-install.md) | governance / permissions |
395
+ | 0301 | [**A red PR names its failing tests in a commit status, and says so when it cannot see** ([task 1003988](https://cloudbongos.com/builders#/task/1003988), goal 1000095). One pull request took nine red CI runs in four hours because the builder box could not learn which test failed: without Checks:read GitHub nulls every check-run node, the server dropped the holes silently, and the red PR was reported as a review-or-pending wait. Now the merge state carries checks_unreadable and a red, unreadable rollup reads failing_check_unreadable; the unit workflow posts the failing test names as a unit-report commit STATUS (readable without the permission) from a separate job that holds the only write token and checks out no PR code; the server returns it as failing_tests, reduced to test-list vocabulary because the PR controls the workflow that wrote it.](0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md) | GDS / shipping pipeline |
@@ -226,7 +226,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
226
226
  - Own-scoped session self-service ([ADR 0206](adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md), task 1003341), in [`src/bongos/routes/my-sessions.js`](../src/bongos/routes/my-sessions.js): `GET /me/sessions` (every live session on the caller's own account — source, user agent, created/last-used/expires, a `current` flag; the token value is never selected), `POST /me/sessions/revoke` (`{session_id}` — one of your own; a miss answers `revoked: 0` so a stranger's id cannot be probed) and `POST /me/sessions/revoke-all` (sign out everywhere, the requesting session included). All three gate on `requireBuilder` alone and key every statement on `req.builder.id` — an account is never named in the query, body or path. They exist because a bearer refreshes to `now()+24h` on every call, so a leaked one never expires, and the cross-builder pair (`POST /auth/revoke`, `/auth/revoke-all`, both still gated on the admin `auth.session.revoke` atom) needed an Archon the victim might not have. Surfaced in the hall at Settings → Active sessions.
227
227
  - Authenticated CLI/web: `GET /me` (returns `rank`), `GET /versions[/progress]`, `GET /tasks[?version=&status=&kind=&discipline=]`, `GET /tasks/claimable[?version=&discipline=]`, `GET /tasks/:id`, `POST /tasks` (accepts `discipline` + `criterion_ids`; **metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)), `PATCH /tasks/:id` (supports `parent_task_id`, `kind`, `discipline`, `goal_id` — (re)assign the task's goal; must be a goal in the task's own version, task 1763; **metic+**), `POST /tasks/:id/promote` (**metic+**), `POST /claims`, `POST /claims/:id/resolve`, `POST /cost`.
228
228
  - Criterion rollup + task↔criterion links ([ADR 0025](adr/0025-structured-criterion-task-link.md), [#435](https://example.com/builders#/task/435)/[#438](https://example.com/builders#/task/438)): `GET /versions/:id/progress` rolls up each done-when criterion → its gating tasks (via `task_criteria`) → live status counts + the not-yet-shipped `remaining[]` + `unattributed_tasks` (the read behind the `/status` skill, in `src/bongos/done-when.js criterionProgress`). The link CRUD mirrors the dependency endpoints: `GET /tasks/:id/criteria`, `POST /tasks/:id/criteria` (**metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)) and `DELETE /tasks/:id/criteria/:criterionId` (**metic+**). A criterion ref is a numeric `done_when_criteria.id`, a positional `"Cn"` token, or a `criterion_id` slug, resolved against the task's version (`POST /tasks` links at create time so `/status` counts the task with no backfill).
229
- - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
229
+ - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
230
230
  - Archon monitoring reads backing the `/watch` page (ADR 0036): `GET /builders/roster`, `GET /grades/by-builder[?days=N]` (**archon-only**, [#726](https://example.com/builders#/task/726) — per-builder grade breakdown for the MARKS section; the project-wide aggregate stays public at `/public/grades`), `GET /audit-log`, `GET /override-requests`, `GET /access-requests`, `GET /security/reports`. Personal-prefs writes backing `/settings`: `GET/PATCH /me/skill-prefs`, `PATCH /me/disciplines`.
231
231
  - Per-builder "needs" + own Gemini key ([ADR 0073](adr/0073-builder-needs-signal-and-byok-gemini-key.md), [#1013](https://example.com/builders#/task/1013)): `GET /me` now also carries `needs` ({items, action_needed_count} — the consistent "the system needs an input from you" signal; `modules/builder-settings/builder-needs.js` is the SSOT — carved out in BV1.R80, resolved by `GET /me` via the `builder-settings` kernel port — rendered by the hall Standing card + the CLI `printNeedsNudge`). The own-key (pragmatic BYOK) endpoints, all own-scoped: `GET /me/art-key/own` (masked meta — last4 only), `PUT /me/art-key/own` (validate-on-save via a Google list-models call → encrypt with `src/bongos/secret-box.js` → store; `503 storage_not_configured` until `BUILDER_SECRET_KEY` is set, `422 invalid_key` on a bad key), `DELETE /me/art-key/own`. The existing `GET /me/art-key` (shared-key box-sync delivery) is extended to also deliver the decrypted own key, which `scripts/gds/fetch-art-key.js` syncs into the local `gemini_api_key` slot (own key wins in the `gen_api.py` cascade).
232
232
  - Builder memory (ADR 0024 cloneable memory + ADR 0026 BFG). Self-scoped (owner is always `req.builder.id`, never input): `POST /memory/sync` (push own memory; own 64 MB parser, vs the global 64 KB), `GET /memory/files`, `GET /memory/file`, `DELETE /memory/file` (the BFG delete affordance, 6C.2). Cross-builder (input-owner) endpoints — all privileged + audit-logged fail-closed: `GET /memory/builders/:id/files`+`/file` (**archon forensic** read), `GET /memory/bfg/builders/:id/files`+`/file` (**BFG-principal only**, 6B.1 read), and the one cross-builder WRITE `POST /memory/builders/:id/bfg-write` (**BFG-principal only**, kill-switched `BFG_WRITE_ENABLED` default-OFF, namespaced `bfg/`, attributed `author: BFG`, write+audit in one tx — dreams + transparent corrections, 6C.1/6C.2).
@@ -2138,5 +2138,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2138
2138
  landed since 1.19.830 with no explicit bump. run 35238952391. (task 1002620)
2139
2139
  1.19.832 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2140
2140
  landed since 1.19.831 with no explicit bump. run 35240512571. (task 1002620)
2141
+ 1.19.833 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2142
+ landed since 1.19.832 with no explicit bump. run 35243115057. (task 1002620)
2143
+ 1.19.834 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2144
+ landed since 1.19.833 with no explicit bump. run 35244719662. (task 1002620)
2141
2145
  ---------------------------------------------------------------------------
2142
2146
  ```
@@ -0,0 +1,42 @@
1
+ // modules/lifecycle/check-rollup.js — read a PR head commit's status-check rollup
2
+ // without mistaking "cannot see" for "nothing failed" (task 1003988, ADR 0301).
3
+ //
4
+ // WHY. Without `Checks: read` GitHub answers the rollup STATE and nulls every CheckRun
5
+ // node inside it. Dropping those holes silently left a credential-less builder box
6
+ // reading `FAILURE, checks: []` — a red PR with no named failure — and one PR took
7
+ // nine red runs because the session could only push again and hope. A StatusContext
8
+ // survives the missing permission, so the `unit` workflow also reports its failing
9
+ // tests as one, and this is where the server reads it back.
10
+
11
+ 'use strict';
12
+
13
+ // The commit status .github/workflows/unit.yml posts. Never `unit`: that name is the
14
+ // required check. tests/unit_report_status.mjs pins the workflow to this constant.
15
+ const UNIT_REPORT_CONTEXT = 'unit-report';
16
+
17
+ // A status description is written by a workflow the PR itself controls, and this one
18
+ // is handed to the builder's agent — keep it to what a test list needs (names, a
19
+ // count, separators) and drop everything else before it travels.
20
+ const safeStatusText = (s) => String(s || '').replace(/[^\w ,:./…-]/g, '').slice(0, 140) || null;
21
+
22
+ // readCheckRollup(statusCheckRollup) → { checks, checks_unreadable, failing_tests }.
23
+ // `checks` keeps the {name, conclusion, status} shape callers already read; the raw
24
+ // description deliberately never rides out on it.
25
+ function readCheckRollup(rollup) {
26
+ const nodes = (rollup && rollup.contexts && rollup.contexts.nodes) || [];
27
+ const readable = nodes.filter(Boolean);
28
+ const checks = readable.map((c) =>
29
+ c.__typename === 'CheckRun'
30
+ ? { name: c.name, conclusion: c.conclusion, status: c.status }
31
+ : { name: c.context, conclusion: c.state, status: 'COMPLETED' }
32
+ );
33
+ const report = readable.find((c) => c.context === UNIT_REPORT_CONTEXT);
34
+ const reportIsRed = !!report && ['FAILURE', 'ERROR'].includes(String(report.state).toUpperCase());
35
+ return {
36
+ checks,
37
+ checks_unreadable: nodes.length > readable.length,
38
+ failing_tests: reportIsRed ? safeStatusText(report.description) : null,
39
+ };
40
+ }
41
+
42
+ module.exports = { readCheckRollup, UNIT_REPORT_CONTEXT };
@@ -42,6 +42,7 @@ const os = require('node:os');
42
42
  const path = require('node:path');
43
43
  const { promisify } = require('node:util');
44
44
  const { repoInfo, userAgent } = require('../../src/module-api');
45
+ const { readCheckRollup, UNIT_REPORT_CONTEXT } = require('./check-rollup');
45
46
  const { loadRepoInfo } = repoInfo;
46
47
 
47
48
  const execFileP = promisify(execFile);
@@ -901,7 +902,7 @@ async function getMergeState({ branch }, deps = {}) {
901
902
  statusCheckRollup{ state contexts(first:50){ nodes{
902
903
  __typename
903
904
  ... on CheckRun{ name conclusion status }
904
- ... on StatusContext{ context state }
905
+ ... on StatusContext{ context state description }
905
906
  }}}
906
907
  }}}
907
908
  }
@@ -931,17 +932,11 @@ async function getMergeState({ branch }, deps = {}) {
931
932
  return {
932
933
  pr_state: 'none', number: null, node_id: null, merged: false, head_sha: null,
933
934
  mergeable: null, merge_state: null, review_decision: null, rollup_state: null, checks: [],
935
+ checks_unreadable: false, failing_tests: null,
934
936
  };
935
937
  }
936
938
  const commit = (node.commits && node.commits.nodes && node.commits.nodes[0] && node.commits.nodes[0].commit) || {};
937
939
  const rollup = commit.statusCheckRollup || null;
938
- // .filter(Boolean): a partial FORBIDDEN (App lacks Checks:read) nulls the individual
939
- // CheckRun ELEMENTS inside contexts.nodes; drop the holes before mapping.
940
- const checks = ((rollup && rollup.contexts && rollup.contexts.nodes) || []).filter(Boolean).map((c) =>
941
- c.__typename === 'CheckRun'
942
- ? { name: c.name, conclusion: c.conclusion, status: c.status }
943
- : { name: c.context, conclusion: c.state, status: 'COMPLETED' }
944
- );
945
940
  return {
946
941
  pr_state: String(node.state || '').toLowerCase(), // open|closed|merged
947
942
  number: node.number,
@@ -952,7 +947,9 @@ async function getMergeState({ branch }, deps = {}) {
952
947
  merge_state: node.mergeStateStatus || null, // CLEAN|BLOCKED|BEHIND|DIRTY|DRAFT|UNSTABLE|HAS_HOOKS|UNKNOWN
953
948
  review_decision: node.reviewDecision || null, // APPROVED|REVIEW_REQUIRED|CHANGES_REQUESTED|null
954
949
  rollup_state: (rollup && rollup.state) || null, // SUCCESS|PENDING|FAILURE|ERROR|EXPECTED
955
- checks,
950
+ // checks + checks_unreadable + failing_tests: a missing Checks:read nulls the
951
+ // CheckRun nodes, and an empty list must never read as "nothing failed".
952
+ ...readCheckRollup(rollup),
956
953
  };
957
954
  }
958
955
 
@@ -974,6 +971,9 @@ function mergeBlockReason(s) {
974
971
  if (s.merge_state === 'BLOCKED') {
975
972
  const bad = s.checks.filter(_BAD_CHECK);
976
973
  if (bad.length) return `failing_check:${bad[0].name}`;
974
+ // Red, and the failing check is one of the nulled nodes: say the list is blind
975
+ // rather than guess "review or pending", which sent a box retrying (task 1003988).
976
+ if (s.checks_unreadable && ['FAILURE', 'ERROR'].includes(s.rollup_state)) return 'failing_check_unreadable';
977
977
  return 'blocked_required_review_or_pending_check';
978
978
  }
979
979
  if (s.merge_state === 'UNSTABLE') {
@@ -1000,6 +1000,7 @@ async function mergeIfGreen({ branch }, deps = {}) {
1000
1000
  pr_state: s.pr_state, number: s.number, merged: s.merged, merged_now: false,
1001
1001
  mergeable: s.mergeable, merge_state: s.merge_state, review_decision: s.review_decision,
1002
1002
  rollup_state: s.rollup_state, checks: s.checks, reason: null,
1003
+ checks_unreadable: s.checks_unreadable, failing_tests: s.failing_tests,
1003
1004
  };
1004
1005
  if (s.merged) return base;
1005
1006
  if (s.pr_state !== 'open') return { ...base, reason: 'no_open_pr' };
@@ -1471,6 +1472,7 @@ module.exports = {
1471
1472
  getPullRequestFiles,
1472
1473
  recheckPullRequest,
1473
1474
  GATE_OWNER_APPROVED_CONTEXT,
1475
+ UNIT_REPORT_CONTEXT,
1474
1476
  landConfirmed,
1475
1477
  deployProbeBlind,
1476
1478
  probeDeployVisibility,
@@ -26,6 +26,9 @@ const os = require('node:os');
26
26
  const path = require('node:path');
27
27
  // Intra-module + the published doorway — a module never requires a core internal.
28
28
  const api = require('../../../src/module-api');
29
+ // depRefusalRow: the ONE shape a dependency refusal carries, shared with the claim
30
+ // path so promote and claim describe a blocked task identically (task 1003553).
31
+ const { depRefusalRow } = require('../dead-deps.js');
29
32
  const auth = api;
30
33
  const db = require('../db');
31
34
  const hierarchy = require('../hierarchy');
@@ -535,6 +538,25 @@ module.exports = function buildTasksRouter() {
535
538
  const updated = await db.setCreditsReward(id, rewardAssign.assign);
536
539
  assignedReward = updated ? updated.credits_reward : rewardAssign.assign;
537
540
  }
541
+ // THE FIRST LINE OF DEFENCE (task 1003553). Promote made a task claimable without
542
+ // ever asking whether its dependencies were done — only the CLAIM refused, with
543
+ // DEPS_NOT_SHIPPED. So a manual promote put dependency-blocked work into the
544
+ // claimable feed, and a builder found out by trying to claim it. The feed lied.
545
+ //
546
+ // claimTask's own comment already named this as the reason it re-checks: "the
547
+ // auto-promotion triggers keep backlog tasks gated, but a manually-promoted task
548
+ // could end up 'ready' with pending deps — this is the second line of defence".
549
+ // The first line is what was missing. The claim-time check STAYS: it is the ADR
550
+ // 0015 gate, and a task can acquire a dependency after it is promoted.
551
+ //
552
+ // Reuses the claim path's own predicate rather than growing a second notion of
553
+ // "blocked" — one definition, so the promote and the claim can never disagree about
554
+ // which edges count (own edges plus those inherited from the goal and criteria).
555
+ const unmetDeps = await db.unsatisfiedDeps(id);
556
+ if (unmetDeps.length > 0) {
557
+ return res.fail('deps_not_shipped', 409, { deps: unmetDeps.map(depRefusalRow) });
558
+ }
559
+
538
560
  await db.updateTaskStatus(id, 'ready');
539
561
  res.json({ ok: true, ...(assignedReward !== null ? { assigned_credits_reward: assignedReward } : {}) });
540
562
  });