@bongos/core 1.19.20 → 1.19.22
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 +36 -26
- package/docs/adr/0162-review-gates-retired-rank-consistent-ci-only.md +67 -0
- package/docs/adr/README.md +1 -0
- package/docs/canonical-permissions.md +8 -5
- package/modules/governance/routes/governance.js +3 -0
- package/modules/hall-ui/public/shell.js +31 -0
- package/modules/hall-ui/public/style.css +24 -0
- package/modules/lifecycle/db.js +18 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/bump-version.js +15 -1
- package/scripts/gds/gating.js +22 -14
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/ship.js +25 -2
- package/src/bongos/auth.js +34 -2
- package/src/module-api.js +5 -1
- package/tests/bump_version.mjs +42 -0
- package/tests/gating_review_retired.mjs +63 -0
- package/tests/governance_page_gates.mjs +17 -6
- package/tests/governance_require_permission.mjs +32 -0
- package/tests/reward_gate.mjs +4 -2
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.22",
|
|
6
|
+
"core_contract": "1.19.22",
|
|
7
|
+
"source_commit": "e089ef90ffdf1b4da4f4a6b7f70d0fb10426bf42",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-11T01:24:29.274Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 285,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1204,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1510,
|
|
20
|
+
"tree_sha256": "0a775991e628ae986369cd819f6b384887fa20c1095f7e7263f07f13ad0e8167",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1264,10 +1264,15 @@
|
|
|
1264
1264
|
"mode": "0000644",
|
|
1265
1265
|
"sha256": "056633fe7521143b0e8ba89297426ffd7253deb9ae5aa2fad65c3dfbffddc44f"
|
|
1266
1266
|
},
|
|
1267
|
+
{
|
|
1268
|
+
"path": "docs/adr/0162-review-gates-retired-rank-consistent-ci-only.md",
|
|
1269
|
+
"mode": "0000644",
|
|
1270
|
+
"sha256": "f6176799c7731da3d4970b75e882e0ab88e31b1a412c87957b9a5085bfc799c7"
|
|
1271
|
+
},
|
|
1267
1272
|
{
|
|
1268
1273
|
"path": "docs/adr/README.md",
|
|
1269
1274
|
"mode": "0000644",
|
|
1270
|
-
"sha256": "
|
|
1275
|
+
"sha256": "b3ed6866c5042d2ee2f1c5680af8d6ef85d36c70a4f9e208eac2e5922461f6dc"
|
|
1271
1276
|
},
|
|
1272
1277
|
{
|
|
1273
1278
|
"path": "docs/api-reference.md",
|
|
@@ -1297,7 +1302,7 @@
|
|
|
1297
1302
|
{
|
|
1298
1303
|
"path": "docs/canonical-permissions.md",
|
|
1299
1304
|
"mode": "0000644",
|
|
1300
|
-
"sha256": "
|
|
1305
|
+
"sha256": "dd85972912629d370d8fc79eff1661001e6b4ba9abe9866181c14fb0782c3131"
|
|
1301
1306
|
},
|
|
1302
1307
|
{
|
|
1303
1308
|
"path": "docs/design-contract.md",
|
|
@@ -2792,7 +2797,7 @@
|
|
|
2792
2797
|
{
|
|
2793
2798
|
"path": "modules/governance/routes/governance.js",
|
|
2794
2799
|
"mode": "0000644",
|
|
2795
|
-
"sha256": "
|
|
2800
|
+
"sha256": "2b390c2c51812aa547f85045fe6d36be6c53086f3fa2468ab7c20c4a7d19dc08"
|
|
2796
2801
|
},
|
|
2797
2802
|
{
|
|
2798
2803
|
"path": "modules/governance/routes/role-sync.js",
|
|
@@ -3242,12 +3247,12 @@
|
|
|
3242
3247
|
{
|
|
3243
3248
|
"path": "modules/hall-ui/public/shell.js",
|
|
3244
3249
|
"mode": "0000644",
|
|
3245
|
-
"sha256": "
|
|
3250
|
+
"sha256": "56c1450dd72fcda19841f3a0a98b918b1b875f49971e93d60218fb62e8a21dad"
|
|
3246
3251
|
},
|
|
3247
3252
|
{
|
|
3248
3253
|
"path": "modules/hall-ui/public/style.css",
|
|
3249
3254
|
"mode": "0000644",
|
|
3250
|
-
"sha256": "
|
|
3255
|
+
"sha256": "41fbe46c9bbc6e9a4397733c070cb2d5e7bfd698d629445f786a44c44a35b0c4"
|
|
3251
3256
|
},
|
|
3252
3257
|
{
|
|
3253
3258
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -3347,7 +3352,7 @@
|
|
|
3347
3352
|
{
|
|
3348
3353
|
"path": "modules/lifecycle/db.js",
|
|
3349
3354
|
"mode": "0000644",
|
|
3350
|
-
"sha256": "
|
|
3355
|
+
"sha256": "7dc50e4e88a204583c753555c76f4103df405a143c4266dbff53793c292803f9"
|
|
3351
3356
|
},
|
|
3352
3357
|
{
|
|
3353
3358
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -3997,12 +4002,12 @@
|
|
|
3997
4002
|
{
|
|
3998
4003
|
"path": "package-lock.json",
|
|
3999
4004
|
"mode": "0000644",
|
|
4000
|
-
"sha256": "
|
|
4005
|
+
"sha256": "f85ec26a3a5cbefec1aca8407043cb2eb2886120722a79126d364736f1a67584"
|
|
4001
4006
|
},
|
|
4002
4007
|
{
|
|
4003
4008
|
"path": "package.json",
|
|
4004
4009
|
"mode": "0000644",
|
|
4005
|
-
"sha256": "
|
|
4010
|
+
"sha256": "9275da3329d188788a0e69b719231cd23ae38b3499802edbcefebded17317067"
|
|
4006
4011
|
},
|
|
4007
4012
|
{
|
|
4008
4013
|
"path": "public-docs/index.html",
|
|
@@ -4502,7 +4507,7 @@
|
|
|
4502
4507
|
{
|
|
4503
4508
|
"path": "scripts/gds/bump-version.js",
|
|
4504
4509
|
"mode": "0000644",
|
|
4505
|
-
"sha256": "
|
|
4510
|
+
"sha256": "9709fcffab1d386c174666788687b1dfd974f141ab552ffd5641abd690259619"
|
|
4506
4511
|
},
|
|
4507
4512
|
{
|
|
4508
4513
|
"path": "scripts/gds/capture.js",
|
|
@@ -4692,7 +4697,7 @@
|
|
|
4692
4697
|
{
|
|
4693
4698
|
"path": "scripts/gds/gating.js",
|
|
4694
4699
|
"mode": "0000644",
|
|
4695
|
-
"sha256": "
|
|
4700
|
+
"sha256": "a304965e79b5ab717626bfb9c74358334576c69f5aefcefa059d46a092f46172"
|
|
4696
4701
|
},
|
|
4697
4702
|
{
|
|
4698
4703
|
"path": "scripts/gds/gds-db-backup.service",
|
|
@@ -4782,7 +4787,7 @@
|
|
|
4782
4787
|
{
|
|
4783
4788
|
"path": "scripts/gds/init.js",
|
|
4784
4789
|
"mode": "0000644",
|
|
4785
|
-
"sha256": "
|
|
4790
|
+
"sha256": "e9fbb7ae477a445252f1171851be93fca14f58e4906d030c93d09b37d517ac20"
|
|
4786
4791
|
},
|
|
4787
4792
|
{
|
|
4788
4793
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -5192,7 +5197,7 @@
|
|
|
5192
5197
|
{
|
|
5193
5198
|
"path": "scripts/gds/ship.js",
|
|
5194
5199
|
"mode": "0000644",
|
|
5195
|
-
"sha256": "
|
|
5200
|
+
"sha256": "65af2a0d6fc54ee3dbb3c7faa44796fed2a5817fa48073cebfd9d6a15837fec7"
|
|
5196
5201
|
},
|
|
5197
5202
|
{
|
|
5198
5203
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5387,7 +5392,7 @@
|
|
|
5387
5392
|
{
|
|
5388
5393
|
"path": "src/bongos/auth.js",
|
|
5389
5394
|
"mode": "0000644",
|
|
5390
|
-
"sha256": "
|
|
5395
|
+
"sha256": "95428c88621510a97a5bfaf89a836508501b1db0f0910f529a260fa2dafce3b5"
|
|
5391
5396
|
},
|
|
5392
5397
|
{
|
|
5393
5398
|
"path": "src/bongos/cascade-dispatch.js",
|
|
@@ -5622,7 +5627,7 @@
|
|
|
5622
5627
|
{
|
|
5623
5628
|
"path": "src/module-api.js",
|
|
5624
5629
|
"mode": "0000644",
|
|
5625
|
-
"sha256": "
|
|
5630
|
+
"sha256": "937021d749587baee02de0c83fa721b99a0e23850c5c12aa5332424673e5d6af"
|
|
5626
5631
|
},
|
|
5627
5632
|
{
|
|
5628
5633
|
"path": "src/module-loader/loader.js",
|
|
@@ -5997,7 +6002,7 @@
|
|
|
5997
6002
|
{
|
|
5998
6003
|
"path": "tests/bump_version.mjs",
|
|
5999
6004
|
"mode": "0000644",
|
|
6000
|
-
"sha256": "
|
|
6005
|
+
"sha256": "98ec16d513074b30de8cd897143e4a3df1b3e54132a09d015b99e13e870de384"
|
|
6001
6006
|
},
|
|
6002
6007
|
{
|
|
6003
6008
|
"path": "tests/cap_newcomer_tasks.mjs",
|
|
@@ -6359,6 +6364,11 @@
|
|
|
6359
6364
|
"mode": "0000644",
|
|
6360
6365
|
"sha256": "a5ca654f46407a3172cfa981f399c159c2769b440058eca509f568111614b00c"
|
|
6361
6366
|
},
|
|
6367
|
+
{
|
|
6368
|
+
"path": "tests/gating_review_retired.mjs",
|
|
6369
|
+
"mode": "0000644",
|
|
6370
|
+
"sha256": "53cd6df5521ee48909b217483163bdf52ecfcc9aa5267f0b8a213c2b4b0f76bc"
|
|
6371
|
+
},
|
|
6362
6372
|
{
|
|
6363
6373
|
"path": "tests/gen_freshness.mjs",
|
|
6364
6374
|
"mode": "0000644",
|
|
@@ -6502,7 +6512,7 @@
|
|
|
6502
6512
|
{
|
|
6503
6513
|
"path": "tests/governance_page_gates.mjs",
|
|
6504
6514
|
"mode": "0000644",
|
|
6505
|
-
"sha256": "
|
|
6515
|
+
"sha256": "be6cc9a4070d9de107cd11bfcb7a38e074f8aeb71198839cd42438d3ea2a0c04"
|
|
6506
6516
|
},
|
|
6507
6517
|
{
|
|
6508
6518
|
"path": "tests/governance_parity.mjs",
|
|
@@ -6522,7 +6532,7 @@
|
|
|
6522
6532
|
{
|
|
6523
6533
|
"path": "tests/governance_require_permission.mjs",
|
|
6524
6534
|
"mode": "0000644",
|
|
6525
|
-
"sha256": "
|
|
6535
|
+
"sha256": "2e8d7f3bb58411efd79727f6edffa448136551ef1c3c6973228fdfa7903f7cb3"
|
|
6526
6536
|
},
|
|
6527
6537
|
{
|
|
6528
6538
|
"path": "tests/governance_resolver.mjs",
|
|
@@ -7157,7 +7167,7 @@
|
|
|
7157
7167
|
{
|
|
7158
7168
|
"path": "tests/reward_gate.mjs",
|
|
7159
7169
|
"mode": "0000644",
|
|
7160
|
-
"sha256": "
|
|
7170
|
+
"sha256": "7ae4d9af4d389c34f0d16f921858cbbf5d0d57f3219f830d10552ebcdc43908f"
|
|
7161
7171
|
},
|
|
7162
7172
|
{
|
|
7163
7173
|
"path": "tests/reward_mode_cost_plus_only.mjs",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ADR 0162 — Review gates retired: rank-consistent CI checks only on `main`
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-10
|
|
4
|
+
**Context:** Owner directive (jaxri, Archon), recorded live 2026-08-08–10; idea 1000657 → task 1002635. Amends ADR 0159 (which introduced CODEOWNERS) and the gating posture of ADR 0058/0042.
|
|
5
|
+
**Status:** Accepted. The live half (dropping `require_code_owner_reviews` from branch protection) was applied by the repo-admin account on 2026-08-10 and verified behaviorally (a probe PR touching a formerly-owned path read `mergeStateStatus: CLEAN` on a green `unit` check — PR 32, closed unmerged). This ADR lands the repo half so no re-arm path resurrects the gate.
|
|
6
|
+
|
|
7
|
+
## The incident that forced the decision
|
|
8
|
+
|
|
9
|
+
Wave 3 of the rank choke-point remediation built the merge gates inert (required `unit`
|
|
10
|
+
check, `gating.js`, a CODEOWNERS file scoped to the gate-review HARD_FLOOR_GLOBS); task
|
|
11
|
+
1002619 armed them. Within a day, the code-owner review requirement had stranded two
|
|
12
|
+
graded-PASS wave-4 ships (PR 28 sat `BLOCKED` behind a review only the `example-owner` account
|
|
13
|
+
could give) and then blocked the fix for the strand it caused (the ship.js ci-land fix,
|
|
14
|
+
task 1002629, and the release-cascade fix, task 1002636, both touch owned paths).
|
|
15
|
+
|
|
16
|
+
The mechanics are structural, not incidental. GitHub can only express the gate as
|
|
17
|
+
**accounts** — it has no concept of GDS ranks; PR authors cannot approve their own PRs;
|
|
18
|
+
and on this instance nearly every PR is authored by agent sessions running under the
|
|
19
|
+
owner-archon's daily login. So "code-owner review" degenerates into "one specific
|
|
20
|
+
GitHub account must click on every gate-surface PR," which is:
|
|
21
|
+
|
|
22
|
+
1. **Identity-pinned, not rank-based** — against the instance's governance principle
|
|
23
|
+
(ADR 0016; the no-hardcoded-Archon-identity rule).
|
|
24
|
+
2. **The opposite of the remediation's goal.** The owner's words: builders should have
|
|
25
|
+
*fewer* rank-related shipping problems, not more; a code-owners layer "can only
|
|
26
|
+
create shipping problems for builders."
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
Review requirements on `main` are **retired permanently**:
|
|
31
|
+
|
|
32
|
+
- Branch protection carries **required status checks only** (today: `unit`; the
|
|
33
|
+
`secret-scan` / `gate-review` contexts join when their workflows ship — ADR 0058).
|
|
34
|
+
- `gating.js` `DEFAULT_PROTECTION` sets `required_pull_request_reviews: null`, and
|
|
35
|
+
`cmdOn` **normalizes both restore paths** (snapshot and default) to null — a snapshot
|
|
36
|
+
taken while the review gate was live must not resurrect it on an off/on cycle.
|
|
37
|
+
- The `CODEOWNERS` file is **deleted**. The `CODEOWNERS` *glob* stays in
|
|
38
|
+
`gate-review.js` `HARD_FLOOR_GLOBS`, `protected-surfaces.json`, and the publish
|
|
39
|
+
denylist — so reintroducing the file is itself a flagged gate-surface change, and the
|
|
40
|
+
hard-floor path list keeps protecting the same surfaces where it actually enforces
|
|
41
|
+
(the sandbox gate).
|
|
42
|
+
|
|
43
|
+
## What now guards the safety machinery
|
|
44
|
+
|
|
45
|
+
The compensating controls are all rank-consistent and automatic:
|
|
46
|
+
|
|
47
|
+
| Control | When it bites | Fails |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| required `unit` check (suite + fitness) | before every merge | closed |
|
|
50
|
+
| gate-review sandbox (task 1002607) | agent ship flow on hard-floor paths | closed |
|
|
51
|
+
| subagent grader panel | every GDS ship | closed |
|
|
52
|
+
| post-merge main-audit (task 1002612, every push) | after any merge, incl. a bypass | audit + revert |
|
|
53
|
+
|
|
54
|
+
## Consequences
|
|
55
|
+
|
|
56
|
+
- **Positive:** no human click anywhere in the land path; a graded-PASS ship lands on
|
|
57
|
+
CI alone; the wave-4/-5 strand class cannot recur; the gate posture matches the
|
|
58
|
+
rank-first governance principle.
|
|
59
|
+
- **Negative / accepted trade:** there is **no pre-merge human approval on
|
|
60
|
+
safety-machinery changes**. A compromised or prompt-injected session whose work
|
|
61
|
+
passes the unit check reaches `main`, and is caught *after* merge by the main-audit
|
|
62
|
+
rather than before. The owner chose this trade explicitly (2026-08-08, reaffirmed
|
|
63
|
+
2026-08-10) with the mitigations above in place.
|
|
64
|
+
- **Reversal path:** re-add a CODEOWNERS file, restore a `required_pull_request_reviews`
|
|
65
|
+
block in `gating.js`, remove the `cmdOn` normalization, and re-apply protection from
|
|
66
|
+
an admin account. All four are deliberate steps — the gate cannot come back by
|
|
67
|
+
accident, which is the point of the normalization.
|
package/docs/adr/README.md
CHANGED
|
@@ -233,5 +233,6 @@ This keeps the decision history honest and traceable.
|
|
|
233
233
|
|
|
234
234
|
| 0160 | [**The authoritative main audit gets a repo-verifiable caller** ([task 1002612](https://cloudbongos.com/builders#/task/1002612)). Findings G-3 + G-8, honest limit S-1: `main-audit.js` — the post-push layer [ADR 0043](<redacted>.md) designates authoritative — had two declared invokers that never existed in this git history, and `audit-authorship.sh` (the no-hardcoded-Archon-identity enforcer) had no caller either and turned out red with 340 pre-existing hits on untouched main (idea 1000653). Now a `main-audit` workflow audits every push to `main` over exactly the pushed `before..after` range (clamped-window fallback); the roster comes from the public leaderboard when no session exists (server-authoritative, login-only keying, an absent builder resolves as unresolved — never falsely trusted); new `--require-roster` makes an empty roster a loud DID-NOT-RUN exit 2 (an audit that cannot fire a blocker must not exit green); the lane runs non-strict per the audit's own zero-false-positive calibration; and `audit-authorship.sh` rides as an advisory job until the 1000653 triage. Detective lane, not a merge gate — deliberately outside the required PR contexts. Rider: the known-failing ratchet caught task 1002611's publish_manifest fix exactly as designed — entry removed, ceiling ratcheted 3 → 2, remaining entries re-owned to [task 1002617](https://cloudbongos.com/builders#/task/1002617), idea 1000542 superseded. Rejected: a GDS token secret (silent-fail dependency the devcontainer lane warned about), `--strict` in CI (permanent red rots into wallpaper), enforcing authorship before triage.](<redacted>.md) | governance / build pipeline |
|
|
235
235
|
| 0161 | [**Publish on merge: every merge to core `main` auto-publishes `@bongos/core`** (task 1002620). Supersedes ADR 0137's manual owner-gated publish after its own "when to revisit" clause fired twice in three weeks: 171 commits/10 days, then 104 commits/5 days, sat on `main` reachable by no pin while every layer reported success. A `publish` workflow rides behind the required `unit` check (`workflow_run`, success-gated): when HEAD's `CORE_VERSION` is already on npm, CI bumps a PATCH via the shape-preserving `bump-version.js` (+ changelog line; pinned by `tests/bump_version.mjs`) and pushes the carrier commit; `package-core --self-verify --publish --live` builds through the fail-closed no-leak gate; the release is tagged `core-v<v>`. A MINOR/MAJOR stays a human in-task edit — it decides what patch-channel subscribers receive unattended (ADR 0136), a call CI must not make. Token custody knowingly reverses (owner-accepted): `NPM_TOKEN` write + `RELEASE_PUSH_TOKEN` PAT as Actions secrets, mitigated by the lane shipping INERT behind the `PUBLISH_ON_MERGE=1` repo variable — an owner kill-switch symmetrical to `gating.js`. Idempotent by version-compare; npm duplicate rejection is the backstop; queued no-cancel concurrency collapses merge races.](<redacted>.md) | build pipeline / distribution |
|
|
236
|
+
| 0162 | [**Review gates retired: rank-consistent CI checks only on `main`** ([task 1002635](https://cloudbongos.com/builders#/task/1002635); owner directive, idea 1000657). Amends ADR 0159. Within a day of task 1002619 arming the merge gates, the code-owner review requirement had stranded two graded-PASS wave-4 ships (PR 28 sat BLOCKED behind a review only the repo-owner account could give) and then blocked the fixes for the strands it caused — structurally, not incidentally: GitHub can only express the gate as *accounts* (no rank concept, no self-approval), so on a solo-archon instance it degenerates into "one named login must click every gate-surface PR" — identity-pinned authority, against ADR 0016's rank principle and the remediation's own goal. Decision: branch protection carries required status checks ONLY; `gating.js` `DEFAULT_PROTECTION` sets `required_pull_request_reviews: null` and `cmdOn` normalizes BOTH restore paths so a pre-retirement snapshot cannot resurrect the gate (`tests/gating_review_retired.mjs` pins it with a counter-example); the CODEOWNERS *file* is deleted while its *glob* stays in the hard-floor lists so reintroducing it is itself gate-flagged. Accepted trade, chosen explicitly by the owner: no pre-merge human approval on safety-machinery changes — the unit check, the fail-closed gate-review sandbox, the grader panel, and the every-push main-audit are the net, catching a compromised session's merge after the fact rather than before.](<redacted>.md) | governance / build pipeline |
|
|
236
237
|
|
|
237
238
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -250,11 +250,14 @@ Authority over *the code itself* rides the same ladder, enforced server-side fro
|
|
|
250
250
|
|
|
251
251
|
ADR 0016's per-request rank check covers `/api/gds/*` only. Two surfaces it does **not** cover —
|
|
252
252
|
`git push` to `main` and `ssh` to the droplet — were used in the Builder 25 incident to land an
|
|
253
|
-
un-graded tiering change straight on prod `main`.
|
|
254
|
-
branch protection
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
un-graded tiering change straight on prod `main`. **Since 2026-08-08 (task 1002619), `main` HAS
|
|
254
|
+
live branch protection: the required `unit` status check blocks every direct push, so all work
|
|
255
|
+
lands via PR.** The protection is *checks-only* — a code-owner review requirement was briefly
|
|
256
|
+
enforced and then deliberately retired by the owner (ADR 0162, task 1002635): gates are
|
|
257
|
+
rank-consistent CI checks, never a named human's approval. (The 2026-07-24 state — no protection
|
|
258
|
+
at all, PR #19 merged with zero checks — is preserved in ADR 0043 §"Branch-protection status" as
|
|
259
|
+
history.) `main-audit.js` remains the authoritative *post-merge* layer — it catches what any
|
|
260
|
+
pre-merge gate misses, independent of the builder's cooperation:
|
|
258
261
|
|
|
259
262
|
- A builder **below Metic** (`xenos`, `thetes`) may **not modify permission-sensitive paths** — the
|
|
260
263
|
rank/authz machinery, the ship/grade/deploy pipeline, DB `migrations/`, `infra/`, and the
|
|
@@ -62,6 +62,9 @@ const governancePort = {
|
|
|
62
62
|
isSystem: catalog.isSystem,
|
|
63
63
|
isGrantable: catalog.isGrantable,
|
|
64
64
|
permissionKeys: catalog.permissionKeys,
|
|
65
|
+
// The seeded floor of one atom, for denial text that names the rank instead of
|
|
66
|
+
// only the atom (task 1002613). Null for principal keys and unknown keys.
|
|
67
|
+
permissionFloor: (key) => (catalog.byKey(key) || {}).floor || null,
|
|
65
68
|
resolvePrincipalPermissions: resolver.resolvePrincipalPermissions,
|
|
66
69
|
principalHasPermission: resolver.principalHasPermission,
|
|
67
70
|
isPrincipalKey: catalog.isPrincipalKey,
|
|
@@ -360,6 +360,36 @@
|
|
|
360
360
|
applyZoom();
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
// task 1002613 (map N-1): a server page-gate denial lands here with
|
|
364
|
+
// ?denied=<permission>[&reason=unavailable]. Render the refusal — "you are
|
|
365
|
+
// signed in; you lack X" — then strip the query so a refresh or a shared link
|
|
366
|
+
// doesn't re-show a stale notice. Cosmetic only; the gate already ran.
|
|
367
|
+
function renderDeniedNotice() {
|
|
368
|
+
// Hand-parsed (no URLSearchParams): shell.js is dependency-free by design,
|
|
369
|
+
// and the hall unit tests boot it in a bare vm context.
|
|
370
|
+
var search = (window.location && window.location.search) || '';
|
|
371
|
+
var m = /[?&]denied=([^&]+)/.exec(search);
|
|
372
|
+
if (!m) return;
|
|
373
|
+
var denied = decodeURIComponent(m[1]);
|
|
374
|
+
if (!/^[a-z0-9_.-]{1,80}$/i.test(denied)) return;
|
|
375
|
+
var unavailable = /[?&]reason=unavailable(?:&|$)/.test(search);
|
|
376
|
+
var topbar = document.getElementById('app-topbar');
|
|
377
|
+
if (!topbar) return;
|
|
378
|
+
var el = document.createElement('div');
|
|
379
|
+
el.className = 'denied-notice';
|
|
380
|
+
el.setAttribute('role', 'status');
|
|
381
|
+
el.innerHTML =
|
|
382
|
+
'<span>' + (unavailable
|
|
383
|
+
? 'You’re signed in, but access to that page couldn’t be verified just now (the permission system didn’t answer), so it stayed closed. Try again in a minute.'
|
|
384
|
+
: 'You’re signed in, but that page needs the <code>' + esc(denied) + '</code> permission — your current role doesn’t hold it.') +
|
|
385
|
+
'</span>' +
|
|
386
|
+
'<button type="button" class="denied-notice__close" aria-label="Dismiss">×</button>';
|
|
387
|
+
el.querySelector('.denied-notice__close').addEventListener('click', function () { el.remove(); });
|
|
388
|
+
topbar.insertAdjacentElement('afterend', el);
|
|
389
|
+
var rest = search.replace(/[?&](?:denied|reason)=[^&]*/g, '').replace(/^&/, '?');
|
|
390
|
+
history.replaceState(null, '', window.location.pathname + rest + window.location.hash);
|
|
391
|
+
}
|
|
392
|
+
|
|
363
393
|
function renderFoot() {
|
|
364
394
|
var el = document.getElementById('app-foot');
|
|
365
395
|
if (!el) return;
|
|
@@ -615,6 +645,7 @@
|
|
|
615
645
|
|
|
616
646
|
renderSidebar();
|
|
617
647
|
renderTopbar();
|
|
648
|
+
renderDeniedNotice();
|
|
618
649
|
renderFoot();
|
|
619
650
|
applyTheme();
|
|
620
651
|
applyZoom();
|
|
@@ -455,6 +455,30 @@ button { font-family: var(--font-body); }
|
|
|
455
455
|
overflow: hidden;
|
|
456
456
|
text-overflow: ellipsis;
|
|
457
457
|
}
|
|
458
|
+
/* Page-gate denial notice (task 1002613, map N-1) — rendered by shell.js from
|
|
459
|
+
the ?denied= query a refused page redirect carries. */
|
|
460
|
+
.denied-notice {
|
|
461
|
+
display: flex;
|
|
462
|
+
align-items: baseline;
|
|
463
|
+
gap: 12px;
|
|
464
|
+
padding: 10px 24px;
|
|
465
|
+
font-size: 13px;
|
|
466
|
+
color: var(--warn);
|
|
467
|
+
background: var(--warn-weak);
|
|
468
|
+
border-bottom: 1px solid var(--border);
|
|
469
|
+
}
|
|
470
|
+
.denied-notice code {
|
|
471
|
+
font-size: 12px;
|
|
472
|
+
}
|
|
473
|
+
.denied-notice__close {
|
|
474
|
+
margin-left: auto;
|
|
475
|
+
border: 0;
|
|
476
|
+
background: none;
|
|
477
|
+
color: inherit;
|
|
478
|
+
font-size: 16px;
|
|
479
|
+
line-height: 1;
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
}
|
|
458
482
|
.topbar-menu {
|
|
459
483
|
display: none;
|
|
460
484
|
align-items: center;
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -42,7 +42,7 @@ const doneWhen = require('./done-when.js');
|
|
|
42
42
|
const { generalGoalTitle } = require('./goal-advisory.js');
|
|
43
43
|
// BV1.R106 (ADR 0151): the permission-expressed QUEUE axis of claim eligibility.
|
|
44
44
|
// Kept deliberately separate from the per-task requires_rank floor below.
|
|
45
|
-
const { claimEligibilityAllows } = require('./claim-eligibility.js');
|
|
45
|
+
const { claimEligibilityAllows, CLAIM_ANY, CLAIM_NEWCOMER } = require('./claim-eligibility.js');
|
|
46
46
|
// Pure kernel-adjacent matchers via the doorway (never the core file).
|
|
47
47
|
const { matchProtected } = api.permissionPathCheck;
|
|
48
48
|
const { touchesOverlap } = api.pathMatch;
|
|
@@ -2572,7 +2572,23 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
2572
2572
|
// task.claim.any / task.claim.newcomer — see ./claim-eligibility.js.
|
|
2573
2573
|
const newcomerFriendly = task.newcomer_friendly;
|
|
2574
2574
|
if (!claimEligibilityAllows({ heldPermissions, newcomerFriendly, rankRuleAllows: xenosClaimAllowed(builderRank, newcomerFriendly) })) {
|
|
2575
|
-
|
|
2575
|
+
// task 1002613 (map §4.4): the bare code reported a PERMISSION decision as
|
|
2576
|
+
// a rank one — no axis, no remedy. Copy INSUFFICIENT_RANK's shape below:
|
|
2577
|
+
// name both sides of the wall and the way forward.
|
|
2578
|
+
const opened = newcomerFriendly === true;
|
|
2579
|
+
throw {
|
|
2580
|
+
code: 'rank_too_low_for_task',
|
|
2581
|
+
legacy_code: 'XENOS_RESTRICTED',
|
|
2582
|
+
required: opened ? [CLAIM_ANY, CLAIM_NEWCOMER] : [CLAIM_ANY],
|
|
2583
|
+
actual: builderRank,
|
|
2584
|
+
newcomer_friendly: opened,
|
|
2585
|
+
message: opened
|
|
2586
|
+
? `claiming this task needs ${CLAIM_ANY} or ${CLAIM_NEWCOMER}, and your role holds neither`
|
|
2587
|
+
: `this task is not opened to newcomers (newcomer_friendly is not set) and your role lacks ${CLAIM_ANY}`,
|
|
2588
|
+
hint: opened
|
|
2589
|
+
? 'your builder appears to hold no claim permission at all — ask for your rank-role to be re-synced'
|
|
2590
|
+
: 'pick a newcomer_friendly task from the claimable feed (/builder-start), or a builder whose role holds task.claim.any can claim this one',
|
|
2591
|
+
};
|
|
2576
2592
|
}
|
|
2577
2593
|
|
|
2578
2594
|
// V3.R82 (#301): the inverse gate — a task can RAISE its minimum required
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.22",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.22",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.22",
|
|
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",
|
|
@@ -72,5 +72,19 @@ function main() {
|
|
|
72
72
|
console.log(JSON.stringify({ from, to, applied: apply }));
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// The commit subject THIS lane writes for its own bump commits — publish.yml's
|
|
76
|
+
// `git commit -m` mirrors it verbatim (tests/bump_version.mjs pins the two
|
|
77
|
+
// against each other). isReleaseCommit is the cascade terminator (task 1002636):
|
|
78
|
+
// publish.yml no-ops when current main HEAD matches, because everything at that
|
|
79
|
+
// HEAD was already published by the run that created the commit. Deliberately
|
|
80
|
+
// narrow — a HUMAN release commit ("release: core 1.19.2 — carrier for the
|
|
81
|
+
// init-blocker fixes") must NOT match: that version is new and still needs
|
|
82
|
+
// publishing.
|
|
83
|
+
const RELEASE_COMMIT_RE = /^release: core \d+\.\d+\.\d+ — CI auto-patch carrier \(publish-on-merge/;
|
|
84
|
+
|
|
85
|
+
function isReleaseCommit(subject) {
|
|
86
|
+
return RELEASE_COMMIT_RE.test(String(subject == null ? '' : subject));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { bumpPatch, CONST_RE, RELEASE_COMMIT_RE, isReleaseCommit };
|
|
76
90
|
if (require.main === module) main();
|
package/scripts/gds/gating.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
// scripts/gds/gating.js — owner kill-switch for main-branch merge gating.
|
|
3
3
|
//
|
|
4
4
|
// What "gating" means here — TWO independent levers, both toggled together:
|
|
5
|
-
// 1. GitHub branch protection on `main`: required status checks
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// 1. GitHub branch protection on `main`: required status checks ONLY — no
|
|
6
|
+
// review requirement of any kind (ADR 0162, task 1002635: the owner
|
|
7
|
+
// retired the code-owner/human-approval layer after it stranded graded
|
|
8
|
+
// work; gates are rank-consistent CI checks, and the normalization in
|
|
9
|
+
// cmdOn keeps a pre-retirement snapshot from resurrecting it). The
|
|
10
|
+
// required contexts are ONLY checks that exist and can pass today —
|
|
11
|
+
// currently `unit` (the suite + fitness lane, task 1002610). The planned
|
|
12
|
+
// `secret-scan` and `gate-review` contexts (ADR 0058 phase 3;
|
|
13
|
+
// CORE_EXPECTED_CHECKS in adopt-preflight.js) join DEFAULT_PROTECTION
|
|
14
|
+
// when their workflows ship — requiring a context nothing reports blocks
|
|
15
|
+
// every merge indefinitely (D-12, ADR 0159).
|
|
12
16
|
// 2. The application-layer trust gates that run on every PR — the gate-review
|
|
13
17
|
// and grade-gate CI workflows (ADR 0058 phases 1-3; NOT YET IN THE TREE —
|
|
14
18
|
// workflowState() reports them 'absent' and the toggles below no-op until
|
|
@@ -17,7 +21,8 @@
|
|
|
17
21
|
// (it escalates gate-surface changes — infra/, migrations/, CODEOWNERS,
|
|
18
22
|
// scripts/gds plumbing — to owner approval). Branch protection alone does
|
|
19
23
|
// NOT neutralize these, which is why `off` also `gh workflow disable`s them.
|
|
20
|
-
// With gating ON,
|
|
24
|
+
// With gating ON, every PR — gate-surface or not — merges on the required
|
|
25
|
+
// status checks alone; no human approval is ever required (ADR 0162).
|
|
21
26
|
// With gating OFF, ordinary `gh pr merge N` (and every builder / autonomous
|
|
22
27
|
// session) merges with no checks and no approval. `unit` keeps running either
|
|
23
28
|
// way — a real safety net that forces no approval.
|
|
@@ -78,12 +83,11 @@ const DEFAULT_PROTECTION = {
|
|
|
78
83
|
],
|
|
79
84
|
},
|
|
80
85
|
enforce_admins: false,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
86
|
+
// ADR 0162 (task 1002635): review gates are RETIRED — the owner chose
|
|
87
|
+
// rank-consistent CI checks over human-approval gates after the code-owner
|
|
88
|
+
// requirement stranded graded-PASS work (PR 28). Never reintroduce a
|
|
89
|
+
// required_pull_request_reviews block here; cmdOn also normalizes snapshots.
|
|
90
|
+
required_pull_request_reviews: null,
|
|
87
91
|
restrictions: null,
|
|
88
92
|
};
|
|
89
93
|
|
|
@@ -302,6 +306,10 @@ function cmdOn(repo) {
|
|
|
302
306
|
putBody = DEFAULT_PROTECTION;
|
|
303
307
|
console.log('⚠ branch protection: no snapshot — restoring the baked-in known-good default.');
|
|
304
308
|
}
|
|
309
|
+
// ADR 0162 (task 1002635): review gates are retired. Normalize BOTH restore
|
|
310
|
+
// paths — a snapshot taken while the code-owner requirement was live would
|
|
311
|
+
// otherwise silently resurrect it on the next off/on cycle.
|
|
312
|
+
putBody = { ...putBody, required_pull_request_reviews: null };
|
|
305
313
|
putProtection(repo, putBody);
|
|
306
314
|
const after = getProtection(repo);
|
|
307
315
|
if (!after) fail('PUT returned but protection still reads as OFF — check `gh` auth / admin rights');
|
package/scripts/gds/init.js
CHANGED
|
@@ -362,7 +362,7 @@ function kickoffTasks(spec) {
|
|
|
362
362
|
title: 'Arm the merge gate before a second builder joins (ci deploy mode)',
|
|
363
363
|
kind: 'infra',
|
|
364
364
|
description: 'For a shared instance on ci deploy mode, enable branch protection on main + required checks ' +
|
|
365
|
-
'(unit / secret-scan / gate-review)
|
|
365
|
+
'(unit / secret-scan / gate-review), so untrusted builders cannot push straight to production. No review requirement — gates are CI checks, not human approvals (ADR 0162). Skippable for a solo/local instance.',
|
|
366
366
|
},
|
|
367
367
|
];
|
|
368
368
|
// Production topology is either AUTOMATED (the owner opted into provisioning — the
|
package/scripts/gds/ship.js
CHANGED
|
@@ -3780,6 +3780,27 @@ async function shipMain() {
|
|
|
3780
3780
|
console.error(`(Your work is committed and the task is at 'completed' — re-running resumes, task 941.)`);
|
|
3781
3781
|
process.exit(1);
|
|
3782
3782
|
}
|
|
3783
|
+
// task 1002613 (map P-1): the one genuine permission wall on the ship path
|
|
3784
|
+
// used to surface as a raw envelope dump — an atom name is not a rank, and
|
|
3785
|
+
// no remedy printed at the one station where self-service exists. Interpret
|
|
3786
|
+
// it: name the wall, the floor (from the server's floors map — never
|
|
3787
|
+
// hardcoded), and both ways forward.
|
|
3788
|
+
const errObj = confirm.data && confirm.data.error;
|
|
3789
|
+
const errCode = typeof errObj === 'string' ? errObj : (errObj && errObj.code);
|
|
3790
|
+
if (confirm.status === 403 && errCode === 'permission_forbidden') {
|
|
3791
|
+
const details = (errObj && typeof errObj === 'object' && errObj.details) || {};
|
|
3792
|
+
const floor = details.floors && details.floors['task.confirm.grade_bypass'];
|
|
3793
|
+
const floorNote = floor ? ` (floor: ${floor})` : '';
|
|
3794
|
+
console.error('');
|
|
3795
|
+
console.error(`✖ Confirming without a passing grade is permission-gated: task.confirm.grade_bypass${floorNote}.`);
|
|
3796
|
+
console.error(' This is an authorization wall, not a verdict on your work.');
|
|
3797
|
+
console.error(' Self-service: re-run WITHOUT --skip-grade to use the subagent grader, and if a');
|
|
3798
|
+
console.error(' grade fails, fix its findings and re-run (unlimited, no attempt cap):');
|
|
3799
|
+
console.error(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
3800
|
+
console.error(` If the grade genuinely cannot pass, ask a builder whose role holds`);
|
|
3801
|
+
console.error(` task.confirm.grade_bypass${floorNote} to POST /api/gds/tasks/${taskId}/confirm.`);
|
|
3802
|
+
process.exit(1);
|
|
3803
|
+
}
|
|
3783
3804
|
console.error(`manual-confirm failed (${confirm.status}):`, confirm.data);
|
|
3784
3805
|
process.exit(1);
|
|
3785
3806
|
}
|
|
@@ -3817,7 +3838,8 @@ async function shipMain() {
|
|
|
3817
3838
|
console.error('Task stays at completed. Manual options:');
|
|
3818
3839
|
console.error(` - Re-run /builder-ship ${taskId} to try grading again, or`);
|
|
3819
3840
|
console.error(` - node scripts/gds/ship.js ${taskId} --regrade after fixing findings, or`);
|
|
3820
|
-
console.error(` - POST /api/gds/tasks/${taskId}/confirm to manual-confirm
|
|
3841
|
+
console.error(` - POST /api/gds/tasks/${taskId}/confirm to manual-confirm (last resort —`);
|
|
3842
|
+
console.error(' permission-gated on task.confirm.grade_bypass).');
|
|
3821
3843
|
process.exit(1);
|
|
3822
3844
|
}
|
|
3823
3845
|
|
|
@@ -4000,7 +4022,8 @@ async function regradeMain() {
|
|
|
4000
4022
|
});
|
|
4001
4023
|
if (!gradePost.ok) {
|
|
4002
4024
|
console.error(`grade POST failed (${gradePost.status}):`, gradePost.data);
|
|
4003
|
-
console.error('Task stays at completed. Fix the issue and re-run --regrade,
|
|
4025
|
+
console.error('Task stays at completed. Fix the issue and re-run --regrade (unlimited, no attempt cap).');
|
|
4026
|
+
console.error('(POST /confirm is a last resort — it is permission-gated on task.confirm.grade_bypass.)');
|
|
4004
4027
|
process.exit(1);
|
|
4005
4028
|
}
|
|
4006
4029
|
|
package/src/bongos/auth.js
CHANGED
|
@@ -1502,7 +1502,12 @@ async function runPageGate(name, req, res, next) {
|
|
|
1502
1502
|
}
|
|
1503
1503
|
const held = await resolveHeldPermissions(session.builder_id, session.raw_permission_keys);
|
|
1504
1504
|
if (held === null || !held.has(permission)) {
|
|
1505
|
-
|
|
1505
|
+
// N-1 (task 1002613): a bare deniedTo rendered nothing — every denial read as
|
|
1506
|
+
// "I signed in and got dumped at the home page". Name the permission (and an
|
|
1507
|
+
// authority outage, distinctly) in the query so the hall shell can say why.
|
|
1508
|
+
// Same destination, same audience — only the redirect learned to speak.
|
|
1509
|
+
const reason = held === null ? '&reason=unavailable' : '';
|
|
1510
|
+
res.redirect(302, `${deniedTo}?denied=${encodeURIComponent(permission)}${reason}`);
|
|
1506
1511
|
return;
|
|
1507
1512
|
}
|
|
1508
1513
|
next();
|
|
@@ -1693,11 +1698,38 @@ function requireRank(...allowed) {
|
|
|
1693
1698
|
// null` analogue). Reporting only the intersection explains the denial without
|
|
1694
1699
|
// dumping the builder's entire permission set into an error body.
|
|
1695
1700
|
//
|
|
1701
|
+
// The body also carries a `message` and a `floors` map (missing atom → its
|
|
1702
|
+
// catalog floor rank) so the denial names the rank, not just the atom (task
|
|
1703
|
+
// 1002613, choke-point map §4.4 P-1). Floors resolve through the port at deny
|
|
1704
|
+
// time — never hardcoded prose, which goes stale when a floor moves (the
|
|
1705
|
+
// D-16/N-3 failure shape ADR 0157 left behind).
|
|
1706
|
+
//
|
|
1696
1707
|
// Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('governance.manage'), handler)
|
|
1697
1708
|
function requirePermission(...perms) {
|
|
1698
1709
|
const required = Object.freeze([...perms]);
|
|
1699
1710
|
return async function permissionGate(req, res, next) {
|
|
1700
|
-
const deny = (held) =>
|
|
1711
|
+
const deny = (held) => {
|
|
1712
|
+
const missing = held === null ? [...required] : required.filter((key) => !held.includes(key));
|
|
1713
|
+
const floors = {};
|
|
1714
|
+
const governance = seams.resolveOptional('governance');
|
|
1715
|
+
if (governance && typeof governance.permissionFloor === 'function') {
|
|
1716
|
+
for (const key of missing) {
|
|
1717
|
+
const floor = governance.permissionFloor(key);
|
|
1718
|
+
if (floor) floors[key] = floor;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
const floorNote = Object.keys(floors).length > 0
|
|
1722
|
+
? ` (floor: ${[...new Set(Object.values(floors))].join(', ')})`
|
|
1723
|
+
: '';
|
|
1724
|
+
const message = held === null
|
|
1725
|
+
? `authority could not be established — denied fail-closed (ADR 0016); this route requires ${required.join(', ') || 'a permission list'}${floorNote}`
|
|
1726
|
+
: `missing permission${missing.length === 1 ? '' : 's'}: ${missing.join(', ')}${floorNote}`;
|
|
1727
|
+
return res.fail('permission_forbidden', {
|
|
1728
|
+
status: 403,
|
|
1729
|
+
message,
|
|
1730
|
+
details: { required, held, floors: Object.keys(floors).length > 0 ? floors : null },
|
|
1731
|
+
});
|
|
1732
|
+
};
|
|
1701
1733
|
const builderId = req.builder && req.builder.id;
|
|
1702
1734
|
if (!builderId) return deny(null);
|
|
1703
1735
|
if (required.length === 0) return deny(null);
|
package/src/module-api.js
CHANGED
|
@@ -467,8 +467,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
467
467
|
// landed since 1.19.18 with no explicit bump. run 31346996715. (task 1002620)
|
|
468
468
|
// 1.19.20 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
469
469
|
// landed since 1.19.19 with no explicit bump. run 31347092915. (task 1002620)
|
|
470
|
+
// 1.19.21 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
471
|
+
// landed since 1.19.20 with no explicit bump. run 31446766597. (task 1002620)
|
|
472
|
+
// 1.19.22 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
473
|
+
// landed since 1.19.21 with no explicit bump. run 31449168428. (task 1002620)
|
|
470
474
|
// ---------------------------------------------------------------------------
|
|
471
|
-
const CORE_VERSION = '1.19.
|
|
475
|
+
const CORE_VERSION = '1.19.22'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
472
476
|
|
|
473
477
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
474
478
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/bump_version.mjs
CHANGED
|
@@ -69,3 +69,45 @@ test('the REAL src/module-api.js is bumpable right now (shape not drifted)', ()
|
|
|
69
69
|
assert.match(from, /^\d+\.\d+\.\d+$/);
|
|
70
70
|
assert.ok(text.includes(`const CORE_VERSION = '${to}';`));
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
// ---- isReleaseCommit: the cascade terminator (task 1002636) ----
|
|
74
|
+
//
|
|
75
|
+
// The bug: publish.yml's bump+publish push re-fired the workflow, which found
|
|
76
|
+
// the just-published version "taken" — the BUMP trigger, not a no-op — so every
|
|
77
|
+
// release armed the next one (core 1.19.6 → 1.19.20+ from two work merges).
|
|
78
|
+
// The terminator no-ops the run when main HEAD is this lane's own release
|
|
79
|
+
// commit. These tests pin the predicate to the EXACT subject the workflow
|
|
80
|
+
// writes (extracted from publish.yml, not retyped — the two can't drift apart)
|
|
81
|
+
// and pin that a HUMAN release commit never matches (skipping one would
|
|
82
|
+
// swallow its publish entirely).
|
|
83
|
+
|
|
84
|
+
const { isReleaseCommit, RELEASE_COMMIT_RE } = require(path.join(ROOT, 'scripts', 'gds', 'bump-version.js'));
|
|
85
|
+
const PUBLISH_YML = fs.readFileSync(path.join(ROOT, '.github', 'workflows', 'publish.yml'), 'utf8');
|
|
86
|
+
|
|
87
|
+
test('matches the exact -m subject publish.yml writes (extracted, not retyped)', () => {
|
|
88
|
+
const m = PUBLISH_YML.match(/git commit -m "([^"]+)"/);
|
|
89
|
+
assert.ok(m, 'publish.yml must contain the bump git commit -m literal');
|
|
90
|
+
const subject = m[1].replace('${TO}', '9.9.9');
|
|
91
|
+
assert.ok(isReleaseCommit(subject), `predicate must match the workflow's own subject: ${subject}`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('a HUMAN release commit never matches — skipping it would swallow its publish', () => {
|
|
95
|
+
assert.equal(isReleaseCommit('release: core 1.19.2 — carrier for the init-blocker fixes (task 1002624)'), false);
|
|
96
|
+
assert.equal(isReleaseCommit('release: core 2.0.0 — the doorway break (ADR 9999)'), false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('work-merge subjects, junk, and empties never match', () => {
|
|
100
|
+
assert.equal(isReleaseCommit('Merge pull request #30 from lars589/claude/cloudbongos-npm-registry-b9a57f'), false);
|
|
101
|
+
assert.equal(isReleaseCommit('fix(ship): protection-aware deploy mode'), false);
|
|
102
|
+
assert.equal(isReleaseCommit(''), false);
|
|
103
|
+
assert.equal(isReleaseCommit(null), false);
|
|
104
|
+
assert.equal(isReleaseCommit(undefined), false);
|
|
105
|
+
assert.equal(RELEASE_COMMIT_RE.source.startsWith('^'), true, 'anchored — a subject QUOTING a release message must not match');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('publish.yml wires the terminator: guard step present, every armed step gated', () => {
|
|
109
|
+
assert.ok(PUBLISH_YML.includes('id: relhead'), 'the Already released? guard step exists');
|
|
110
|
+
assert.match(PUBLISH_YML, /isReleaseCommit/, 'the guard delegates to bump-version.js — single source');
|
|
111
|
+
const gates = (PUBLISH_YML.match(/steps\.relhead\.outputs\.release_head == 'false'/g) || []).length;
|
|
112
|
+
assert.ok(gates >= 7, `every post-guard armed step must carry the gate (setup-node, install, resolve, bump, pack, publish, tag) — found ${gates}`);
|
|
113
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// tests/gating_review_retired.mjs — review gates stay retired (ADR 0162, task 1002635).
|
|
2
|
+
//
|
|
3
|
+
// The owner retired the code-owner/human-approval layer after it stranded
|
|
4
|
+
// graded-PASS work (PR 28) — gates are rank-consistent CI checks only. Two
|
|
5
|
+
// resurrection paths existed and both are pinned shut here:
|
|
6
|
+
// 1. gating.js DEFAULT_PROTECTION carrying a required_pull_request_reviews
|
|
7
|
+
// block (the baked-in re-arm), and
|
|
8
|
+
// 2. cmdOn replaying a pre-retirement SNAPSHOT whose put_body still holds
|
|
9
|
+
// require_code_owner_reviews: true.
|
|
10
|
+
// gating.js is a CLI (main() runs on require), so these are source-shape pins
|
|
11
|
+
// (the goal_advisory.mjs pattern), with a counter-example proving the checker
|
|
12
|
+
// catches the old shape.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/gating_review_retired.mjs
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const GATING = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'gating.js'), 'utf8');
|
|
24
|
+
|
|
25
|
+
// The retirement invariant, as a checkable predicate over the source.
|
|
26
|
+
function reviewGateRetired(src) {
|
|
27
|
+
const defaultHasNull = /required_pull_request_reviews:\s*null/.test(src);
|
|
28
|
+
const noRequireTrue = !/require_code_owner_reviews:\s*true/.test(src);
|
|
29
|
+
const cmdOnNormalizes = /putBody\s*=\s*\{\s*\.\.\.putBody,\s*required_pull_request_reviews:\s*null\s*\}/.test(src);
|
|
30
|
+
return { defaultHasNull, noRequireTrue, cmdOnNormalizes, ok: defaultHasNull && noRequireTrue && cmdOnNormalizes };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('DEFAULT_PROTECTION requires no reviews and no code-owner block exists anywhere', () => {
|
|
34
|
+
const r = reviewGateRetired(GATING);
|
|
35
|
+
assert.ok(r.defaultHasNull, 'DEFAULT_PROTECTION must set required_pull_request_reviews: null');
|
|
36
|
+
assert.ok(r.noRequireTrue, 'no require_code_owner_reviews: true may exist in gating.js (ADR 0162)');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('cmdOn normalizes BOTH restore paths — a pre-retirement snapshot cannot resurrect the gate', () => {
|
|
40
|
+
const r = reviewGateRetired(GATING);
|
|
41
|
+
assert.ok(r.cmdOnNormalizes, 'cmdOn must overwrite required_pull_request_reviews to null before putProtection');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('the CODEOWNERS file is gone; its GLOB guards remain where they enforce', () => {
|
|
45
|
+
assert.ok(!fs.existsSync(path.join(ROOT, 'CODEOWNERS')), 'CODEOWNERS must not exist (ADR 0162)');
|
|
46
|
+
const gateReview = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'gate-review.js'), 'utf8');
|
|
47
|
+
assert.match(gateReview, /'CODEOWNERS'/, 'the HARD_FLOOR glob stays — reintroducing the file is itself gate-flagged');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the checker actually CATCHES the pre-retirement shape (not a no-op)', () => {
|
|
51
|
+
const oldShape = `
|
|
52
|
+
const DEFAULT_PROTECTION = {
|
|
53
|
+
required_pull_request_reviews: {
|
|
54
|
+
dismiss_stale_reviews: false,
|
|
55
|
+
require_code_owner_reviews: true,
|
|
56
|
+
required_approving_review_count: 0,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
function cmdOn(repo) { putProtection(repo, putBody); }`;
|
|
60
|
+
const r = reviewGateRetired(oldShape);
|
|
61
|
+
assert.equal(r.ok, false, 'the old review-gated shape must fail the predicate');
|
|
62
|
+
assert.equal(r.noRequireTrue, false, 'require_code_owner_reviews: true must be the detected offender');
|
|
63
|
+
});
|
|
@@ -74,12 +74,22 @@ const PRE_R107_AUDIENCE = {
|
|
|
74
74
|
requireGovernancePage: { xenos: false, thetes: false, metic: true, archon: true },
|
|
75
75
|
};
|
|
76
76
|
// Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
|
|
77
|
-
// they already have a session, so that would loop.
|
|
77
|
+
// they already have a session, so that would loop. Since task 1002613 (map N-1)
|
|
78
|
+
// the redirect carries ?denied=<permission> — and &reason=unavailable when the
|
|
79
|
+
// authority system itself never answered — so the hall shell can render WHY
|
|
80
|
+
// instead of silently dumping the builder at the home page.
|
|
78
81
|
const DENIED_TO = {
|
|
79
82
|
requireBuilderPage: '/builders',
|
|
80
83
|
requireNonXenosPage: '/builders/not-ready',
|
|
81
84
|
requireGovernancePage: '/builders',
|
|
82
85
|
};
|
|
86
|
+
const GATE_PERMISSION = {
|
|
87
|
+
requireBuilderPage: 'page.view.public',
|
|
88
|
+
requireNonXenosPage: 'page.view.builder',
|
|
89
|
+
requireGovernancePage: 'page.view.governance',
|
|
90
|
+
};
|
|
91
|
+
const deniedLoc = (gate, { unavailable = false } = {}) =>
|
|
92
|
+
`${DENIED_TO[gate]}?denied=${GATE_PERMISSION[gate]}${unavailable ? '&reason=unavailable' : ''}`;
|
|
83
93
|
// The equivalent rank floor, for the independent cross-check below.
|
|
84
94
|
const GATE_FLOOR = {
|
|
85
95
|
requireBuilderPage: 'xenos',
|
|
@@ -166,8 +176,8 @@ test('A2: a denied SIGNED-IN builder is redirected to an ungated page, not JSON-
|
|
|
166
176
|
if (PRE_R107_AUDIENCE[gate][rank]) continue;
|
|
167
177
|
const { res, nexted } = await runGate(gate, { rank });
|
|
168
178
|
assert.equal(nexted, false, `${gate}/${rank} must not admit`);
|
|
169
|
-
assert.deepEqual(res._redirect, { code: 302, loc:
|
|
170
|
-
`${gate}/${rank} must 302 to ${
|
|
179
|
+
assert.deepEqual(res._redirect, { code: 302, loc: deniedLoc(gate) },
|
|
180
|
+
`${gate}/${rank} must 302 to ${deniedLoc(gate)} — naming the permission it lacked`);
|
|
171
181
|
assert.equal(res._status, null, `${gate}/${rank} must not set an HTTP error status`);
|
|
172
182
|
assert.equal(res._json, null, `${gate}/${rank} must not answer a JSON body`);
|
|
173
183
|
assert.notEqual(res._redirect.loc, SIGN_IN,
|
|
@@ -211,7 +221,8 @@ test('B2: an UNREGISTERED governance port denies every gate, it does not admit',
|
|
|
211
221
|
for (const gate of GATES) {
|
|
212
222
|
const { res, nexted } = await runGate(gate, { rank: 'archon', port: false });
|
|
213
223
|
assert.equal(nexted, false, `${gate}: a missing authority system must never open a page gate`);
|
|
214
|
-
assert.equal(res._redirect.loc,
|
|
224
|
+
assert.equal(res._redirect.loc, deniedLoc(gate, { unavailable: true }),
|
|
225
|
+
`${gate}: an authority outage must say so (&reason=unavailable), distinct from a permission denial`);
|
|
215
226
|
assert.equal(res._json, null);
|
|
216
227
|
}
|
|
217
228
|
});
|
|
@@ -223,7 +234,7 @@ test('B3: a throwing resolver denies (a DB blip must not open the page)', async
|
|
|
223
234
|
resolve: async () => { throw new Error('connection terminated'); },
|
|
224
235
|
});
|
|
225
236
|
assert.equal(nexted, false, `${gate} must fail CLOSED on a resolve failure`);
|
|
226
|
-
assert.equal(res._redirect.loc,
|
|
237
|
+
assert.equal(res._redirect.loc, deniedLoc(gate, { unavailable: true }));
|
|
227
238
|
}
|
|
228
239
|
});
|
|
229
240
|
|
|
@@ -247,7 +258,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
247
258
|
seams._reset();
|
|
248
259
|
assert.equal(calls, 2, 'the resolver must be consulted on every request');
|
|
249
260
|
assert.equal(admittedAgain, false, 'a revoked role must deny on the very next request');
|
|
250
|
-
assert.equal(second._redirect.loc, '
|
|
261
|
+
assert.equal(second._redirect.loc, deniedLoc('requireGovernancePage'));
|
|
251
262
|
});
|
|
252
263
|
|
|
253
264
|
test('B5: every permission a page gate names is a real catalog key', async () => {
|
|
@@ -83,6 +83,38 @@ test('multiple permissions are AND, not OR — holding one of two still denies',
|
|
|
83
83
|
assert.equal(both.nexted, true, 'holding both must admit');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test('the denial names the floor rank, not just the atom (task 1002613, map P-1)', async () => {
|
|
87
|
+
// With the real port shape (permissionFloor present) the body carries a floors
|
|
88
|
+
// map and a message naming the rank. The floor is read from the catalog here so
|
|
89
|
+
// this test moves WITH a floor change instead of pinning stale prose — the
|
|
90
|
+
// D-16/N-3 failure shape the fix exists to avoid.
|
|
91
|
+
seams._reset();
|
|
92
|
+
seams.registerProvider('governance', {
|
|
93
|
+
resolveBuilderPermissions: holding('idea.triage'),
|
|
94
|
+
permissionFloor: (key) => (catalog.byKey(key) || {}).floor || null,
|
|
95
|
+
});
|
|
96
|
+
const res = fakeRes();
|
|
97
|
+
await auth.requirePermission('task.confirm.grade_bypass')({ builder: BUILDER }, res, () => {});
|
|
98
|
+
seams._reset();
|
|
99
|
+
assert.equal(res._status, 403);
|
|
100
|
+
const err = res._json.error;
|
|
101
|
+
const floor = catalog.byKey('task.confirm.grade_bypass').floor;
|
|
102
|
+
assert.deepEqual(err.details.floors, { 'task.confirm.grade_bypass': floor },
|
|
103
|
+
'floors maps each MISSING atom to its seeded floor');
|
|
104
|
+
assert.ok(err.message.includes('task.confirm.grade_bypass'), 'the message names the atom');
|
|
105
|
+
assert.ok(err.message.includes(floor), 'the message names the rank — an atom name is not a rank');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a port without permissionFloor still denies with a message; floors is null', async () => {
|
|
109
|
+
const { res } = await run(
|
|
110
|
+
auth.requirePermission('governance.manage'), { builder: BUILDER }, holding('idea.triage'),
|
|
111
|
+
);
|
|
112
|
+
assert.equal(res._status, 403);
|
|
113
|
+
assert.equal(res._json.error.details.floors, null);
|
|
114
|
+
assert.ok(res._json.error.message.includes('governance.manage'),
|
|
115
|
+
'the message names the missing atom even when no floor is resolvable');
|
|
116
|
+
});
|
|
117
|
+
|
|
86
118
|
test('no req.builder fails closed (gate mounted without requireBuilder)', async () => {
|
|
87
119
|
const { res, nexted } = await run(auth.requirePermission('idea.triage'), {}, holding('idea.triage'));
|
|
88
120
|
assert.equal(nexted, false);
|
package/tests/reward_gate.mjs
CHANGED
|
@@ -162,8 +162,10 @@ t('claim path AUTO-ASSIGNS inside the FOR UPDATE transaction (never refuses)', (
|
|
|
162
162
|
// The window is a slice heuristic, not a contract: it only has to cover the
|
|
163
163
|
// pre-flight half of claimTask (which spans ~13k chars). Widened 5000 -> 6000 in
|
|
164
164
|
// BV1.R106, when the permission-expressed claim gate pushed the reward-gate block
|
|
165
|
-
// past the old edge
|
|
166
|
-
|
|
165
|
+
// past the old edge; 6000 -> 7000 in task 1002613, when the enriched
|
|
166
|
+
// rank_too_low_for_task denial did the same. Keep it comfortably below the
|
|
167
|
+
// claimTasksBatch boundary (~15.5k chars in).
|
|
168
|
+
const body = dbSrc.slice(start, start + 7000);
|
|
167
169
|
assert.match(body, /rewardGateAssignment\(task\)/, 'claimTask must consult the auto-assign decision');
|
|
168
170
|
assert.match(body, /UPDATE tasks SET credits_reward/, 'claimTask must persist the assigned reward in-tx');
|
|
169
171
|
// it must SELECT the columns the gate needs.
|