@bongos/core 1.19.920 → 1.19.921

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.920",
6
- "core_contract": "1.19.920",
7
- "source_commit": "f4047b39eee17385567cbe07c45821af322b8f51",
5
+ "core_version": "1.19.921",
6
+ "core_contract": "1.19.921",
7
+ "source_commit": "b57a8de976b2d06f0400384b4c8bd5d1f0d69508",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-20T19:16:17.763Z",
9
+ "built_at": "2026-09-20T20:00:16.702Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 508,
12
+ "docs_redacted": 509,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2303,
14
+ "functional_verbatim": 2305,
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": 2840,
20
+ "tree_sha256": "9ea93825644aa4fadb711161dbfbf72e4a032f44e888e27c23fbf66d1a695b32",
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,15 @@
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
+ },
2092
2097
  {
2093
2098
  "path": "docs/adr/README.md",
2094
2099
  "mode": "0000644",
2095
- "sha256": "087b5151c182476c1f541e239776099319ad7fa95e6ebc2dc5ccf2b320c84f0c"
2100
+ "sha256": "7f11cc3e7cf17031aae449f13724fe1c42350516e5294732b6db1eb6e3917673"
2096
2101
  },
2097
2102
  {
2098
2103
  "path": "docs/api-reference.md",
@@ -2662,7 +2667,7 @@
2662
2667
  {
2663
2668
  "path": "docs/module-api-changelog.md",
2664
2669
  "mode": "0000644",
2665
- "sha256": "760c792a9c7d16c1b9fc1b77eaf915baf0d9e652d04c84571118c521bbde0700"
2670
+ "sha256": "5693553f86384a3e66f0e6ba77ba1e6f83130a440d0a1f17179173b98696a026"
2666
2671
  },
2667
2672
  {
2668
2673
  "path": "docs/modules-contract.md",
@@ -4514,6 +4519,11 @@
4514
4519
  "mode": "0000644",
4515
4520
  "sha256": "597f883d4878a8fb585585331e11dc219e8a57d1459be4039116b60e7fdc556d"
4516
4521
  },
4522
+ {
4523
+ "path": "modules/grading/grader-no-artifact.js",
4524
+ "mode": "0000644",
4525
+ "sha256": "ab20c06268f30d0ed7198774d7435237f63440550235bdc5b926870db56886e7"
4526
+ },
4517
4527
  {
4518
4528
  "path": "modules/grading/grader-prompt.js",
4519
4529
  "mode": "0000644",
@@ -4592,7 +4602,7 @@
4592
4602
  {
4593
4603
  "path": "modules/grading/grader.js",
4594
4604
  "mode": "0000644",
4595
- "sha256": "1234bb3c9938fd4cf71c39e4660d69c56a27de667ee7ff09b0f865374b6beba4"
4605
+ "sha256": "968363ed43dfd3792462996b061e90cb7af30d9c31a6d6bd39e68ec812b47706"
4596
4606
  },
4597
4607
  {
4598
4608
  "path": "modules/grading/module.json",
@@ -7872,12 +7882,12 @@
7872
7882
  {
7873
7883
  "path": "package-lock.json",
7874
7884
  "mode": "0000644",
7875
- "sha256": "c4064939e0c8b7f6452d0348427109332caa61e8759e40a60ded3dc4f704602a"
7885
+ "sha256": "90c995ff72567f0389795d8aabe1e76d05d0ac47b604081c0f03725a0a66737a"
7876
7886
  },
7877
7887
  {
7878
7888
  "path": "package.json",
7879
7889
  "mode": "0000644",
7880
- "sha256": "840b50ec98565b70f1550ea34aa8813bf5e8cbb8ae7dece18ed1e45abe6ad608"
7890
+ "sha256": "f406c6c8801548af676175664eb13a14e0f80ee58a8106eca0ed075baab77136"
7881
7891
  },
7882
7892
  {
7883
7893
  "path": "public-docs/index.html",
@@ -8977,7 +8987,7 @@
8977
8987
  {
8978
8988
  "path": "scripts/gds/run-unit-tests.js",
8979
8989
  "mode": "0000644",
8980
- "sha256": "a40a90ba6255820130aa6a94ddc314b643177e4a153750dc6290550a75e1a4e8"
8990
+ "sha256": "d3567553fb963ad78c0ab36dd283fbf5c46e188dd7538e00fed36442f7ab2515"
8981
8991
  },
8982
8992
  {
8983
8993
  "path": "scripts/gds/runner-drift.js",
@@ -9212,7 +9222,7 @@
9212
9222
  {
9213
9223
  "path": "scripts/gds/ship-flow.js",
9214
9224
  "mode": "0000644",
9215
- "sha256": "4b8179e5c1aae3fc4845457a4089c57f6cf105c33d03d21d3ade99776371e50f"
9225
+ "sha256": "b6198e134156b755bcffa3c385c9b2ec780dcd7681791c1d3fb938217bd8102b"
9216
9226
  },
9217
9227
  {
9218
9228
  "path": "scripts/gds/ship-git.js",
@@ -9222,12 +9232,12 @@
9222
9232
  {
9223
9233
  "path": "scripts/gds/ship-grade-steps.js",
9224
9234
  "mode": "0000644",
9225
- "sha256": "158a01214499cd229a51819cd2dfcfee2eae34475f87f23795f81ec83d54f337"
9235
+ "sha256": "204834c41dd184ebc076dcd57d6a5370a910f4fc9c276bb3984b08b43f550012"
9226
9236
  },
9227
9237
  {
9228
9238
  "path": "scripts/gds/ship-grade.js",
9229
9239
  "mode": "0000644",
9230
- "sha256": "b1fb38635433b579fbb38d65e92b9db226cccb753866ab7a2a15227796be66cd"
9240
+ "sha256": "40c6a9f6aa6a75cdf8a5e41bfa8318766636cba64e47aee870c26a719676713d"
9231
9241
  },
9232
9242
  {
9233
9243
  "path": "scripts/gds/ship-honesty.js",
@@ -9877,7 +9887,7 @@
9877
9887
  {
9878
9888
  "path": "src/module-api.js",
9879
9889
  "mode": "0000644",
9880
- "sha256": "111908611a709491751fd92695758cca7125efc22bf5e68e908a8c4f45cf6301"
9890
+ "sha256": "6e48d3680bf84d97e45001deee8c64b2a5a31eed9ce9b68515b13d4a42086203"
9881
9891
  },
9882
9892
  {
9883
9893
  "path": "src/module-loader/catalog.js",
@@ -11217,7 +11227,7 @@
11217
11227
  {
11218
11228
  "path": "tests/git_merge_regen.mjs",
11219
11229
  "mode": "0000644",
11220
- "sha256": "a55ae255245a53429cc7948e35ea0515bf58307f826d605d6ffd52cc60196ef8"
11230
+ "sha256": "79208b90f1b785af56ab057c858e96cea4e67ebf247858457addedce55e71b06"
11221
11231
  },
11222
11232
  {
11223
11233
  "path": "tests/github_push_ancestry.mjs",
@@ -11649,6 +11659,11 @@
11649
11659
  "mode": "0000644",
11650
11660
  "sha256": "1823c173f811d52d93b63201d0ebb0d85f2b1e42a569e6331580ef097f5c05cc"
11651
11661
  },
11662
+ {
11663
+ "path": "tests/grader_no_artifact.mjs",
11664
+ "mode": "0000644",
11665
+ "sha256": "c8450702a2d805d1cf5e826a3dedf49fff6cd01c6ca540fa9c64444fc874d5d3"
11666
+ },
11652
11667
  {
11653
11668
  "path": "tests/grader_panel.mjs",
11654
11669
  "mode": "0000644",
@@ -11657,7 +11672,7 @@
11657
11672
  {
11658
11673
  "path": "tests/grader_prompt_fencing.mjs",
11659
11674
  "mode": "0000644",
11660
- "sha256": "fa155f3c5faa3763d1a51d56b7746ad870e826e3249f5e9b08108a3cfbfcbd72"
11675
+ "sha256": "b626e614de000078eef1b305f6322d94326f032bcbeba2808baaa9dcc8ed3103"
11661
11676
  },
11662
11677
  {
11663
11678
  "path": "tests/grader_quality_window_overflow.mjs",
@@ -12507,7 +12522,7 @@
12507
12522
  {
12508
12523
  "path": "tests/no_artifact_escalation.mjs",
12509
12524
  "mode": "0000644",
12510
- "sha256": "3d2b03a39cbefc02565a190dee0612e14576edbf32ea0fccf798d6ed28fee8f1"
12525
+ "sha256": "f1523ee48247d18c7326b9aa0b442ca2c9fab8bd41dbe9f23a4cb89e1986bee0"
12511
12526
  },
12512
12527
  {
12513
12528
  "path": "tests/oauth_secret.mjs",
@@ -13682,7 +13697,7 @@
13682
13697
  {
13683
13698
  "path": "tests/skip_is_not_pass.mjs",
13684
13699
  "mode": "0000644",
13685
- "sha256": "6258b4a3ad76a1d9139b053535c173760c5b42aa82510541501b6fef5ddd1fe1"
13700
+ "sha256": "db9e246f1ce7ef6306b938d325c450e31f2e834244f64dd2f0aaea96e760d987"
13686
13701
  },
13687
13702
  {
13688
13703
  "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.
@@ -409,5 +409,6 @@ This keeps the decision history honest and traceable.
409
409
  | 0316 | [**The ignite floor is a scheduling dial, not the trusted tier** ([task 1004084](https://cloudbongos.com/builders#/task/1004084), goal 1000095 — BONGOS-V2). BV2 decision 3 makes the ignite floor — who may turn a quick idea straight into claimable work — a per-instance setting, and decision 16 adds the descent decay rate beside it, the two to share one home. Both the spec and the seeded task described the mechanism the same way: the floor IS the `METIC_PLUS` constant in `modules/ideas/routing.js`, so this is "a setting, not a refactor". Tracing the code before building refuted that: `METIC_PLUS` is spelled once but READ for four semantically different gates — the two `decideLanding` bug branches (land at `ready` or at `backlog`), `mayProposeCriterion` (who may file a `kind='criterion-proposal'`, a planning-scope write), and the protected-scope wall in `authorizeRoutedLanding` (who may route work into the permission / ship-grade / deploy / migration core). Only the first two are the ignite floor, so making the CONSTANT configurable would mean an instance setting its floor to `xenos` — exactly the "project of one wants every thought claimable" case the owner asked for — ALSO gaining the right to file criterion-proposals and to breach the protected core. The codebase already said so in three places nobody had read into the spec: the constant's own header ("It grants nothing — the server-side rank floor and the goal's scope wall are separate, load-bearing gates"), the protected-scope branch ("the two are layers, not alternatives"), and `tests/filing_abuse_matrix.mjs`, which models the widening as an ATTACK and pins the literal. Decision: a separate dial. `modules/ideas/config.js` + `config/ideas.neutral.json` carry both knobs on the `modules/government/config.js` precedence (env -> instance root `config/ideas.json` -> core root neutral; the ADR 0268 dual-root split matters, since one root reads the host pack out of `node_modules` where `npm ci` erases it). `igniteFloor` defaults to `metic`, resolving to exactly the `METIC_PLUS` set, and is threaded into `decideLanding` as a ready-made `trustedRanks` **Set** by the callers — `routing.js` is TOLD the floor and never reads config, staying pure and DB-free — while `METIC_PLUS` stays a FIXED constant for the other two gates, neither of which takes a floor argument (pinned structurally). `descentDecayRate` defaults to 0.5 and ships with no production caller, because R11 is its intended reader and depends on the unbuilt R08 — the same deliberate slice boundary `modules/government/config.js` documents for itself, and what decision 16 asked for. Everything fails to VANILLA rather than dead: a malformed pack, an unknown rank or an out-of-band rate falls back, and an unrecognised floor yields the DEFAULT set, never an empty one (failing closed would stop every ignition on a typo). Rejected: configuring `METIC_PLUS` as written (a security widening in a config file's clothes); splitting it into four named constants (the other three want the same fixed tier, and naming them invites configuring them); having `routing.js` read the config itself (makes the landing matrix impure and untestable at a different floor). Spec decision 3's "spelled in one place" describes the intent, not the mechanism; the spec was not edited and the task description was amended in flight.](0316-the-ignite-floor-is-not-the-trusted-tier.md) | ideas / permissions |
410
410
  | 0315 | [**A plane with no proxy is refused, not given a no-op ingress** ([task 1004017](https://cloudbongos.com/builders#/task/1004017), goal 1000106 — BONGOS-V2). The piece [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) explicitly deferred: the self-host image ships no proxy, so provisioning step 6's `<slug>.caddy` snippet write and `systemctl reload caddy` cannot work there and a child instance gets no front door. **Two failure shapes, only one visible**: a DOMAINED child already half-refused, because `caddy adapt` fails with no binary and [ADR 0303](0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)'s own catch turns that into a gap — but the message told the operator to add a directive to `/etc/caddy/Caddyfile`, a file the container does not have and that ADR 0303 §1 forbids provisioning from creating; a DOMAINLESS child skipped that preflight entirely (it is gated on `inst.domain`), reached step 6 and died on the `sudo` write, an uncaught throw that bypasses the `{ok:false}` pending-retry path and hard-errors the instance on attempt one — a crash recorded where a refusal was the honest answer. Decision: `ingressPlaneGap(exec)` probes the plane with `caddy version` — one cheap unprivileged read, the same posture as `checkCaddyWiring` — and both standup legs refuse with `Ingress preflight failed: …`, deliberately NOT domain-gated, because the domainless child is the one 0303 could not see. **Only a missing binary proves there is no proxy**: under remote exec `boxExec` is `ssh <host> bash -s`, so an unreachable co-hosting box throws here too, and diagnosing a network blip as 'this plane has no proxy, provision elsewhere' would be a confident wrong answer that burns the intent's retry budget — exit 127 is read as proof, anything else answers 'could not confirm a reverse proxy on this plane' with the real cause leading. The three refusals are ordered **capacity → ingress → wiring**, cheapest and most fundamental first and all before any mutation, which moves 0303's preflight behind the ADR 0285 capacity measurement; both still refuse before anything changes, so only the precedence between two refusals moves. **No ingress seam and no no-op backend**, despite the obvious symmetry with 0306: a `none` implementation whose `writeSite`/`reload` are no-ops lets the standup SUCCEED while leaving the child unreachable — neither half of the task's done-when, and exactly the silently-broken instance it exists to prevent. Does not decide: what a child's front door IS on a container plane (a published port, a bundled proxy, an explicit localhost-only shape) — a future ADR that adds one replaces this refusal rather than extending it. Teardown is left alone on purpose: it already reloads `allowFail`, and refusing there would strand a row instead of cleaning it.](0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md) | provisioning / ingress |
411
411
  | 0317 | [**A plane that cannot give an instance its own account is refused, not given a shared uid** ([task 1004018](https://cloudbongos.com/builders#/task/1004018), goal 1000106 — BONGOS-V2). The last of the three container gaps [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) deferred, and the sibling of [ADR 0315](0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md). [ADR 0281](0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md) makes an instance's identity its own unix account, and the unit's `User=`/`Group=` is what stops one co-tenant reading a sibling's `/proc/<pid>/environ` — where every instance's `EnvironmentFile` carries its GitHub App private key and OAuth client secret (audit B3). `useradd` needs uid 0; the self-host image is `node:22-bookworm-slim` running `USER node` with **no sudo**, so there the account cannot be created at all. **Two failure shapes, neither honest**: today step 1c's `boxExec(ensureInstanceUserCmd(...))` is not `allowFail`, so it throws AFTER every preflight passed, AFTER the status flip to `provisioning`, and — for a standalone — after a repo pull and an `npm ci`, recording a crash with the box half-changed; and tomorrow it goes quiet, because a compose self-host only stops short of step 1c by ADR 0315's coincidence, and 0315 says in its own consequences that a future front door **replaces** that refusal. Decision: `accountPlaneGap(exec, { privileged })` in `provision-repo.js` probes with `${sudo}sh -c 'test "$(id -u)" = 0'` and `provisionInstance` refuses with `Account preflight failed: …` before any mutation. **The probe runs the SAME escalation the mutation will** (`PROVISION_SUDO=1` renders the same `sudo ` prefix `ensureInstanceUserCmd` uses), so a pass is evidence rather than inference. **`sh -c`, not a bare `test`, is load-bearing**: under sudo the outer unprivileged shell would expand `$(id -u)` first and answer about the wrong user, passing on exactly the plane this refuses — and for the same reason there is no pipeline, whose exit status is its last stage's. **Only a shell that ran and answered proves it**: `boxExec` is `ssh <host> bash -s` under remote exec, so exit 1 (`test` said no) and 127 (no sudo binary) are read as proof while ssh's own 255 answers "could not confirm" with the real cause leading — ADR 0315's rule applied to a different question. Ordered **third**, after capacity and ingress and before 0303's wiring check: the first three ask about the PLANE, 0303 asks about this instance's DOMAIN, and 0315's order is extended rather than rearranged. **Rejected: accepting it** — documenting a self-host container as single-tenant with a shared uid is a silent downgrade of the vector 0281 closed, observable nowhere on the running box, and "single-tenant" is not what a container hosting several child projects is; **an identity seam with a container implementation**, because nothing exists to put behind it and a seam whose second backend does nothing is 0315's no-op wearing a seam's clothes; **probing `useradd --help` / `command -v useradd`**, which succeed unprivileged on the very image this refuses; **`${sudo}id -u` compared against `0`**, which decides on captured stdout where every sibling preflight and every test fake treats exit status as the contract; and **refusing inside step 1c**, which keeps the refusal after the status flip. Not covered, deliberately: root-but-no-`useradd` (an Alpine-style plane fails loudly at step 1c as today), and teardown (0315's reason — a refusal there strands a row instead of cleaning it). No `PROVISION_ALLOW_SHARED_UID`: a runner either reaches root on this plane or it does not.](0317-a-plane-that-cannot-give-an-instance-its-own-account-is-refused.md) | provisioning / identity |
412
+ | 0320 | [**An empty diff is not an ungraded diff: grade the claim instead** ([task 1004011](https://cloudbongos.com/builders#/task/1004011), goal 1000118 — BONGOS-V2; scoped out of task 1003950 shape 2). `ship.js --allow-empty` closes a task that produced no committed artifact, and it used to do two things at once: set `skipGrade`, so the subagent grader never ran, and therefore route the close through `POST /tasks/:id/confirm`, gated on `task.confirm.grade_bypass` — one of [ADR 0157](0157-archon-is-rank-and-identity-only.md)'s four trust-boundary keys, so a Metic could not close an artifact-free task at all. **Both halves were wrong.** The permission was the wrong question: `grade_bypass` means *ratify past a grade that FAILED*, a genuine override of an existing verdict, and a no-artifact close has no verdict to override — charging the same key for "it was judged and rejected" and "there was nothing to judge" conflates an override with an absence. And nothing was being checked, which is the more serious half: the assertions in the handoff notes ("the premise was already fixed", "this was a pure ops verification", "the row was updated directly") went into the ledger unread, on exactly the ships where the record is the ONLY evidence that exists. A permission gate is not scrutiny — an Archon clicking confirm is not someone checking whether the claim is true. **Decision:** on `--allow-empty` the grader is not skipped, it is RE-POINTED — it grades the notes + value summary against the task description, asking whether the task is genuinely artifact-free and whether the evidence supports that. A pass is a real passing grade confirming through the ordinary rank-open path; a fail is answered with better evidence, not a key. Three sub-decisions carry it: the rubric axes are **re-asked, not replaced** (`functional_fidelity` → is this genuinely artifact-free, `code_quality` → is the evidence specific and checkable), because the gate arithmetic and `/grade-audit`, the aggregates and the cost model all read those columns and a third pair of names would mean teaching every one of them a synonym; **two generalist shots, not the four-lens panel** (Hacker/Quality/Efficiency are lenses over code — over three paragraphs of prose they are four readings of the same short text at four times the cost — so the lenses go, recorded in `signals.lenses_dropped` per the #513 trivial-diff precedent; but the panel's UNANIMOUS gate was also the cross-check that makes injection hard, and the first cut kept a single shot and lost it, so the species asks the same question twice and collapses by **unanimity, not median** — under a median the mean of a fooled 10 and a suspicious 3 is a passing 6.5 and the second reading launders the first; a split is recorded as `signals.shot_dissent`; the collapse covers ALL THREE gate axes including `confidence`, which the second grading round caught missing — it is the one axis living in `signals` rather than `rubric_scores`, and the server derives `lowConfidence` purely from the submitted value, so an honest 0.2 "I cannot judge this" averaged with a confident 0.9 would have cleared the 0.4 gate at 0.55); and the species marker lives in **`signals.no_artifact_grade`, not `grader_kind`**, because the server's `validateAndScore` hardcodes `task_grades.grader_kind` to `'subagent'` for everything on that route. **D4: every builder-controlled field is scrubbed and fenced** (`grader-workers/fence.js`, SR-15 / #995) — the panel workers have fenced the diff, title, description and value summary since #995, and this species is the worse case on both sides: the notes are not context around an artifact the model can independently check, they ARE the evidence, and a successful injection does not flip one of four workers, it closes a task past the very scrutiny this ADR added to replace the Archon-only gate. The first implementation shipped it unfenced and the grader caught it as a blocker. The no-follow rule is restated AFTER the artifact too, since the notes may run to 24,000 characters and a rule stated once at the top is a long way behind by the end of an adversarial payload. The prompt renders no changed-files or diff section at all, so the absence cannot read as the finding. No diff hash is stored — hashing the empty diff makes it a reuse key shared by every empty-diff ship of the task, handing the next close a pass this one earned. Unchanged: `--skip-grade` still reaches the Archon gate (the task 1003950 escalation split still chooses the refusal), and below the protected-path floor the server's Guard 1b (task 1002646) still fails a zero-`committed_files` pass closed, correctly — this moves the Metic-and-above case, which is the case that was filed. **Drops task 1004010**, the alternative shape — a new lower-floored permission atom for the same close — by owner decision of 2026-09-20: it answers "who may skip the check" when the better question is "why is there no check", and it permanently widens the permission surface, the same class of change the owner declined three days earlier for `version.create` ([task 1003853](https://cloudbongos.com/builders#/task/1003853), [ADR 0250](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) §6a). Also rejected: lowering the floor on the existing key (treats the gate as the point; nothing would read the notes); grading the notes with the full panel; a new pair of rubric axes for prose (honest about the measurement, but moves the gate arithmetic and every consumer of it for a naming gain).](0320-an-empty-diff-is-not-an-ungraded-diff.md) | grading / permissions |
412
413
  | 0319 | [**A removal verdict is a row beside the question, because both answers close it the same way** ([task 1004095](https://cloudbongos.com/builders#/task/1004095), goal 1000095 — BONGOS-V2). [ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md) settled that a ship deleting an earlier shipped task’s files files a QUESTION and a person answers it; task 1004090 built the question and nothing could answer it. Building the answer surfaced the property that decides the design: a proposal is a question, not work, so CONFIRMING and DISMISSING both leave nothing to build and both end the task at `abandoned` — meaning a status alone records neither, and brightness could not tell the star it must dim from the one it must not. ADR 0313 had already rejected the obvious alternative (a `removed` status; `shipped` is terminal and much of the ledger leans on it — a removal is a fact recorded BESIDE work, not a new terminal state). Decision: `lifecycle_removal_verdicts` (migration `lifecycle_009`), PRIMARY KEY on `proposal_task_id` so one proposal holds one answer ever — a second decide is REFUSED (409), never upserted and never an idempotent 200, because overwriting destroys the record that a person answered differently; the row is locked `FOR UPDATE` first, so simultaneous Confirm and Dismiss cannot both read “undecided”, and `ALREADY_DECIDED` is checked BEFORE `PROPOSAL_CLOSED` because an answered proposal is also abandoned and the useful message is the one naming the earlier verdict. The verdict and the status change commit in ONE transaction. What this does NOT ship is the target list: the affected tasks and paths stay PROSE in the proposal body (the owner’s 2026-09-20 decision, since [ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md) forbids widening the runner to fan out), and nothing here parses it — brightness designs that storage. What brightness does get is structured: the verdict plus `ship_task_id`, denormalised out of the `source_ref` that encodes it as a string nothing can join on. Surface: spec decision 15’s “existing awaiting-nod surface” is the CARD, not the QUEUE — [ADR 0299](0299-the-nod-queue-stays-scoped-to-routed-filings.md) keeps that queue scoped to routed filings and a removal proposal has no idea lineage at all, so it gets its own slice on the same card at the same floor. The PERMISSION is reused (`task.promote`, Metic+, read and write both) and the VERB is not: no existing route means “that work is genuinely gone”, and `/tasks/:id/abandon` would close the question without recording which answer was given. Top-level `/removal-proposals` rather than `/tasks/removal-proposals`, because the manifest mounts `tasks` first and its `/tasks/:id` swallows a later file’s literal segment. Rot is INHERITED: `backlog` is rottable and `abandoned` is not, so an unanswered proposal rots and an answered one leaves the feed with no code written for either — pinned against the rot module’s own exported vocabulary. Rejected: the status as the record; parsing the body at confirm time; widening the nod queue; separate `/confirm` and `/dismiss` routes; upserting a second verdict; a `LIKE ‘prefix%’` match (a rule key may contain an underscore, which is a LIKE wildcard).](0319-a-removal-verdict-is-a-row-beside-the-question.md) | ideas / lifecycle |
413
414
  | 0318 | [**Listing a file for task affinity is not claiming to own it** ([task 1004110](https://cloudbongos.com/builders#/task/1004110), goal 1000095 — BONGOS-V2). The case [ADR 0314](0314-module-protection-is-per-path-not-per-module.md) named and left open, closed by neither of the two routes it predicted. `builder-settings` derived `protected` — Archon-only to scope a goal to — on exactly one glob, `src/bongos/routes/me.js`, caught by the blanket `src/bongos/routes/` prefix. **That glob was never an ownership claim:** `MODULE_GLOBS` listed it for task AFFINITY and said in the same comment that the `GET /me` aggregator stays a thin core facade, and `isProtected()` cannot tell the two apart. **Both predicted routes were measured and rejected.** Narrowing the blanket prefix: all eighteen files under `src/bongos/routes/` are protected by that prefix AND NOTHING ELSE, so narrowing unwalls `auth.js`, `builders.js`, `backup.js` and `core-update.js` together — now pinned as a tripwire test rather than left as a caution. Moving `me.js` into `modules/builder-settings/`: the aggregator resolves TEN module ports (`lifecycle`, `reward`, `onboarding`, `government`, `specialities`, `grade`, `box`, `art` ×2, `discord`) and `builder-settings` is one of them, so hosting it inside one module means resolving nine foreign ports from the inside — the `core -> module` edge [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 forbids. Decision: **ownership follows what the file is** — `me.js` moves to `kernel`, which already owns `src/bongos/routes/security.js`. This is 0314 one step on: owning the command is not owning the authority, and **being listed next to a module is not owning it either**. **Rejected: a general affinity tier** in `MODULE_GLOBS` mirroring `isCommandPath` — the audit does not support it. Three other modules (`kernel`, `devsecops`, `methodology`) are also protected only by paths outside their own directory, but in each the foreign path genuinely IS the domain; `builder-settings` was the only module protected by a path its own source called core, so a new tier would be a new place for a hole to hide, built for a population of one. `protectedModules()` goes 9 -> 8. **The wall that matters does not move:** a sub-Metic builder touching `/me` is still refused by `checkPermissionPaths`, asserted directly rather than assumed. The sixteen still-unowned route files remain [task 1004116](https://cloudbongos.com/builders#/task/1004116), whose option (a) this de-risks.](0318-listing-a-file-for-task-affinity-is-not-owning-it.md) | permissions / module scope |
@@ -2314,5 +2314,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2314
2314
  landed since 1.19.918 with no explicit bump. run 35531198454. (task 1002620)
2315
2315
  1.19.920 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2316
2316
  landed since 1.19.919 with no explicit bump. run 35531720944. (task 1002620)
2317
+ 1.19.921 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2318
+ landed since 1.19.920 with no explicit bump. run 35534076467. (task 1002620)
2317
2319
  ---------------------------------------------------------------------------
2318
2320
  ```