@bongos/core 1.19.920 → 1.19.922

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.
Files changed (32) hide show
  1. package/.bongos-core.json +71 -31
  2. package/.claude/skills/grade-audit/SKILL.md +9 -0
  3. package/docs/adr/0320-an-empty-diff-is-not-an-ungraded-diff.md +159 -0
  4. package/docs/adr/0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md +163 -0
  5. package/docs/adr/README.md +2 -0
  6. package/docs/module-api-changelog.md +4 -0
  7. package/modules/economy/idea-brightness.js +212 -0
  8. package/modules/economy/idea-size-historical.js +22 -3
  9. package/modules/economy/idea-size.js +30 -3
  10. package/modules/grading/grader-no-artifact.js +347 -0
  11. package/modules/grading/grader.js +6 -0
  12. package/modules/lifecycle/cascade.js +36 -0
  13. package/modules/lifecycle/migrations/lifecycle_010_removal_targets.sql +116 -0
  14. package/modules/lifecycle/module.json +2 -1
  15. package/modules/lifecycle/removal-targets.js +217 -0
  16. package/modules/lifecycle/routes/lifecycle.js +9 -0
  17. package/modules/lifecycle/routes/removal-proposals.js +33 -0
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/scripts/gds/provision-core-upgrade.js +26 -2
  21. package/scripts/gds/run-unit-tests.js +17 -1
  22. package/scripts/gds/ship-flow.js +54 -7
  23. package/scripts/gds/ship-grade-steps.js +43 -5
  24. package/scripts/gds/ship-grade.js +36 -0
  25. package/src/module-api.js +1 -1
  26. package/tests/core_upgrade_runner.mjs +56 -0
  27. package/tests/git_merge_regen.mjs +87 -3
  28. package/tests/grader_no_artifact.mjs +360 -0
  29. package/tests/grader_prompt_fencing.mjs +46 -0
  30. package/tests/idea_brightness.mjs +550 -0
  31. package/tests/no_artifact_escalation.mjs +13 -3
  32. package/tests/skip_is_not_pass.mjs +91 -1
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.920",
6
- "core_contract": "1.19.920",
7
- "source_commit": "f4047b39eee17385567cbe07c45821af322b8f51",
5
+ "core_version": "1.19.922",
6
+ "core_contract": "1.19.922",
7
+ "source_commit": "b57f79a73cefa42840ff10d77aab958af1fe4ed4",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-20T19:16:17.763Z",
9
+ "built_at": "2026-09-20T20:40:52.111Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 508,
12
+ "docs_redacted": 510,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2303,
14
+ "functional_verbatim": 2309,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2837,
20
- "tree_sha256": "0aa0c6f824621e1dd477c005962f961b6c9e71756b59f6bf383b122152964b5b",
19
+ "file_count": 2845,
20
+ "tree_sha256": "36516d370c40955c53b7b0b70845d49e03d34aaecf7d5e58a54bf78ed454f694",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -152,7 +152,7 @@
152
152
  {
153
153
  "path": ".claude/skills/grade-audit/SKILL.md",
154
154
  "mode": "0000644",
155
- "sha256": "5aa0804e6b4162f0dd0f5608e8a0964890e680e0f7cefe4fe2efdacc7ddbc340"
155
+ "sha256": "c32df53b87b71a15e6b6e605c0c9698a07ff99a94bc6325fd99648d9c2d38122"
156
156
  },
157
157
  {
158
158
  "path": ".claude/skills/grade-recover/SKILL.md",
@@ -2089,10 +2089,20 @@
2089
2089
  "mode": "0000644",
2090
2090
  "sha256": "2fbb3e040b55162284242f545af76b1254fafbecfe042b8c792dc0dca475c4f8"
2091
2091
  },
2092
+ {
2093
+ "path": "docs/adr/0320-an-empty-diff-is-not-an-ungraded-diff.md",
2094
+ "mode": "0000644",
2095
+ "sha256": "e582b4ece7a5b0acf4392d8b3d4942d55b52dc487d7c6a7c3a159203485ec633"
2096
+ },
2097
+ {
2098
+ "path": "docs/adr/0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md",
2099
+ "mode": "0000644",
2100
+ "sha256": "3a743808de440eebd5fcdd74a4c88e41acc82cc02ce5632cb392b2aa0c05b48e"
2101
+ },
2092
2102
  {
2093
2103
  "path": "docs/adr/README.md",
2094
2104
  "mode": "0000644",
2095
- "sha256": "087b5151c182476c1f541e239776099319ad7fa95e6ebc2dc5ccf2b320c84f0c"
2105
+ "sha256": "caece332e048f6ee15517740255c4d715efc025c82756ac095ccde7814b60daa"
2096
2106
  },
