@bongos/core 1.19.625 → 1.19.627

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.625",
6
- "core_contract": "1.19.625",
7
- "source_commit": "19a2b295b0d1a0755168ec39b249dc1a5f9b961e",
5
+ "core_version": "1.19.627",
6
+ "core_contract": "1.19.627",
7
+ "source_commit": "9132daa97509f28f38b53cf54a6311f0a43f788d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T08:21:23.081Z",
9
+ "built_at": "2026-09-09T18:37:19.003Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 464,
12
+ "docs_redacted": 465,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2094,
14
+ "functional_verbatim": 2098,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2582,
20
- "tree_sha256": "c6d6c5f0e0b67618fd3d94d6beb816506c9d15c480cc0dc0de0768b4b27cce23",
19
+ "file_count": 2587,
20
+ "tree_sha256": "6f56a454e970eef9adac1d969b0e28f75d79cdc9843f666de389fd2766607e55",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -162,7 +162,7 @@
162
162
  {
163
163
  "path": ".claude/skills/goal-create/SKILL.md",
164
164
  "mode": "0000644",
165
- "sha256": "2df2c9d9ea817c52c47c074ada7aa725a96ea47db90d6d78acd2f986790edc6f"
165
+ "sha256": "535eb9fd7d3effdc2df7e003bb227c65600055b49dc98bc6f832d3198ba417a0"
166
166
  },
167
167
  {
168
168
  "path": ".claude/skills/goal-review/SKILL.md",
@@ -1869,10 +1869,15 @@
1869
1869
  "mode": "0000644",
1870
1870
  "sha256": "cc44045ea59f76dabc62fe43993ffab6004add88c7f6142bfe9215f7f2c33c99"
1871
1871
  },
1872
+ {
1873
+ "path": "docs/adr/0270-rank-floor-derives-without-a-prediction.md",
1874
+ "mode": "0000644",
1875
+ "sha256": "a16ee8bb0d163fec8ffce252e165de4010954e45a7f61bc205163c70077451ee"
1876
+ },
1872
1877
  {
1873
1878
  "path": "docs/adr/README.md",
1874
1879
  "mode": "0000644",
1875
- "sha256": "6b90114abfa5e891a8f2ea16c547c960c982030d6228d43c2a833840123a3f69"
1880
+ "sha256": "26757599a787417d621bd760d61f156f41b12dc73a195d82b912cc6e956d5642"
1876
1881
  },
1877
1882
  {
1878
1883
  "path": "docs/api-reference.md",
@@ -1907,12 +1912,12 @@
1907
1912
  {
1908
1913
  "path": "docs/copy-inventory.md",
1909
1914
  "mode": "0000644",
1910
- "sha256": "69a114c124cf97c86e7449366d4abfc1cc193649c1abdc2b93655f45886c7bfc"
1915
+ "sha256": "da4d2796fa7ecbfb26c308cb7d1c5074c8c7d0d76d98995549166f265c53c656"
1911
1916
  },
1912
1917
  {
1913
1918
  "path": "docs/copy-registry.json",
1914
1919
  "mode": "0000644",
1915
- "sha256": "bfd0e82c97405348f7d2f5bb7cacfa3143adab91f5b9ebfcd7fa8d7a804e76f7"
1920
+ "sha256": "222db57e3effc7706591e17b7ab901b198c79a46212c16b3221d1bb2b31754e2"
1916
1921
  },
1917
1922
  {
1918
1923
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2757,7 +2762,7 @@
2757
2762
  {
2758
2763
  "path": "docs/module-api-changelog.md",
2759
2764
  "mode": "0000644",
2760
- "sha256": "6979862e5d574d45a8cf53196775cb2f3c53e601e4f812e3bd69c3b3a9c7cc15"
2765
+ "sha256": "ccefe35e8bc2cd1de16f6164b9d2782d9dbd6c5c8660ab75e7a0bf198078d2dc"
2761
2766
  },
2762
2767
  {
2763
2768
  "path": "docs/modules-contract.md",
@@ -3779,6 +3784,11 @@
3779
3784
  "mode": "0000644",
3780
3785
  "sha256": "c10c4bbbead4a3bafde336f3433a1432818880751f7a21436a6162f0dbde1abf"
3781
3786
  },
3787
+ {
3788
+ "path": "migrations/core_236_rank_floor_backfill.sql",
3789
+ "mode": "0000644",
3790
+ "sha256": "06b53d66e710dfbf602011459ec8d69adce8e281632927daea0735f8078b2f86"
3791
+ },
3782
3792
  {
3783
3793
  "path": "modules/agents/lib/validate.js",
3784
3794
  "mode": "0000644",
@@ -4912,7 +4922,7 @@
4912
4922
  {
4913
4923
  "path": "modules/hall-ui/public/goal-inbox.js",
4914
4924
  "mode": "0000644",
4915
- "sha256": "524a475dd6ab05e805e8db6baf11c90e10a3e6e76d7a423ffe571cafe61c0324"
4925
+ "sha256": "0e6aacc6e720d1865c45f71b2458f23c5bac8f1643082256f4d7e71d97078994"
4916
4926
  },
4917
4927
  {
4918
4928
  "path": "modules/hall-ui/public/goals-lib.js",
@@ -5507,7 +5517,7 @@
5507
5517
  {
5508
5518
  "path": "modules/ideas/routes/inbox.js",
5509
5519
  "mode": "0000644",
5510
- "sha256": "8dec7682099d131ca050487dafe259bf4c9b511103c472cd0cbce54b4b9a898f"
5520
+ "sha256": "5e7bdb2c09e1e62aacb4b40843392e2c1ca1534a69191c00c3103a257f086c31"
5511
5521
  },
5512
5522
  {
5513
5523
  "path": "modules/ideas/routing.js",
@@ -5602,7 +5612,7 @@
5602
5612
  {
5603
5613
  "path": "modules/lifecycle/db-rank-authz.js",
5604
5614
  "mode": "0000644",
5605
- "sha256": "c93780ea71bb2f8f3d2b21f042c21d029f1ed5336a3aaf178fb3e14925dcc19b"
5615
+ "sha256": "ac33825a705a0d541fc7fe94858f967ef8ef9713a2b7a1f1a03ce268494bb317"
5606
5616
  },
5607
5617
  {
5608
5618
  "path": "modules/lifecycle/db-shared.js",
@@ -5617,7 +5627,7 @@
5617
5627
  {
5618
5628
  "path": "modules/lifecycle/db-tasks.js",
5619
5629
  "mode": "0000644",
5620
- "sha256": "65558689ccfcea8f71b98cd51eaf1fb45d39267f7e6f6cf8fc35814da2752a34"
5630
+ "sha256": "d8cce5b44106009987d3d32b711a114e4505b4e504bbc6fdfc84f161024ee0cd"
5621
5631
  },
5622
5632
  {
5623
5633
  "path": "modules/lifecycle/db-versions.js",
@@ -7682,12 +7692,12 @@
7682
7692
  {
7683
7693
  "path": "package-lock.json",
7684
7694
  "mode": "0000644",
7685
- "sha256": "b1bf1316b6bf58eede860ae199233be76e1beb440d2cf7f4c96171e8f6347f05"
7695
+ "sha256": "72fea99ed1a6a192978e5b257da8b6c819a649e210da1395d7fdace85b35cd4d"
7686
7696
  },
7687
7697
  {
7688
7698
  "path": "package.json",
7689
7699
  "mode": "0000644",
7690
- "sha256": "1e54c8559b66bcda15cbfb24e216bb5d3421a9e0c9d07f240c2414019288fe9a"
7700
+ "sha256": "22510ead9bfe323615390297a28fab238a3a053defb3849e071f7ad66b84aa0d"
7691
7701
  },
7692
7702
  {
7693
7703
  "path": "public-docs/index.html",
@@ -8142,7 +8152,7 @@
8142
8152
  {
8143
8153
  "path": "scripts/gds/fitness.js",
8144
8154
  "mode": "0000644",
8145
- "sha256": "a93e0d79fe1d557d09711e9b9bb62ffc7687b91ee2981b71eb89176eb0fc2ce6"
8155
+ "sha256": "cd9f61f69ef8d017a39bed129e41a3d3944cf53bfaf6ff26eb868e46611b6dc3"
8146
8156
  },
8147
8157
  {
8148
8158
  "path": "scripts/gds/gate-review.js",
@@ -8187,7 +8197,7 @@
8187
8197
  {
8188
8198
  "path": "scripts/gds/gen-api-docs.js",
8189
8199
  "mode": "0000644",
8190
- "sha256": "03b4263f19c0f7157160011f43a174599e5294f0b62fdaf3a6e943e9bfeec855"
8200
+ "sha256": "f9c0d6a1fb24158109d2753c9c2bcb79060a67198a7396ed7ba85bdeadfdeaca"
8191
8201
  },
8192
8202
  {
8193
8203
  "path": "scripts/gds/gen-atlas.js",
@@ -8574,6 +8584,11 @@
8574
8584
  "mode": "0000644",
8575
8585
  "sha256": "084fa5c4da1e755fa0edde5ec770e5151a9f66a247bca222bc7a2366ccf61748"
8576
8586
  },
8587
+ {
8588
+ "path": "scripts/gds/route-shadow-guard.js",
8589
+ "mode": "0000644",
8590
+ "sha256": "be2196b6f1e8859924d5f6b50c5a0cb476938ea70d37b5f820713dd8f5fd15f7"
8591
+ },
8577
8592
  {
8578
8593
  "path": "scripts/gds/routine-schedule.js",
8579
8594
  "mode": "0000644",
@@ -9282,7 +9297,7 @@
9282
9297
  {
9283
9298
  "path": "src/bongos/route-rank-check.js",
9284
9299
  "mode": "0000644",
9285
- "sha256": "96b884a69b7b709a256d220c97a972f9827d74a36a864c216a7b37db3fdf66be"
9300
+ "sha256": "ad2d07652864d577736f6c0f83e27ff197fe2201796eb06c5577743c16119a5b"
9286
9301
  },
9287
9302
  {
9288
9303
  "path": "src/bongos/routes.js",
@@ -9422,7 +9437,7 @@
9422
9437
  {
9423
9438
  "path": "src/module-api.js",
9424
9439
  "mode": "0000644",
9425
- "sha256": "7a6f5a46c866658041e6299fe46188834184f510dd496edef5cb2f7b0c414088"
9440
+ "sha256": "a28b94a91c283d898bed5fe0810e029e31b3ab33760b2c132a320058d3692bff"
9426
9441
  },
9427
9442
  {
9428
9443
  "path": "src/module-loader/catalog.js",
@@ -9542,7 +9557,7 @@
9542
9557
  {
9543
9558
  "path": "tests/api_docs.mjs",
9544
9559
  "mode": "0000644",
9545
- "sha256": "56c093b994b9539ac93fdf206efe00b0c4318e01f65f4fbe377c31981c7c5bd6"
9560
+ "sha256": "b8725188b7385ec26d35365256cd3eab25d62eaeabfeab6a979de0a447b77b04"
9546
9561
  },
9547
9562
  {
9548
9563
  "path": "tests/api_error_envelope_buildcheck.mjs",
@@ -11057,7 +11072,7 @@
11057
11072
  {
11058
11073
  "path": "tests/helpers.mjs",
11059
11074
  "mode": "0000644",
11060
- "sha256": "b3f0a87c51c9e655c712db00a0a8763e2fc08e4de3c6904c6080385e7c7dad14"
11075
+ "sha256": "6a2360675b70ff2b32ca514e2baafa2aecb10e397a2f88630928ee93b351b34b"
11061
11076
  },
11062
11077
  {
11063
11078
  "path": "tests/hierarchy-config.mjs",
@@ -11157,7 +11172,7 @@
11157
11172
  {
11158
11173
  "path": "tests/idea_hall_composer.mjs",
11159
11174
  "mode": "0000644",
11160
- "sha256": "dbe8f91e4f3acc566a36547ce42d30328ba9e651a622fe1f25092d49d4e3a955"
11175
+ "sha256": "84c5bb805688c484ea83d31e371f1b25fba1daaeb380cba41a21594ddb724de1"
11161
11176
  },
11162
11177
  {
11163
11178
  "path": "tests/idea_ideate_full.mjs",
@@ -11182,7 +11197,7 @@
11182
11197
  {
11183
11198
  "path": "tests/idea_routing.mjs",
11184
11199
  "mode": "0000644",
11185
- "sha256": "d4d4e6ce60f1524e821e0c6c9f2844c7ba791b5502072dc00496224404afa990"
11200
+ "sha256": "3a2c5d2e6c3231f69847e1e2aea073e78caf26228b500dbdf612b5bae354d0a2"
11186
11201
  },
11187
11202
  {
11188
11203
  "path": "tests/idea_spark_hall.mjs",
@@ -11197,7 +11212,7 @@
11197
11212
  {
11198
11213
  "path": "tests/idea_spark_queue.mjs",
11199
11214
  "mode": "0000644",
11200
- "sha256": "43f801610e37062c54e631904aecaa10aa2486139aba3aa9eeb820d84fd32368"
11215
+ "sha256": "8bfc36537ffccae702e74e8763534fa535486a2eb8bbf9ee546a2c7368d1dc6b"
11201
11216
  },
11202
11217
  {
11203
11218
  "path": "tests/idea_task_lineage.mjs",
@@ -11772,7 +11787,7 @@
11772
11787
  {
11773
11788
  "path": "tests/profile_activity.mjs",
11774
11789
  "mode": "0000644",
11775
- "sha256": "013df2528c4998588c1d3f89a9a25f8fa1e912b8abc2d8e95c26c250869a386f"
11790
+ "sha256": "a2c62f9c4acf6f7e683766ae29a616c9325465468a86f38c1d512f58efd124df"
11776
11791
  },
11777
11792
  {
11778
11793
  "path": "tests/profile_connections_ui.mjs",
@@ -11902,7 +11917,7 @@
11902
11917
  {
11903
11918
  "path": "tests/prose_edits.mjs",
11904
11919
  "mode": "0000644",
11905
- "sha256": "a613b7cfa675db5231c44319146333d173d2dfa53da092654d5bd11a141483f4"
11920
+ "sha256": "a36acb19e59db1f9eb885ab6497a43d1c8ce0ba6198b0f5cc98f33bf08837fdf"
11906
11921
  },
11907
11922
  {
11908
11923
  "path": "tests/provision.mjs",
@@ -12044,10 +12059,15 @@
12044
12059
  "mode": "0000644",
12045
12060
  "sha256": "aea6f9ec053bd9477288025b58ed76c166c2d59a24d7ea3481edadba90113887"
12046
12061
  },
12062
+ {
12063
+ "path": "tests/rank_floor_wiring.mjs",
12064
+ "mode": "0000644",
12065
+ "sha256": "f81f559b5428497b2246580c7921ecd1ba5a2f1034cf29eb5e5c21b04675f216"
12066
+ },
12047
12067
  {
12048
12068
  "path": "tests/rank_gate.mjs",
12049
12069
  "mode": "0000644",
12050
- "sha256": "d8fff57f5fee95bf11a5c1df3e2c0e342d39ef5b22c5e87f47a61dd9a3d2a8cb"
12070
+ "sha256": "78626dc42e6d31685352e43e1e8e094a904f59d9d0562caf6b6e126934df973f"
12051
12071
  },
12052
12072
  {
12053
12073
  "path": "tests/rank_tier_single_source.mjs",
@@ -12164,6 +12184,11 @@
12164
12184
  "mode": "0000644",
12165
12185
  "sha256": "bdc58224050392ece492ae8e8f12f62a8934d86e3993e0df5b8cb0aac80602e8"
12166
12186
  },
12187
+ {
12188
+ "path": "tests/route_shadow_guard.mjs",
12189
+ "mode": "0000644",
12190
+ "sha256": "552479822e1349a56bf68c636a35787d44da2abacf6917339ce5ce39223b9f2d"
12191
+ },
12167
12192
  {
12168
12193
  "path": "tests/route_validation.mjs",
12169
12194
  "mode": "0000644",
@@ -67,7 +67,7 @@ Everything here is your call. It is presented in Phase 4 as a proposal, not as q
67
67
  - **`category_id`** — the goal's HUMAN category (task 1003277; ADR 0192 — advisory, it gates nothing). Read the live set with `GET /api/gds/work-categories` (never hard-code the labels), infer the best fit from the goal's outcome and its `scope_modules`, and state your pick in the Phase-4 approval round like every other drafted field. **When nothing in the set fits, that is a designed prompt, not an error**: do NOT silently pick the nearest, and do NOT leave it null and move on — surface the gap in the gray-area round and offer to create a new category (`POST /api/gds/work-categories` with `{"label":"..."}`; a duplicate slug converges on the existing row), then use it. Creating uncategorised stays legal — if the user declines to name one, POST the goal without `category_id` and note that the 201 body will carry a `category_advisory` reminding whoever looks next.
68
68
  - **`succeeds_goal_id`** — set it when carrying an achieved goal forward. `POST /goals` validates the predecessor exists *before* creating, so a bad ref can't orphan a goal; it then stamps the predecessor's `succeeded_by_goal_id`. Archiving the predecessor is a **separate** call.
69
69
  - **Criteria — 2 to 4.** A goal's criteria are narrower than a version's: each should plausibly anchor 2–5 tasks. Draft `criterion_id` as a stable kebab slug (position-independent, and what `criterion_ids` on `POST /tasks` should reference); `criterion_md` is the outcome prose.
70
- - **Seed tasks — 3 to 8.** Per criterion, with `kind`, a one-line done-when, `criterion_ids: ["<slug>"]`, and dependency edges. Omit `touches[]` (advisory, ADR 0049 — backfilled from the real diff at ship). Never hand-set `requires_rank` (auto-derives, ADR 0084). Proof tasks use `kind='verify'`; exploration uses `kind='spike'` (and spikes are deliberately left in backlog).
70
+ - **Seed tasks — 3 to 8.** Per criterion, with `kind`, a one-line done-when, `criterion_ids: ["<slug>"]`, and dependency edges. Omit `touches[]` (advisory, ADR 0049 — backfilled from the real diff at ship). Never hand-set `requires_rank`: it auto-derives (ADR 0084) from `needs_migration` and the goal's own scope wall, neither of which needs a prediction (ADR 0270). **Do set `needs_migration: true` on any task that will alter the live database** — it is the input that floors such a task at Metic, and omitting it is how a schema change lands in the open queue. Proof tasks use `kind='verify'`; exploration uses `kind='spike'` (and spikes are deliberately left in backlog).
71
71
 
72
72
  ## Phase 4 — Ask, then get sign-off
73
73
 
@@ -0,0 +1,59 @@
1
+ # ADR 0270 — The rank floor derives from facts, not from a prediction
2
+
3
+ **Date:** 2026-09-09
4
+ **Context:** BONGOS-V2, goal [1000111](https://cloudbongos.com/builders#/task/1003663) (Working area 7 — Government), task [1003663](https://cloudbongos.com/builders#/task/1003663). Repairs an interaction between [ADR 0084](<redacted>.md) (the per-task `requires_rank` floor) and [ADR 0049](<redacted>.md) (the split parallel-safety contract, which retired predictive `touches[]`). Generalises the protected-scope floor task 1003066 (BV1.R05) shipped for the ideas vector.
5
+ **Status:** Accepted.
6
+
7
+ ## Problem
8
+
9
+ ADR 0084 gave every task a `requires_rank` floor, derived at create time by `deriveRequiredRank(touches, securitySensitive)`: a task touching the rank/authz/ship-grade/deploy/migration core floors at `metic`, everything else stays `xenos`. The floor is enforced at claim time, as the `INSUFFICIENT_RANK` rung of the claim refusal ladder.
10
+
11
+ ADR 0049 then retired **predictive** `touches[]`. It is no longer declared up front; it is backfilled from the real committed diff **at ship** — which is *after* the claim the floor exists to gate. So on every create vector that omits it, `matchProtected()` saw an empty array and the floor could not rise. The derivation did not fail loudly; it silently returned `xenos` forever, because it is computed once at creation and never recomputed when `touches` is later backfilled.
12
+
13
+ Measured on this instance before the fix:
14
+
15
+ - **88 tasks declared `needs_migration: true`; 68 of them (77%) sat at `requires_rank: 'xenos'`.**
16
+ - Among them, task 1003372 — *"Hash `builder_sessions.token` at rest"* — was `status: ready`, claimable by any Xenos. A newcomer could claim a rewrite of the auth token store.
17
+ - Goal 1000111 alone (scope wall: `kernel`, `lifecycle`, `government`, `hall-ui`) held 20 claimable tasks at `xenos`, including two that drop columns from the goal-membership authz tables.
18
+
19
+ Two mechanisms kept it invisible:
20
+
21
+ 1. **The authoring routes disagree.** `POST /goals/:id/tasks` makes `touches[]` **required** — it is the blast radius its scope wall checks — so the floor does derive there, and the route comment correctly promises "a protected/sensitive task floors to Metic and a sub-Metic author cannot claim it". Its sibling `POST /tasks` treats `touches[]` as optional, and that is the route `/goal-create` actually files seed tasks through.
22
+ 2. **The skill told authors to disable both inputs.** `goal-create/SKILL.md` said, in one line: *omit `touches[]`* … *never hand-set `requires_rank`*. Omit the only input and never set the output — together those guarantee `xenos`.
23
+
24
+ The task that raised this asked for "an Archon-level review of how the floor should now be derived". Its premise — *every* task since ADR 0049 — turned out to be overstated (August 2026: 83 tasks, 56 `xenos`; September: 11 and 9), because other vectors still pass `touches[]`. The gap is vector-specific. That distinction is what makes the fix cheap.
25
+
26
+ ## Decision
27
+
28
+ **The floor derives from facts a task already carries at create time, never from a prediction.** `deriveRequiredRank` gains a third argument, `opts`, with two touches-free inputs. Any one of four conditions floors a task at `metic`:
29
+
30
+ | Input | Why it is a fact, not a prediction |
31
+ |---|---|
32
+ | `security_sensitive` | Author-declared, unchanged since ADR 0084. |
33
+ | `needs_migration` | The direct successor to the `'migrations/'` entry `POST /tasks` used to fold into `touches[]` before it became a dedicated boolean. A task altering the live database **is** the migration core. |
34
+ | `scopeProtected` | The owning goal's `scope_modules` wall reaches protected territory. Always available — `goal_id` is required on every task since BV1.R60 — and read through the same `scopeIncludesProtected` primitive the pre-push hook, the grader and main-audit use, so it cannot drift from them. |
35
+ | `matchProtected(touches)` | Retained. Still correct on the vectors that *do* declare touches; simply inert on the ones that don't. |
36
+
37
+ `metic` remains the ceiling. `archon` is never auto-assigned — "only the Archon may claim this" stays a deliberate manual override.
38
+
39
+ Three properties travel with the decision:
40
+
41
+ - **Derived at the lowest create layer.** In `db-tasks.createTask`, after `goalIdEffective` resolves — not in each route. This is the argument ADR 0096 already made for creation-time reward assignment: every vector (`POST /tasks`, `POST /goals/:id/tasks`, idea-promotion, capture, newcomer-restock, copy-desk, kickoff-seed, seed scripts) inherits the floor, and a vector added next year cannot forget it. Patching the two authoring routes separately is precisely how they came to disagree.
42
+ - **Raise-only, everywhere.** `highestRank` still clamps upward only, so an explicit `requires_rank` can raise the bar and never drop below the derived floor. `updateTaskRequiresRank` re-derives with the **same four inputs**; without that, an Archon setting a migration task to `xenos` would have succeeded, silently undoing the create-time protection.
43
+ - **Every setter of a floor input ratchets.** A floor derived only at creation is a floor with a back door: `needs_migration` is PATCH-able (task 1002648, to correct the create-time heuristic), so a Metic could have flipped it true on an existing `xenos` task and left the floor untouched — reopening on the PATCH path exactly the gap this ADR closes on the create path. `updateTaskNeedsMigration` is therefore now the exact sibling of `updateTaskSecuritySensitive`: the same `CASE WHEN … requires_rank IN ('xenos','thetes') THEN 'metic'` ratchet, raising only. `tests/rank_floor_wiring.mjs` asserts the ratchet appears in **exactly two** setters, so a third boolean that starts feeding `deriveRequiredRank` cannot quietly ship without one.
44
+ - **Opt-in on a strict `=== true`.** A stray truthy value cannot wall off the open queue.
45
+
46
+ ## What was rejected
47
+
48
+ - **Re-requiring predictive `touches[]` at create.** This is the obvious fix and it is wrong: it re-litigates ADR 0049, whose whole finding was that a wrong up-front prediction blocked or de-credited correct work. The floor is not worth reviving the thing that was deliberately deleted.
49
+ - **Recomputing the floor when `touches` is backfilled at ship.** Too late by construction. The claim it gates has already happened; the recompute would only relabel history.
50
+ - **Deriving from `module_key`.** More precise than the goal wall in principle, but it is null on most tasks (including the one that reported this), so it would leave the same hole.
51
+ - **A new gate or approval step.** Nothing here adds a rung to the claim refusal ladder; the existing `INSUFFICIENT_RANK` rung simply starts receiving the correct input.
52
+
53
+ ## Consequences
54
+
55
+ - **A protected goal wall is blunt, and that is accepted.** `scopeIncludesProtected` is true when *any* module in the wall is protected, so a purely `hall-ui` task inside goal 1000111 (`hall-ui` + `kernel` + `lifecycle` + `government`) also floors at `metic`. This is the same semantics the ideas vector has enforced since task 1003066, it fails in the safe direction, and `metic` is the first *trusted working* rank rather than a rare one. The cost is a smaller Xenos queue in mixed-scope goals. The remedy, when it bites, is to narrow the goal's wall — which is the honest fix anyway.
56
+ - **The newcomer queue is unaffected today** — there are currently **zero** claimable `newcomer_friendly` tasks, in protected-scope goals or otherwise. Worth re-checking if the beginner queue is ever restocked inside a protected-scope goal; a `newcomer_friendly` carve-out was deliberately **not** added, because an exception that lowers a safety floor is exactly the kind of thing that should require a decision rather than exist by default.
57
+ - **A one-time backfill**, `migrations/<redacted>.sql`, raises still-claimable (`backlog`/`ready`) `xenos` rows that meet either new condition. It is scoped to claimable work because the floor is a claim-time gate — rewriting shipped or abandoned rows would change no decision that has not already been made. It raises only, and there is no down-migration, for the reason ADR 0084 gave: lowering a safety floor is never automatic.
58
+ - **The protected-module list is spelled once in that migration**, as a point-in-time snapshot. A migration cannot call into JS, and a historical backfill should record the roster as it stood the day it ran. The live authority remains `module-scope-map.scopeIncludesProtected` over `PROTECTED_GLOBS`.
59
+ - **`goal-create/SKILL.md` now tells authors to set `needs_migration: true`** on any task that will alter the live database. It is the input that floors such a task, and omitting it is how a schema change reaches the open queue.
@@ -361,3 +361,4 @@ This keeps the decision history honest and traceable.
361
361
  | 0267 | [**Unanimity, and the revise-and-re-sit loop** ([task 1003733](https://cloudbongos.com/builders#/task/1003733) · goal 1000111 — *Government*, owner decision 2026-09-08). A Full Idea is ratified by **unanimous** agreement from round one; a sitting that does not carry RETURNS to its author, who revises and re-sits it, unbounded, until the board is unanimous. `unanimous` joins `PASS_RULES` as a fourth answer on the axis these rules actually differ on — **what silence means**: it is two conditions, not one, because "nobody objected" is true of an empty room. Everyone who spoke must have said yes AND somebody must have spoken, so an unvoted sitting RETURNS, which is exactly what makes the clock the owner asked for safe under it ([ADR 0191](<redacted>.md) §4: a deadline may only be given to a rule whose expiry means return). **There is deliberately no membership denominator** — that is the whole difference from `majority`, and it is the owner's "silence does not block" as code: an absent member is not in the reckoning, so one yes out of four carries. The stated cost is that an attentive minority can ratify on a distracted board; the alternative is one person on holiday stopping the pipeline. Trigger changes follow from the rule: a single yes does NOT close it (a later member must still be able to object — the sitting runs its clock, full turnout closes early), while a reasoned objection closes it AT ONCE (no vote-changing in v1 fixes the outcome, and the author needs the feedback to revise). ADR 0191 §3's author rule rides both unchanged. **`BOARD_DEFAULTS` is NOT moved**: a fresh instance stays the day-one monarchy, because under any non-author-yes rule a solo founder can never ratify their own ideas (0191 §5) — adoption is a board amendment, and no env var can change a constitution. The revise-and-re-sit loop already existed (`POST /inbox/:id/resubmit` re-grades and re-fires the window hook); what the board owes it is now pinned by test — a return CLOSES the item so the next window may open, and pays nothing. Named limits: a zero-vote return carries no human objection (the author gets the mechanical weakest-section flag), and this changes nothing live until the separate defect where a ratified amendment does not survive to the next read is fixed.](<redacted>.md) | government / board room |
362
362
  | 0268 | [**The constitution comes from two roots, and a decision that did not take must say so** ([task 1003739](https://cloudbongos.com/builders#/task/1003739) · goal 1000111 — *Government*). On 2026-08-25 cloudbongos.com's board RATIFIED an amendment (rank:metic+ · consent · a 1440-minute sitting) and the constitution never changed — for two weeks `GET /government/constitution` answered the day-one monarchy with the passed amendment sitting in its own `history` array directly beneath the contradicting `board` block. Every sitting since was decided under a rule the board had voted to replace, and it silently un-shipped [ADR 0191](<redacted>.md) (majority) and would have un-shipped [ADR 0267](<redacted>.md) the same way. **Cause:** `modules/government/config.js` resolved BOTH its neutral starter and its instance pack from one `path.resolve(__dirname, '..', '..')`, under a comment saying the roots coincided *today* — true in a single checkout, false on a STANDALONE instance ([ADR 0108](<redacted>.md) §1), where the server runs `node_modules/@cloudbongos/core` with `WorkingDirectory=<instance repo>`. The neutral path stayed right (it really is core content); the INSTANCE path resolved inside the core package, so the host's own `config/government.json` was never read and `applyBoardAmendment` wrote the ratified amendment into `node_modules`, where the nightly core upgrade ([ADR 0161](<redacted>.md) cuts a release per merge) erased it within a day. **Nothing threw and nothing could have** — both paths exist, both are writable, and an absent instance pack is a legal state, so the read fell through to the neutral monarchy exactly as designed. `src/branding.js` had the pattern right three files away. **Fix:** neutral from `resolveCoreRoot()`, instance from `resolveInstanceRoot()`, reached through `src/module-api` — which had exposed NEITHER resolver, and that absence is precisely why the module re-derived the wrong one ([ADR 0083](<redacted>.md) forbids requiring a core internal). Resolved at load, not per call, because `resolveInstanceRoot()` falls back to `process.cwd()` and a lazy resolve would let a `chdir` move the constitution; `applyBoardAmendment` now mkdirs, since the write targets a directory the core does not own. Single-checkout behaviour is byte-identical, asserted rather than claimed. **The detector, which is the durable half:** `constitutionView` carries a `divergence` block comparing the newest PASSED amendment (sanitized as an apply would write it) field-by-field against what is in force, rendered above the hall's dials in words rather than config keys. It REPORTS and never HEALS — the instance pack is also the file a human editing the constitution touches, so a silent re-apply would revert a legitimate hand edit with no way to tell the two apart. A RETURNED amendment is never compared (that is what *returned* means). Does NOT put the 2026-08-25 amendment back in force — that is a board act. Sibling sweep RUN, not promised: one real hit filed as [task 1003745](https://cloudbongos.com/builders#/task/1003745) (quarantine.js's repo-containment refusal is blind to the instance repo).](<redacted>.md) | government / instance roots |
363
363
  | 0269 | [**The CLI session store is host-keyed at a fixed anchor; the per-brand file stays the active pointer** ([task 1003741](https://cloudbongos.com/builders#/task/1003741) · goal 1000090 — *Working area 4, Bongos Core distribution*). The CLI resolved its session path from the BRANDING PACK, read out of whatever checkout the process stood in — one slot, that moved. Standalone (the public CLI's whole situation) no checkout means no brand, so every instance shared `~/.config/cloudbongos/gds-session.json` and signing into a second DESTROYED the first; in-repo it landed in `~/.config/<slug>/` where the standalone CLI could never find it (`npx … api GET /me` from a bare dir returned the cloudbongos builder while a valid hermeslines session sat on the same machine). Not theoretical — the owner's config dir carries a hand-made `gds-session.<redacted>.bak.json` and four more of the same shape. **Decision: one file per instance keyed by HOST at a FIXED anchor (`~/.config/<FALLBACK_DIR>/instances/<host>.json`), with the per-brand `gds-session.json` left exactly where it is as the ACTIVE pointer.** Four load-bearing parts. (1) The anchor must never read `configHome()`/`configDirName()`/the pack — a test asserts the function body names none, since routing it back through the brand silently reinstates the split. (2) The active pointer does NOT move: every existing reader (in-repo skills, the card hook, the dev box, `readSessionToken`) looks there, and the store is purely additive. (3) The OUTGOING session is archived BEFORE the incoming one lands — a session written before the store existed is not in it, so without this the first login after upgrading still loses it, the bug surviving its own fix. (4) A stored token is VERIFIED against `/api/gds/me` before being reinstated; unverified falls through to the real device flow, `--force` signs in as somebody else. `login` also names the other instances and how to switch, since sessions are now kept rather than overwritten. Files 0600 in a 0700 dir; an `api_base` naming no host is not stored rather than stored under a guess, so nothing can write outside the store. The dir is `instances/` NOT `sessions/` — `sessions` is a live module key and [ADR 0083](<redacted>.md) §Decision #4 forbids kernel machinery naming one; the fitness gate caught it. Rejected: moving the active pointer (breaks every reader for no visible gain); one file holding a map (changes a format hooks and the dev box parse today); keying under `configHome()` (the bug); restoring without verifying (hands over a dead session that looks live); a new `bongos switch` verb (login is the command already reached for, and the public CLI's surface is deliberately small).](<redacted>.md) | cli / auth / distribution |
364
+ | 0270 | [**The rank floor derives from facts, not from a prediction** ([task 1003663](https://cloudbongos.com/builders#/task/1003663) · goal 1000111 — *Working area 7, Government*). [ADR 0084](<redacted>.md) derives every task’s claim-time `requires_rank` floor from `touches[]` + `security_sensitive`; [ADR 0049](<redacted>.md) then retired PREDICTIVE `touches[]`, which is now backfilled from the real committed diff at SHIP — after the claim the floor exists to gate. So on every create vector that omits it the matcher saw an empty array, and because the floor is computed ONCE at creation and never recomputed, it returned `xenos` forever. Nothing threw: an empty `touches[]` is a legal state. **Measured before the fix: 88 tasks declared `needs_migration` and 68 of them (77%) sat at `xenos`** — including [task 1003372](https://cloudbongos.com/builders#/task/1003372), a READY task to hash the `builder_sessions` token store at rest, claimable by any newcomer; goal 1000111 alone held 20 such rows. Two mechanisms hid it: the authoring routes DISAGREE (`POST /goals/:id/tasks` REQUIRES `touches[]` as its scope-wall blast radius, so the floor derives there, while its sibling `POST /tasks` — the route `/goal-create` actually files seed tasks through — treats it as optional), and `goal-create/SKILL.md` told authors in ONE line to *omit `touches[]`* and *never hand-set `requires_rank`*: omit the only input, never set the output. **Decision:** the floor derives from facts a task already carries at create time, never from a prediction — `deriveRequiredRank` gains `opts` with two touches-free inputs, `needs_migration` (the direct successor to the `migrations/` entry `POST /tasks` used to fold into `touches[]`) and `scopeProtected` (the owning goal’s `scope_modules` wall, ALWAYS available since `goal_id` is required, read through the same `scopeIncludesProtected` primitive the pre-push hook, grader and main-audit use, so it cannot drift). Derived at the LOWEST create layer in `createTask` after `goalIdEffective` resolves — the ADR 0096 argument — so every vector inherits it and patching the two routes separately (which is how they came to disagree) is not repeated. `updateTaskRequiresRank` re-derives with the same four inputs, else an Archon setting a migration task to `xenos` would silently undo the create-time floor; raise-only throughout; `archon` still never auto-assigned; a strict `=== true` so a stray truthy value cannot wall off the open queue. One-time backfill `core_236` raises still-CLAIMABLE rows only (the floor is a claim-time gate; rewriting shipped rows changes no live decision) and never lowers. **Accepted cost:** a protected goal wall is blunt — any protected module in the wall floors the whole goal, so a `hall-ui` task in a mixed goal floors at Metic too; same semantics the ideas vector has enforced since task 1003066, fails safe, and the remedy is to narrow the wall. Zero claimable `newcomer_friendly` tasks exist today, and no carve-out was added for them on purpose. Rejected: re-requiring predictive `touches[]` (re-litigates ADR 0049); recomputing at ship (too late by construction); deriving from `module_key` (null on most tasks, including the reporter).](<redacted>.md) | permissions / task lifecycle |
@@ -597,7 +597,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
597
597
  | `ae84ebe1d735` | certain | Name cannot be blank. | `modules/hall-ui/public/settings.js:831` |
598
598
  | `31e91ee6ce03` | certain | Name must be {…} characters or fewer. | `modules/hall-ui/public/settings.js:836` |
599
599
  | `5e8199266a8a` | certain | Name unvouched | `modules/hall-ui/public/watch.js:350` |
600
- | `7def635421ea` | certain | Needs your attention | `modules/hall-ui/public/goal-inbox.js:385` |
600
+ | `7def635421ea` | certain | Needs your attention | `modules/hall-ui/public/goal-inbox.js:396` |
601
601
  | `87f09d983f6c` | certain | New category label | `modules/hall-ui/public/project-settings.js:138` |
602
602
  | `d427d9dab01c` | certain | New claims are paused: {…} confirmed {…} can't land yet, so {…} reward is pending. Rebase and re-ship, or rele | `modules/hall-ui/public/hall-render.js:618` |
603
603
  | `7fcc2583c372` | certain | New custom rank | `modules/hall-ui/public/government.html:79` |
@@ -6401,7 +6401,7 @@
6401
6401
  "surface": "builders-hall",
6402
6402
  "text": "Needs your attention",
6403
6403
  "file": "modules/hall-ui/public/goal-inbox.js",
6404
- "line": 385,
6404
+ "line": 396,
6405
6405
  "origin": "js-markup",
6406
6406
  "confidence": "certain"
6407
6407
  },
@@ -1699,5 +1699,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1699
1699
  landed since 1.19.623 with no explicit bump. run 34326009063. (task 1002620)
1700
1700
  1.19.625 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1701
1701
  landed since 1.19.624 with no explicit bump. run 34328603214. (task 1002620)
1702
+ 1.19.626 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1703
+ landed since 1.19.625 with no explicit bump. run 34363350286. (task 1002620)
1704
+ 1.19.627 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1705
+ landed since 1.19.626 with no explicit bump. run 34390069410. (task 1002620)
1702
1706
  ---------------------------------------------------------------------------
1703
1707
  ```
@@ -0,0 +1,56 @@
1
+ -- core_236_rank_floor_backfill.sql — raise the requires_rank floor on work that
2
+ -- was created while the derivation was inert (task 1003663, goal 1000111,
3
+ -- ADR 0270).
4
+ --
5
+ -- WHY THERE IS ANYTHING TO BACK FILL. deriveRequiredRank has always derived the
6
+ -- per-task floor from touches[] + security_sensitive (task 1498, ADR 0085). ADR
7
+ -- 0049 then retired PREDICTIVE touches[] — it is backfilled from the real
8
+ -- committed diff at SHIP, which is after the claim this floor exists to gate —
9
+ -- so on every create vector that omits it the matcher saw an empty array and the
10
+ -- floor never rose. Measured on this instance before the fix: 88 tasks declared
11
+ -- needs_migration and 68 of them (77%) sat at requires_rank='xenos', including
12
+ -- task 1003372, a READY task to rewrite the auth token store at rest.
13
+ --
14
+ -- ADR 0270 gives the derivation two touches-free inputs at create time
15
+ -- (needs_migration, and whether the owning goal's scope wall reaches protected
16
+ -- territory). That fixes every task created from now on. This migration is the
17
+ -- one-time pass over the rows that already exist.
18
+ --
19
+ -- SCOPED TO STILL-CLAIMABLE WORK, on purpose. Only 'backlog' and 'ready' rows are
20
+ -- touched. The floor is enforced at CLAIM time (the INSUFFICIENT_RANK rung of the
21
+ -- claim refusal ladder), so raising it on an already-claimed, shipped or
22
+ -- abandoned row changes no decision that has not already been made — it would
23
+ -- only rewrite history and dirty updated_at across the ledger.
24
+ --
25
+ -- RAISES ONLY, like every other path that writes this column. The WHERE clause
26
+ -- selects requires_rank='xenos', so a row an Archon has already lifted to 'metic'
27
+ -- or 'archon' is left exactly as it is. There is no down-migration for the same
28
+ -- reason ADR 0085 gave: lowering a safety floor is never automatic.
29
+ --
30
+ -- THE MODULE LIST BELOW IS A POINT-IN-TIME SNAPSHOT, not a second source of
31
+ -- truth. The live authority is src/bongos/module-scope-map.scopeIncludesProtected
32
+ -- over PROTECTED_GLOBS (permission-path-check.js) — the same roster the pre-push
33
+ -- hook, the grader and main-audit read, and what the running code calls. A
34
+ -- migration cannot call into JS, and a historical backfill SHOULD record the
35
+ -- roster as it stood the day it ran rather than drift with a later one, so the
36
+ -- nine protected keys are spelled here once and never read again.
37
+
38
+ UPDATE tasks t
39
+ SET requires_rank = 'metic',
40
+ updated_at = now()
41
+ WHERE t.requires_rank = 'xenos'
42
+ AND t.status IN ('backlog', 'ready')
43
+ AND (
44
+ -- The migration core, by the task's own declaration.
45
+ t.needs_migration = true
46
+ -- Or the owning goal's scope wall reaches protected territory.
47
+ OR EXISTS (
48
+ SELECT 1
49
+ FROM goals g
50
+ WHERE g.id = t.goal_id
51
+ AND g.scope_modules && ARRAY[
52
+ 'kernel', 'lifecycle', 'grading', 'onboarding', 'builder-settings',
53
+ 'autonomy', 'government', 'dev-box', 'devsecops'
54
+ ]::text[]
55
+ )
56
+ );
@@ -370,7 +370,18 @@
370
370
  function loadRot() {
371
371
  return getJSON(`${API}/inbox/rotting`)
372
372
  .then((data) => renderRot(data))
373
- .catch(() => { /* signed out, older server, or a hiccup: never break the hall */ });
373
+ .catch((err) => {
374
+ // STILL FAIL-OPEN — a rot feed that cannot load must never break the hall,
375
+ // and being signed out or on an older server are both ordinary. But it now
376
+ // SAYS SO (task 1003747). The bare `.catch(() => {})` this replaces is how
377
+ // the feed stayed dead in production for releases: /inbox/rotting was being
378
+ // shadowed by another module's `/inbox/:id` and answering 400 bad_id, and
379
+ // this handler swallowed it into an empty section that looked like "nothing
380
+ // is rotting". A silent catch on the ONLY caller of a route is indistin-
381
+ // guishable from a healthy empty state — which is the whole failure.
382
+ console.warn('[hall] rot feed unavailable — the "Going quiet" section is empty '
383
+ + 'because the request failed, NOT because nothing is rotting:', err);
384
+ });
374
385
  }
375
386
 
376
387
  // task 2169: this widget IS the "Needs your attention" card — ONE capped
@@ -526,13 +526,41 @@ module.exports = function buildInboxRouter() {
526
526
  return res.json({ builder_id: builderId, ideas, total, limit, offset });
527
527
  }, { errorCode: 'idea_trail_failed' }));
528
528
 
529
+ // THE `(\d+)` ON THE TWO BARE-ID ROUTES BELOW IS LOAD-BEARING, NOT DECORATION
530
+ // (task 1003747). An idea id is always a positive integer, and constraining the
531
+ // param to digits is what stops these routes from swallowing a LITERAL
532
+ // `/inbox/<word>` contributed by ANOTHER module. It swallowed exactly that:
533
+ // `GET /inbox/rotting` (the rot feed, modules/lifecycle/routes/rot.js) answered
534
+ // 400 `bad_id` in production, because mountModuleRoutes
535
+ // (src/module-loader/loader.js) mounts modules in discovery order — `ideas`
536
+ // before `lifecycle` — so this matcher saw "rotting", failed to parse it as an
537
+ // id, and ended the request before the rot handler was ever reached. The whole
538
+ // 30-day rot cadence (ADR 0232) was dead and silent for releases.
539
+ //
540
+ // Declaring literals before `:id` (the convention modules/ideas/CLAUDE.md
541
+ // records) only fixes collisions INSIDE this file; it cannot reach a route
542
+ // another module contributes. The digit constraint can, because it makes the
543
+ // non-numeric path fall THROUGH to whatever is mounted later, whatever the
544
+ // order. `scripts/gds/route-shadow-guard.js` fails the build on any remaining
545
+ // pair of this shape, and gen-api-docs strips the constraint so it never
546
+ // reaches the published spec.
547
+ //
548
+ // Trade-off, taken deliberately: `/inbox/abc` now 404s ("no such route")
549
+ // instead of 400 `bad_id`. No test pinned that 400, and 404 is the more honest
550
+ // answer for a path that matches no route.
551
+ //
552
+ // The blank line below is deliberate: commentBlockAbove() in gen-api-docs.js
553
+ // walks upward from the route and stops at it, so THIS rationale stays in the
554
+ // source for the next reader without becoming the route's public API
555
+ // description.
556
+
529
557
  // rank: any-builder — single idea read for the "#NNN" deep-link resolver
530
558
  // (#1047). Mirrors GET /blockers/:id. The hall resolves /#/idea/N by id, but
531
559
  // the /inbox LIST returns only the open-first-50, so a closed/older idea ref
532
560
  // would dead-end at a "no such idea" card. getIdea() reads any idea by id.
533
561
  // Authed: a logged-out resolve hits the hall's softAuth "sign in" card, so no
534
562
  // idea text is exposed publicly (the linked refs are public, the bodies aren't).
535
- router.get('/inbox/:id', api.requireBuilder, async (req, res) => {
563
+ router.get('/inbox/:id(\\d+)', api.requireBuilder, async (req, res) => {
536
564
  const id = parseId(req, res, { code: 'bad_id', positiveInt: true });
537
565
  if (id === null) return;
538
566
  try {
@@ -1025,7 +1053,13 @@ module.exports = function buildInboxRouter() {
1025
1053
  return res.json({ ok: true, idea: result.idea, delta: result.delta, unchanged: !result.delta });
1026
1054
  }, { errorCode: 'idea_body_update_failed' }));
1027
1055
 
1028
- router.patch('/inbox/:id', api.requireBuilder, api.requirePermission('idea.triage'), async (req, res) => {
1056
+ // `(\d+)` for the same reason as the GET above — see that comment. No PATCH
1057
+ // literal collides today; the constraint is here so the pair of bare-id routes
1058
+ // on this path stay symmetric and a future `PATCH /inbox/<word>` cannot be
1059
+ // quietly eaten. Kept above a blank line so it stays out of the published API
1060
+ // description, same as the GET's rationale.
1061
+
1062
+ router.patch('/inbox/:id(\\d+)', api.requireBuilder, api.requirePermission('idea.triage'), async (req, res) => {
1029
1063
  if (validateOrRespond(req, res, {
1030
1064
  // Enum kept light — inbox.updateIdea returns a structured BAD_STATUS
1031
1065
  // error if the value falls outside the accepted set, and the supervisor
@@ -146,7 +146,7 @@ function rankAllowsTask(builderRank, requiredRank) {
146
146
  return have >= needed;
147
147
  }
148
148
 
149
- // deriveRequiredRank — task 1498 / ADR 0085. The per-task requires_rank floor
149
+ // deriveRequiredRank — task 1498 / ADR 0084. The per-task requires_rank floor
150
150
  // (V3.R82 #301) shipped INERT: no create/edit path ever set it, so every task
151
151
  // sat at the 'xenos' default and the claim gate never bit. We now DERIVE a floor
152
152
  // from what a task touches + its sensitivity, reusing the SAME protected-path
@@ -157,10 +157,35 @@ function rankAllowsTask(builderRank, requiredRank) {
157
157
  // sub-Metic builder already cannot push to). Everything else stays 'xenos' (the
158
158
  // open general queue). NEVER auto-assigns 'archon': "only the Archon may claim
159
159
  // this" is a deliberate manual override, not something to infer.
160
+ //
161
+ // task 1003663 / ADR 0270 — WHY THERE ARE NOW FOUR INPUTS, NOT TWO. The floor
162
+ // above derived from touches[] alone, and ADR 0049 then retired PREDICTIVE
163
+ // touches[]: it is backfilled from the real committed diff at SHIP, which is
164
+ // after the claim this floor exists to gate. So the derivation quietly went
165
+ // inert on the vectors that omit it — 68 of the 88 tasks declaring
166
+ // needs_migration sat at 'xenos', including a live-DB migration rewriting the
167
+ // auth token store. The fix is NOT to bring prediction back; it is to derive
168
+ // from facts a task already carries at create time. `opts` takes those:
169
+ // { needsMigration, scopeProtected }.
160
170
  // Pure + exported so tests/rank_gate.mjs can exercise it without a DB.
161
171
 
162
- function deriveRequiredRank(touches, securitySensitive) {
172
+ function deriveRequiredRank(touches, securitySensitive, opts = {}) {
163
173
  if (securitySensitive === true) return 'metic';
174
+ // task 1003663 / ADR 0270 — the two touches-FREE inputs. ADR 0049 retired
175
+ // PREDICTIVE touches[]: it is backfilled from the real diff at ship, so at
176
+ // CREATE time the matchProtected() arm below sees an empty array on most
177
+ // vectors and the floor never rises. These arms restore it from facts a task
178
+ // already carries at create time.
179
+ //
180
+ // needsMigration — the direct successor to the 'migrations/' entry POST /tasks
181
+ // used to fold into touches[] before it became a dedicated boolean. A task
182
+ // that alters the LIVE database is the migration core by definition.
183
+ if (opts.needsMigration === true) return 'metic';
184
+ // scopeProtected — the owning goal's scope wall reaches protected territory.
185
+ // Unlike touches[] this is ALWAYS available (goal_id is required on every task
186
+ // since BV1.R60), and it is computed from the SAME protected roster the
187
+ // pre-push hook, the grader and main-audit read, so it cannot drift from them.
188
+ if (opts.scopeProtected === true) return 'metic';
164
189
  const files = Array.isArray(touches) ? touches : [];
165
190
  if (files.length > 0 && matchProtected(files).length > 0) return 'metic';
166
191
  return 'xenos';