2097
2107
  {
2098
2108
  "path": "docs/api-reference.md",
@@ -2662,7 +2672,7 @@
2662
2672
  {
2663
2673
  "path": "docs/module-api-changelog.md",
2664
2674
  "mode": "0000644",
2665
- "sha256": "760c792a9c7d16c1b9fc1b77eaf915baf0d9e652d04c84571118c521bbde0700"
2675
+ "sha256": "d657d8b2fc7eb2bb4379a22a41d95b835ac655848626120a2218ab7603df5313"
2666
2676
  },
2667
2677
  {
2668
2678
  "path": "docs/modules-contract.md",
@@ -4224,15 +4234,20 @@
4224
4234
  "mode": "0000644",
4225
4235
  "sha256": "736eed2c89863d6eb28072627a40ac4f8945832cfb7b1d1aba10e6cc65483901"
4226
4236
  },
4237
+ {
4238
+ "path": "modules/economy/idea-brightness.js",
4239
+ "mode": "0000644",
4240
+ "sha256": "745da28c27ebcd50b349c3fe027f7e3f14888f9e012e0c9443656de249444ba1"
4241
+ },
4227
4242
  {
4228
4243
  "path": "modules/economy/idea-size-historical.js",
4229
4244
  "mode": "0000644",
4230
- "sha256": "f99f62eb4eaeda10b32284dbe27f87e64d5b6b82c9eb4aa442057d0de13be761"
4245
+ "sha256": "215eefac80aaf3624a8ff5d5284f29388a2e1b492424dd152b3d9bbe074e3a01"
4231
4246
  },
4232
4247
  {
4233
4248
  "path": "modules/economy/idea-size.js",
4234
4249
  "mode": "0000644",
4235
- "sha256": "d1fa5fda6b34bd2a8cf8d563c780bc4b12f1cdfb955e13f51799a589609ac1c0"
4250
+ "sha256": "8818bea53d2ce90fe0bc98b53bfed1af11c4eeb541942b96c4cf50a162791634"
4236
4251
  },
4237
4252
  {
4238
4253
  "path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
@@ -4514,6 +4529,11 @@
4514
4529
  "mode": "0000644",
4515
4530
  "sha256": "597f883d4878a8fb585585331e11dc219e8a57d1459be4039116b60e7fdc556d"
4516
4531
  },
4532
+ {
4533
+ "path": "modules/grading/grader-no-artifact.js",
4534
+ "mode": "0000644",
4535
+ "sha256": "ab20c06268f30d0ed7198774d7435237f63440550235bdc5b926870db56886e7"
4536
+ },
4517
4537
  {
4518
4538
  "path": "modules/grading/grader-prompt.js",
4519
4539
  "mode": "0000644",
@@ -4592,7 +4612,7 @@
4592
4612
  {
4593
4613
  "path": "modules/grading/grader.js",
4594
4614
  "mode": "0000644",
4595
- "sha256": "1234bb3c9938fd4cf71c39e4660d69c56a27de667ee7ff09b0f865374b6beba4"
4615
+ "sha256": "968363ed43dfd3792462996b061e90cb7af30d9c31a6d6bd39e68ec812b47706"
4596
4616
  },
4597
4617
  {
4598
4618
  "path": "modules/grading/module.json",
@@ -5512,7 +5532,7 @@
5512
5532
  {
5513
5533
  "path": "modules/lifecycle/cascade.js",
5514
5534
  "mode": "0000644",
5515
- "sha256": "fd8aeec221eede847aa3b0047c01721ac7e2076e95675a71a117eff3bf3bbc55"
5535
+ "sha256": "176f7463d6f3c7bf2297760d0934f88528ee180dfb0c9e49a4347a449af2820d"
5516
5536
  },
5517
5537
  {
5518
5538
  "path": "modules/lifecycle/check-rollup.js",
@@ -5754,6 +5774,11 @@
5754
5774
  "mode": "0000644",
5755
5775
  "sha256": "e98efbb1537f0d99adb6b6b66c1fdccfd4bbac16b97362d479af903eb9de492c"
5756
5776
  },
5777
+ {
5778
+ "path": "modules/lifecycle/migrations/lifecycle_010_removal_targets.sql",
5779
+ "mode": "0000644",
5780
+ "sha256": "e3e29e791eeb94ab1380ae2e2a13eafaea413faf0d0324a4e2007ab2b3002fca"
5781
+ },
5757
5782
  {
5758
5783
  "path": "modules/lifecycle/mingle.js",
5759
5784
  "mode": "0000644",
@@ -5762,7 +5787,7 @@
5762
5787
  {
5763
5788
  "path": "modules/lifecycle/module.json",
5764
5789
  "mode": "0000644",
5765
- "sha256": "01f96d398408491bc4f1d9357c23c259f16e52184569849a0caf775f5b38836b"
5790
+ "sha256": "36a03e5d8a4fc1129439356ba6daaf284d2cd2ca3d98d35f990e3215ff11198a"
5766
5791
  },
5767
5792
  {
5768
5793
  "path": "modules/lifecycle/pg-constraint-fail.js",
@@ -5794,6 +5819,11 @@
5794
5819
  "mode": "0000644",
5795
5820
  "sha256": "8270ce55255998a4f0bbf574a54df0ba2597013c727b4afb1ce2abad9c9d4d2a"
5796
5821
  },
5822
+ {
5823
+ "path": "modules/lifecycle/removal-targets.js",
5824
+ "mode": "0000644",
5825
+ "sha256": "e80304e7b43fe86852763e2f859bc05c1b88bb50a3eea407342ae6d46b8f9eda"
5826
+ },
5797
5827
  {
5798
5828
  "path": "modules/lifecycle/rot.js",
5799
5829
  "mode": "0000644",
@@ -5862,7 +5892,7 @@
5862
5892
  {
5863
5893
  "path": "modules/lifecycle/routes/lifecycle.js",
5864
5894
  "mode": "0000644",
5865
- "sha256": "f4b080286870b6cd52cd59c5cb7c5a5127c3b0a90df50b7d87495d684767203b"
5895
+ "sha256": "330e42fad79812a05a90256fe31931df2156093b80a648d7c838ecac136c91e4"
5866
5896
  },
5867
5897
  {
5868
5898
  "path": "modules/lifecycle/routes/mingle.js",
@@ -5882,7 +5912,7 @@
5882
5912
  {
5883
5913
  "path": "modules/lifecycle/routes/removal-proposals.js",
5884
5914
  "mode": "0000644",
5885
- "sha256": "329f130d3556dc05981ce6b4b6ce4d55984c6ca42fd0f421fd70cba63eb277ac"
5915
+ "sha256": "e0a0e14a8818c011334e8f61a50c8dd7ad11b4387acfd6ab9e18e570f54ec6c4"
5886
5916
  },
5887
5917
  {
5888
5918
  "path": "modules/lifecycle/routes/rot.js",
@@ -7872,12 +7902,12 @@
7872
7902
  {
7873
7903
  "path": "package-lock.json",
7874
7904
  "mode": "0000644",
7875
- "sha256": "c4064939e0c8b7f6452d0348427109332caa61e8759e40a60ded3dc4f704602a"
7905
+ "sha256": "0169c3fafa67ebf63c9ae35566e8104bcf210ae80973c8e1837874109fe85354"
7876
7906
  },
7877
7907
  {
7878
7908
  "path": "package.json",
7879
7909
  "mode": "0000644",
7880
- "sha256": "840b50ec98565b70f1550ea34aa8813bf5e8cbb8ae7dece18ed1e45abe6ad608"
7910
+ "sha256": "b62dd828c8e8b167dfc4bca7d54fdec8c76b13f89a44e61365d48962abf77ba9"
7881
7911
  },
7882
7912
  {
7883
7913
  "path": "public-docs/index.html",
@@ -8822,7 +8852,7 @@
8822
8852
  {
8823
8853
  "path": "scripts/gds/provision-core-upgrade.js",
8824
8854
  "mode": "0000644",
8825
- "sha256": "5fad0beb0a3d4b322918a99765cb922fcce1de4196c19cb08a0d3bc91c3d149e"
8855
+ "sha256": "1fd6e38184bb0943fdc22e314b9e6cad0b5ebeae7f7370992adbd3b4f2ad5a9e"
8826
8856
  },
8827
8857
  {
8828
8858
  "path": "scripts/gds/provision-disconnect.js",
@@ -8977,7 +9007,7 @@
8977
9007
  {
8978
9008
  "path": "scripts/gds/run-unit-tests.js",
8979
9009
  "mode": "0000644",
8980
- "sha256": "a40a90ba6255820130aa6a94ddc314b643177e4a153750dc6290550a75e1a4e8"
9010
+ "sha256": "d3567553fb963ad78c0ab36dd283fbf5c46e188dd7538e00fed36442f7ab2515"
8981
9011
  },
8982
9012
  {
8983
9013
  "path": "scripts/gds/runner-drift.js",
@@ -9212,7 +9242,7 @@
9212
9242
  {
9213
9243
  "path": "scripts/gds/ship-flow.js",
9214
9244
  "mode": "0000644",
9215
- "sha256": "4b8179e5c1aae3fc4845457a4089c57f6cf105c33d03d21d3ade99776371e50f"
9245
+ "sha256": "b6198e134156b755bcffa3c385c9b2ec780dcd7681791c1d3fb938217bd8102b"
9216
9246
  },
9217
9247
  {
9218
9248
  "path": "scripts/gds/ship-git.js",
@@ -9222,12 +9252,12 @@
9222
9252
  {
9223
9253
  "path": "scripts/gds/ship-grade-steps.js",
9224
9254
  "mode": "0000644",
9225
- "sha256": "158a01214499cd229a51819cd2dfcfee2eae34475f87f23795f81ec83d54f337"
9255
+ "sha256": "204834c41dd184ebc076dcd57d6a5370a910f4fc9c276bb3984b08b43f550012"
9226
9256
  },
9227
9257
  {
9228
9258
  "path": "scripts/gds/ship-grade.js",
9229
9259
  "mode": "0000644",
9230
- "sha256": "b1fb38635433b579fbb38d65e92b9db226cccb753866ab7a2a15227796be66cd"
9260
+ "sha256": "40c6a9f6aa6a75cdf8a5e41bfa8318766636cba64e47aee870c26a719676713d"
9231
9261
  },
9232
9262
  {
9233
9263
  "path": "scripts/gds/ship-honesty.js",
@@ -9877,7 +9907,7 @@
9877
9907
  {
9878
9908
  "path": "src/module-api.js",
9879
9909
  "mode": "0000644",
9880
- "sha256": "111908611a709491751fd92695758cca7125efc22bf5e68e908a8c4f45cf6301"
9910
+ "sha256": "86c69951f0059e5374245ef798c5cd24cd82d836a4ccf706e80f186de986b473"
9881
9911
  },
9882
9912
  {
9883
9913
  "path": "src/module-loader/catalog.js",
@@ -10792,7 +10822,7 @@
10792
10822
  {
10793
10823
  "path": "tests/core_upgrade_runner.mjs",
10794
10824
  "mode": "0000644",
10795
- "sha256": "a8fc77b7a32e4377b4b327bd485cbc17428421ef3fc871770fdaeee3e02e48f4"
10825
+ "sha256": "5d0b4fa1ec48d40ebc801902b5279475533ae2dc8819e587bb95bc17783cba06"
10796
10826
  },
10797
10827
  {
10798
10828
  "path": "tests/cosmos_token_contrast.mjs",
@@ -11217,7 +11247,7 @@
11217
11247
  {
11218
11248
  "path": "tests/git_merge_regen.mjs",
11219
11249
  "mode": "0000644",
11220
- "sha256": "a55ae255245a53429cc7948e35ea0515bf58307f826d605d6ffd52cc60196ef8"
11250
+ "sha256": "79208b90f1b785af56ab057c858e96cea4e67ebf247858457addedce55e71b06"
11221
11251
  },
11222
11252
  {
11223
11253
  "path": "tests/github_push_ancestry.mjs",
@@ -11649,6 +11679,11 @@
11649
11679
  "mode": "0000644",
11650
11680
  "sha256": "1823c173f811d52d93b63201d0ebb0d85f2b1e42a569e6331580ef097f5c05cc"
11651
11681
  },
11682
+ {
11683
+ "path": "tests/grader_no_artifact.mjs",
11684
+ "mode": "0000644",
11685
+ "sha256": "c8450702a2d805d1cf5e826a3dedf49fff6cd01c6ca540fa9c64444fc874d5d3"
11686
+ },
11652
11687
  {
11653
11688
  "path": "tests/grader_panel.mjs",
11654
11689
  "mode": "0000644",
@@ -11657,7 +11692,7 @@
11657
11692
  {
11658
11693
  "path": "tests/grader_prompt_fencing.mjs",
11659
11694
  "mode": "0000644",
11660
- "sha256": "fa155f3c5faa3763d1a51d56b7746ad870e826e3249f5e9b08108a3cfbfcbd72"
11695
+ "sha256": "b626e614de000078eef1b305f6322d94326f032bcbeba2808baaa9dcc8ed3103"
11661
11696
  },
11662
11697
  {
11663
11698
  "path": "tests/grader_quality_window_overflow.mjs",
@@ -11879,6 +11914,11 @@
11879
11914
  "mode": "0000644",
11880
11915
  "sha256": "0d8f07c64f4d17c8c894b2d6395281882efd4e186c5714fe9aab61e2808d4314"
11881
11916
  },
11917
+ {
11918
+ "path": "tests/idea_brightness.mjs",
11919
+ "mode": "0000644",
11920
+ "sha256": "d67d60b1236ccedd01591356f5a5a9a72e4f1efd895ddf5af00c744219a2b3b6"
11921
+ },
11882
11922
  {
11883
11923
  "path": "tests/idea_capture_grades.mjs",
11884
11924
  "mode": "0000644",
@@ -12507,7 +12547,7 @@
12507
12547
  {
12508
12548
  "path": "tests/no_artifact_escalation.mjs",
12509
12549
  "mode": "0000644",
12510
- "sha256": "3d2b03a39cbefc02565a190dee0612e14576edbf32ea0fccf798d6ed28fee8f1"
12550
+ "sha256": "f1523ee48247d18c7326b9aa0b442ca2c9fab8bd41dbe9f23a4cb89e1986bee0"
12511
12551
  },
12512
12552
  {
12513
12553
  "path": "tests/oauth_secret.mjs",
@@ -13682,7 +13722,7 @@
13682
13722
  {
13683
13723
  "path": "tests/skip_is_not_pass.mjs",
13684
13724
  "mode": "0000644",
13685
- "sha256": "6258b4a3ad76a1d9139b053535c173760c5b42aa82510541501b6fef5ddd1fe1"
13725
+ "sha256": "db9e246f1ce7ef6306b938d325c450e31f2e834244f64dd2f0aaea96e760d987"
13686
13726
  },
13687
13727
  {
13688
13728
  "path": "tests/smoke_preflight.mjs",
@@ -20,6 +20,15 @@ node scripts/gds/api.js GET "/api/bongos/tasks/<id>?include=grade"
20
20
 
21
21
  Bucket by grade shape: `passed`, failed, `signals.panel_outcome='unavailable'` (outage — no quality signal existed), or no grade row at all.
22
22
 
23
+ **Then split off the no-artifact species before any leg reads the bucket** (task 1004011, ADR 0320). A grade with `signals.no_artifact_grade === true` judged the builder's **claim that nothing was needed** — the handoff notes and value summary against the task description — not a diff. There is no diff, no panel, and one generalist shot (`signals.lenses_dropped` names the four lenses that did not run). It is a real grade and a real gate, but it measures a different thing, so:
24
+
25
+ - **Do not average or count it alongside diff grades.** A 7.5 over three paragraphs of prose and a 7.5 over a 400-line diff are not the same number.
26
+ - **Leg 2 still applies, and matters more here.** A `blocker`/`major` on a no-artifact pass usually means the *evidence* was thin — queue it the same way.
27
+ - **Leg 3 does not apply** — there is no merged diff to re-read for runtime behaviour. The equivalent question, worth asking on any no-artifact pass that shipped on assertion alone, is: *could a reader today re-run or re-read what the notes claim?* If not, queue it.
28
+ - **A no-artifact FAIL is not an override candidate for Leg 1** unless it also landed. The builder's remedy is better evidence, not a permission.
29
+
30
+ Note `task_grades.grader_kind` is **not** the discriminator: the server records `'subagent'` for everything arriving through `POST /tasks/:id/grade`. `signals.no_artifact_grade` is the durable marker.
31
+
23
32
  ## Leg 1 — the override ledger
24
33
 
25
34
  For every shipped task whose grade **failed** (or is absent), the land needed an override. Cross-reference the ledger (Archon — `override_request.decide`):
@@ -0,0 +1,159 @@
1
+ # ADR 0320 — An empty diff is not an ungraded diff: grade the claim instead
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-20
5
+ - **Task:** 1004011 · goal 1000118 · scoped out of task 1003950 (shape 2)
6
+ - **Supersedes nothing.** Narrows [ADR 0041](0041-temporary-grader-bypass-killswitch.md)'s reach: the `--allow-empty` close is no longer one of the things `task.confirm.grade_bypass` gates.
7
+ - **Drops** task 1004010, the alternative shape (a new permission atom for the same close). Owner decision, 2026-09-20 — see §5.
8
+
9
+ ## 1. Context
10
+
11
+ `scripts/gds/ship.js --allow-empty` is how a builder closes a task that produced no
12
+ committed artifact: the premise turned out to be false, the work was a pure
13
+ verification, the change landed directly in the database or on the host.
14
+
15
+ Until now that flag did two things at once. It set `skipGrade = true` — the subagent
16
+ grader never ran — and it therefore routed the close through
17
+ `POST /tasks/:id/confirm`, whose gate is `task.confirm.grade_bypass`, one of the four
18
+ trust-boundary keys [ADR 0157](0157-archon-is-rank-and-identity-only.md) names. In
19
+ practice a Metic could not close an artifact-free task at all without asking an Archon.
20
+
21
+ Task 1003950 shipped a terminating escalation on that refusal so the builder at least
22
+ stopped looping. It deliberately left the underlying shape alone and filed two
23
+ alternatives: give the close its own lower-floored permission atom (1004010), or remove
24
+ the need for a permission by grading something real (1004011, this one).
25
+
26
+ ## 2. The two things that were wrong
27
+
28
+ **The permission was the wrong question.** `task.confirm.grade_bypass` means *ratify
29
+ this past a grade that FAILED*. That is a genuine trust-boundary act: a verdict exists
30
+ and a human is overriding it. An `--allow-empty` close is not that. No verdict exists,
31
+ because there was nothing for the grader to read. Charging the same key for "it was
32
+ judged and rejected" and "there was nothing to judge" conflates a override with an
33
+ absence, and the cost of the conflation was that the cheapest, least scrutinised ship
34
+ in the system needed the most powerful key to perform.
35
+
36
+ **Nothing was being checked.** This is the more serious half. On a no-artifact ship the
37
+ grader was skipped *entirely*, so the assertions in the handoff notes — "the premise was
38
+ already fixed", "this was a pure ops verification", "the row was updated directly" —
39
+ went into the ledger unexamined. The ledger is the complete record of what happened to
40
+ the project (CLAUDE.md §8), and on exactly the ships where the record is the *only*
41
+ evidence, nothing read it. A permission gate is not a substitute for scrutiny: an Archon
42
+ clicking confirm is not the same as someone checking whether the claim is true.
43
+
44
+ ## 3. Decision
45
+
46
+ **On an `--allow-empty` ship, grade the NOTES (and the value summary) against the task
47
+ description, instead of skipping the grade.** The grader's question becomes: *is this
48
+ task genuinely artifact-free, and does the evidence given support that?* A pass is a
49
+ real passing grade that confirms through the ordinary rank-open path and needs no bypass
50
+ permission. A fail is a real fail, and the remedy is better evidence, not a key.
51
+
52
+ Four sub-decisions carry most of the weight:
53
+
54
+ **D1 — The rubric axes are re-asked, not replaced.** The gate downstream is
55
+ `functional_fidelity >= THRESHOLD AND code_quality >= THRESHOLD AND confidence >= 0.4`,
56
+ and `/grade-audit`, the aggregates and the cost model all read those columns. Inventing
57
+ a third pair of axis names would have meant teaching every one of them a synonym. So the
58
+ axes keep their names and the prompt re-asks them for this species:
59
+ `functional_fidelity` → *is this genuinely artifact-free?*; `code_quality` → *is the
60
+ evidence specific and checkable, or bare assertion?*. The gate arithmetic is untouched.
61
+
62
+ **D2 — Two generalist shots, not the four-lens panel.** The panel is Hacker / Quality /
63
+ Efficiency / (Reader) — lenses over code. Over three paragraphs of prose they are four
64
+ readings of the same short text at four times the cost, so the lenses go, recorded in
65
+ `signals.lenses_dropped` rather than left to be inferred from their absence (the
66
+ trivial-diff precedent, #513: lighter, never silent).
67
+
68
+ The panel's unanimous gate, however, was also the cross-check that makes injection
69
+ hard — an attacker must fool *every* gating worker. This decision's first implementation
70
+ kept a single shot and lost that property; the grader failed the ship on it, correctly
71
+ (see D4). So the species runs **two** independent readings of the same prompt, collapsed
72
+ by **unanimity, not median**: the worst score on each axis, and a pass only if every shot
73
+ passed. Under a median the collapse would be actively harmful — the median of two is
74
+ their mean, so a fooled 10 averages with a suspicious 3 into a passing 6.5 and the second
75
+ reading launders the first. A split verdict is recorded as `signals.shot_dissent`, since
76
+ two readings of the same fenced text disagreeing is itself worth a human's eye.
77
+
78
+ The collapse covers **all three gate axes, `confidence` included** — a point the second
79
+ round of grading caught, because the first fix collapsed only `functional_fidelity` and
80
+ `code_quality`. Confidence is easy to miss: it is the one gate axis that lives in
81
+ `signals` rather than `rubric_scores`. Leaving it at the median would have reintroduced
82
+ the same laundering on the axis where it matters most, since the server's
83
+ `validateAndScore` derives `lowConfidence` purely from the submitted value and never sees
84
+ the client's `unanimous` boolean — a shot honestly reporting 0.2 ("I cannot judge this
85
+ claim", the value that exists to fall through to a human) would average with a confident
86
+ 0.9 and clear the 0.4 gate at 0.55.
87
+
88
+ **D4 — Every builder-controlled field is scrubbed and fenced (SR-15 / #995).** The panel
89
+ workers have wrapped the diff, title, description and value summary in
90
+ `grader-workers/fence.js` since #995, because a builder can embed steering text in their
91
+ own artifact. This species carries the same inputs and is the worse case on both sides of
92
+ the ledger: the notes are not context around an artifact the model can independently
93
+ check, they *are* the evidence; and a successful injection does not merely flip one of
94
+ four workers, it closes a task past the scrutiny this ADR introduced to replace the
95
+ Archon-only gate. So fencing here is not defence in depth, it is the boundary. The
96
+ no-follow rule is also restated *after* the artifact, because the notes may run to 24,000
97
+ characters and a rule stated once at the top is a long way behind by the end of an
98
+ adversarial payload.
99
+
100
+ **D3 — The species marker lives in `signals`, not in `grader_kind`.** The server's
101
+ `validateAndScore` hardcodes `task_grades.grader_kind` to `'subagent'` for everything
102
+ arriving through `POST /tasks/:id/grade`, so the column cannot carry the distinction
103
+ without teaching every consumer of that enum a new member. `signals.no_artifact_grade`
104
+ is submitted verbatim and stored, so it is the durable discriminator.
105
+ `signals.graded_subject = 'notes'` says what was read.
106
+
107
+ ## 4. Consequences
108
+
109
+ - A Metic can now close a genuinely artifact-free task on the strength of a graded
110
+ claim. `task.confirm.grade_bypass` keeps exactly the meaning ADR 0041 gave it and
111
+ loses a use it never should have had.
112
+ - **A second grade species exists in `task_grades`.** Every consumer that averages,
113
+ counts or spot-checks grades must split on `signals.no_artifact_grade` first: a score
114
+ over prose and a score over a 400-line diff are not the same measurement.
115
+ `/grade-audit` is updated with that rule in this change.
116
+ - **No-artifact closes get harder, not easier, for the dishonest.** The permission wall
117
+ was indiscriminate — it stopped the honest Metic and waved through any Archon. The
118
+ grade is discriminating: it reads the claim. "Confirmed working" with nothing a reader
119
+ could re-run now fails an axis that previously did not exist.
120
+ - **The diff-hash reuse key is deliberately not set** for this species. Hashing the empty
121
+ diff would let the *next* empty-diff ship of the same task reuse this grade — a
122
+ different claim, different notes, silently inheriting a pass it never earned.
123
+ - **Below the protected-path floor, nothing changes.** The server's Guard 1b (task
124
+ 1002646) fails a passing grade closed when a below-floor builder submits no
125
+ `committed_files`, and a no-artifact grade submits none by definition. That guard is
126
+ correct and untouched: for a sub-floor builder the permission-path check is genuinely
127
+ unprovable. This decision moves the Metic-and-above case, which is the case that was
128
+ filed.
129
+ - `--skip-grade` is unchanged and still reaches the Archon gate. The escalation split
130
+ task 1003950 added still decides which refusal prints there.
131
+
132
+ ## 5. Alternatives considered
133
+
134
+ **Task 1004010 — a new permission atom for the no-artifact close.** The sibling shape,
135
+ and the one this drops. It would have added a lower-floored relative of a trust-boundary
136
+ key, plus a governance migration and a `system: true` decision of its own. Rejected on
137
+ two grounds: it answers "who may skip the check" when the better question is "why is
138
+ there no check", and it permanently widens the permission surface — the same class of
139
+ change the owner declined three days earlier for `version.create` (task 1003853,
140
+ [ADR 0250](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) §6a).
141
+ Solving the problem without a new atom is the consistent call. The row is closed, with
142
+ its problem statement kept should this fail to land.
143
+
144
+ **Keep skipping the grade, lower the floor on the existing key.** Cheapest, and wrong
145
+ for the same reason as 1004010: it treats the gate as the point. Nothing would read the
146
+ notes.
147
+
148
+ **Grade the notes with the full panel.** Rejected under D2 — four code lenses over prose
149
+ is cost without signal. Note what the rejection had to preserve separately: the panel's
150
+ value here was never its lenses, it was the unanimity of its gate, and D2 keeps that with
151
+ two shots rather than four lenses.
152
+
153
+ **A single generalist shot.** This decision's first implementation, and wrong for a
154
+ reason that only became visible once the prompt was written: it leaves one reading
155
+ between a crafted set of handoff notes and a closed task. Corrected before landing.
156
+
157
+ **A new pair of rubric axes for prose.** Rejected under D1 — honest about the
158
+ measurement, but it moves the gate arithmetic and every consumer of it, for a naming
159
+ gain.
@@ -0,0 +1,163 @@
1
+ # ADR 0321 — Brightness is a payout-weighted share, and a removal's targets are rows
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-20
5
+ - **Task:** [task 1004099](https://cloudbongos.com/builders#/task/1004099) (BV2.R16, goal 1000095 — BONGOS-V2)
6
+ - **Spec:** `docs/specs/<redacted>.md` decisions 4 and 20
7
+ - **Under:** [ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md), [ADR 0319](0319-a-removal-verdict-is-a-row-beside-the-question.md)
8
+
9
+ ## Context
10
+
11
+ An idea's **size** is its total ideator payout from its descent and never falls
12
+ ([ADR 0312](0312-provenance-is-a-second-ideas-owned-edge-table.md) and
13
+ `modules/economy/idea-size.js`). **Brightness** is the other channel: how much of that
14
+ work is still alive. The pair is what lets a thought whose version the project later tore
15
+ out draw *large and dark* — visibly different from a thought that never caused anything
16
+ and draws *small and dark*. One number cannot express both, which is why there are two.
17
+
18
+ Two things had to be settled before it could be computed.
19
+
20
+ **First, what a share is a share OF.** The spec says graded, not binary (decision 20): a
21
+ descent of five things with three alive is partly lit, and goes out only when nothing
22
+ survives.
23
+
24
+ **Second, and the real blocker: the removal facts are stored in the wrong direction.**
25
+ ADR 0319 gave a confirmed removal a row — `lifecycle_removal_verdicts`, keyed on the
26
+ proposal, carrying the verdict and `ship_task_id`. It also said, in the migration and in
27
+ its own text, that the list of tasks whose work was removed was deliberately absent, and
28
+ that *"brightness designs its own storage for targets when it needs one."* That deferral
29
+ is now due.
30
+
31
+ The gap is not cosmetic. `ship_task_id` is the task whose merge **deleted** the files.
32
+ Brightness needs the previously-shipped tasks that **stopped counting**. Those are
33
+ different rows, almost always under different ideas — a later, unrelated piece of work
34
+ tears out an earlier one. Reading the ship as the removed task dims the wrong star, and
35
+ dims it for the wrong idea, while looking like a working query.
36
+
37
+ The attribution itself already exists. `modules/lifecycle/removal-derivation.js` blames
38
+ each deleted path back to the task that introduced it, and the cascade renders the result
39
+ as prose in the proposal's body. `removal-proposals.js` refuses to parse that prose back
40
+ and is right to: *"a body is written for a person to read, and a reader that silently
41
+ depends on its line shape breaks the day somebody improves the wording."*
42
+
43
+ ## Decision
44
+
45
+ ### D1 — The targets are rows, written at proposal time
46
+
47
+ `modules/lifecycle/migrations/lifecycle_010_removal_targets.sql` adds
48
+ `lifecycle_removal_targets (proposal_task_id, removed_task_id, paths)`, written by
49
+ `recordRemovalTargets` from the same `ctx.removals` objects the proposal body renders.
50
+ **One derivation, two renderings** — the prose and the rows cannot disagree, and
51
+ `removal-proposals.js` keeps its refusal intact because something else now carries the
52
+ fact.
53
+
54
+ It must be written at **proposal** time rather than at verdict time. The attribution comes
55
+ from GitHub commit-history reads, capped at eight paths per ship
56
+ (`MAX_ATTRIBUTED_PATHS`) and running on the server's shared push credential — the same
57
+ credential the publish path depends on. Re-deriving when a person clicks Confirm, days
58
+ later, would spend that budget again against a history that has moved on.
59
+
60
+ This is **not** the cascade runner fanning out, which [ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md)
61
+ forbids: the removal rule still files exactly one task. This records a side fact *about*
62
+ that one task — the same shape as the verdict row its answer writes.
63
+
64
+ `ON CONFLICT DO NOTHING`, never `DO UPDATE`. A second walk can legitimately find *fewer*
65
+ paths, because the cap and the rate limit make an exhausted page yield no attribution at
66
+ all; overwriting would let a degraded re-read erase a good first one. For the same
67
+ reason the writer runs on the cascade's `skipped` row as well as its `created` one, so a
68
+ re-ship **repairs** an attribution that a derivation outage missed.
69
+
70
+ ### D2 — Brightness is weighted by payout, not by task count
71
+
72
+ The share is `live / credits`, where each descent task is weighted by what it actually
73
+ earned the idea.
74
+
75
+ Counting tasks instead would make deleting a one-line fix dim a star as hard as deleting
76
+ the feature it belongs to. Weighting by payout makes brightness fall in proportion to how
77
+ much of the idea's *worth* died — and since size is drawn from the same quantity, the two
78
+ channels become a share of one number rather than two unrelated measurements sitting side
79
+ by side. It also makes "large and dead" exact: full size, zero brightness.
80
+
81
+ The basis is **paid + estimated**, matching size. A historical idea's descent was never
82
+ paid for (`idea-size-historical.js` prices those hops); counting only banked credits would
83
+ mean removing an estimated hop dims nothing while the size drawn beside it still counts
84
+ that hop. To support this, `priceDescentForIdea` and `ideaSizes` now return a per-task
85
+ `byTask` breakdown that **sums exactly to `credits`** — assembled from the priced descent
86
+ plus any ledger payment for a task the descent no longer reaches, because `idea-size.js`
87
+ deliberately keeps those in `credits` so size cannot fall, and dropping them here would
88
+ make the parts sum to less than the whole and silently overstate brightness.
89
+
90
+ ### D3 — An idea that earned nothing is LIT, not dark
91
+
92
+ Zero over zero is not zero. A freshly-filed thought has had nothing removed from it, so
93
+ `share: 1` with `hasWork: false`. Returning 0 would make it pixel-identical to the thought
94
+ whose version was torn out — which is precisely the distinction BV2.R19's proof exists to
95
+ check, and would quietly defeat the reason size and brightness are two channels.
96
+ `hasWork` is what lets the five states (BV2.R17) tell a protostar from a remnant without
97
+ re-deriving it.
98
+
99
+ ### D4 — "Could not look" is not "nothing was removed"
100
+
101
+ When the removal facts cannot be read — the lifecycle module is off, the port is
102
+ unregistered — `share` is `null` and `attributable` is `false`, never a confident 1. Same
103
+ posture `idea-size.js` already takes with `historical: null` when the descent walk is
104
+ unavailable: an established absence and an unreadable one are different answers, and only
105
+ one of them may be drawn as a plain full-brightness star.
106
+
107
+ A **confirmed removal that names no target** is the residual case D1 cannot close: it is
108
+ genuinely gone, and brightness does not know which idea to dim. It happens for proposals
109
+ filed before `lifecycle_010`, for attributions where every deletion predates
110
+ `claims.published_head_sha`, and for a derivation outage. Guessing in either direction is
111
+ worse than not dimming — so nothing dims, and `countUnattributedConfirmed()` exists so a
112
+ surface can say *"the sky may read brighter than the truth by up to N confirmed
113
+ removals"* instead of being silently optimistic.
114
+
115
+ ### D5 — Brightness lives in `economy`, behind a port
116
+
117
+ It is a share of the same payouts size is built from, so it sits beside size in
118
+ `modules/economy/idea-brightness.js`; putting it in `lifecycle` would mean that module
119
+ doing money arithmetic. That makes the removal facts a cross-module read, so they come
120
+ through a kernel-mediated port — `lifecycle.removals` — and never a `require`
121
+ ([ADR 0083](<redacted>.md) rule 1), exactly as economy already
122
+ reaches `ideas.descent`.
123
+
124
+ Economy resolves it with `resolveOptional` and **does not declare it in `consumes`**.
125
+ `consumes` means *required*, and `verifyEnabledPorts` refuses to boot when an enabled
126
+ module consumes an unprovided port — declaring it would turn "lifecycle off" into
127
+ "instance will not start". The same non-declaration, for the same reason, as
128
+ `ideas.descent`.
129
+
130
+ ## Consequences
131
+
132
+ - Brightness is computable per idea and falls only when a **person** confirms a removal.
133
+ A dismissed or unanswered proposal dims nothing, which is what stops refactors, renames
134
+ and moves from dimming the sky continuously — ADR 0313's central finding.
135
+ - **No credit is ever clawed back.** `modules/economy/idea-brightness.js` contains no
136
+ writer and is unreachable from any ship path. `shipped` stays terminal, the ledger is
137
+ never rewritten, and size does not move. Only the picture changes.
138
+ - `ideaSizes` grew a field. Callers that spread its result now carry a `Map`; nothing
139
+ serialises it to JSON today, and a surface that wants to must project it rather than
140
+ pass it through.
141
+ - BV2.R17 (the five states) can resolve protostar-vs-remnant from `hasWork` and `share`
142
+ without re-deriving either, and BV2.R18 (the sky) has `removedTasks` to explain *why* a
143
+ star dimmed rather than only that it did.
144
+
145
+ ## Rejected
146
+
147
+ - **Parsing the proposal body.** The attribution is right there as
148
+ ` - task <id> — <title>`. It is also documentation prose that a copy edit may reword at
149
+ any time, and ADR 0319 already refused this for that reason. A reader that depends on a
150
+ human-facing line shape fails silently and dims nothing.
151
+ - **Re-deriving targets at verdict time.** Spends the capped GitHub budget a second time,
152
+ against a history that has moved, on the interactive path where a person is waiting.
153
+ - **Treating `ship_task_id` as the removed task.** The wrong direction; dims the ship's
154
+ own idea instead of the one whose work died. Named here because the column is right
155
+ there on the verdict row and is the obvious thing to reach for.
156
+ - **Counting tasks rather than payouts.** Simpler, and makes a one-line fix weigh the same
157
+ as a feature.
158
+ - **Clamping the share at both ends.** The bottom is clamped so an anomaly renders as a
159
+ legible "nothing survives" rather than a negative; the top is not, and `live` / `removed`
160
+ are reported unclamped, so an impossible figure stays visible instead of being hidden by
161
+ the thing that claims to prevent it — `idea-size.js`'s argument against clamping size.
162
+ - **Storing the removed task's title beside the row.** It lives on `tasks` and can be
163
+ joined for; a copy goes stale the first time a title is edited.