@bongos/core 1.19.626 → 1.19.628

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.626",
6
- "core_contract": "1.19.626",
7
- "source_commit": "15d768a9fe3239e170150c8d2efb98e6de8d98da",
5
+ "core_version": "1.19.628",
6
+ "core_contract": "1.19.628",
7
+ "source_commit": "4f99983713ad375f7f42c932c781c3c77a47bc66",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T14:24:41.469Z",
9
+ "built_at": "2026-09-09T19:56:16.788Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 464,
12
+ "docs_redacted": 466,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2096,
14
+ "functional_verbatim": 2098,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2584,
20
- "tree_sha256": "1fbadd48214869e3a766dde80f634c74c81a7ee96bf480937417526afe50a27b",
19
+ "file_count": 2588,
20
+ "tree_sha256": "ab072d3e169daecbd6fbfc4d2e18d2786ea247cb5b84a4d4d5dc06cf17b0c210",
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",
@@ -192,7 +192,7 @@
192
192
  {
193
193
  "path": ".claude/skills/ideate/SKILL.md",
194
194
  "mode": "0000644",
195
- "sha256": "52bf6ab873aabc5e79cb97014793d2b237d04fd5e348938e5a63dbd7bedc10ca"
195
+ "sha256": "e4279d87f1f4bad8566bb69ea82278bde3a725a3879f972ad3cb34bb2cef9de5"
196
196
  },
197
197
  {
198
198
  "path": ".claude/skills/merge-mode/SKILL.md",
@@ -1869,10 +1869,20 @@
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
+ },
1877
+ {
1878
+ "path": "docs/adr/0271-the-ideator-surface-is-two-halves.md",
1879
+ "mode": "0000644",
1880
+ "sha256": "0d4805df73af0b99c6dfd1c362debaa04d93a8a075f8e4b0cdf11af975bfc62c"
1881
+ },
1872
1882
  {
1873
1883
  "path": "docs/adr/README.md",
1874
1884
  "mode": "0000644",
1875
- "sha256": "6b90114abfa5e891a8f2ea16c547c960c982030d6228d43c2a833840123a3f69"
1885
+ "sha256": "9b75ec13500719a639364bcc9436e9f0cde63119a032c805312153dbb006f390"
1876
1886
  },
1877
1887
  {
1878
1888
  "path": "docs/api-reference.md",
@@ -2757,7 +2767,7 @@
2757
2767
  {
2758
2768
  "path": "docs/module-api-changelog.md",
2759
2769
  "mode": "0000644",
2760
- "sha256": "19d4d61c3cd024932aeb068335ef1d9d69e37a59a0e7d653a5e6136bb4972c19"
2770
+ "sha256": "f0bda08beedf16b6f2eed29c598f4a7602c35e84f084ca744861c6242656fe01"
2761
2771
  },
2762
2772
  {
2763
2773
  "path": "docs/modules-contract.md",
@@ -3779,6 +3789,11 @@
3779
3789
  "mode": "0000644",
3780
3790
  "sha256": "c10c4bbbead4a3bafde336f3433a1432818880751f7a21436a6162f0dbde1abf"
3781
3791
  },
3792
+ {
3793
+ "path": "migrations/core_236_rank_floor_backfill.sql",
3794
+ "mode": "0000644",
3795
+ "sha256": "06b53d66e710dfbf602011459ec8d69adce8e281632927daea0735f8078b2f86"
3796
+ },
3782
3797
  {
3783
3798
  "path": "modules/agents/lib/validate.js",
3784
3799
  "mode": "0000644",
@@ -3852,7 +3867,7 @@
3852
3867
  {
3853
3868
  "path": "modules/builder-settings/interaction-prefs.js",
3854
3869
  "mode": "0000644",
3855
- "sha256": "5a1184ee4ef1db413358e41fd4ce3f88a1e99cfe4f2ff44bd9bcc4182bc3de64"
3870
+ "sha256": "64575386423570872fd0b10751cc1863cc813c992ef27d0be26b7b924d9c6f52"
3856
3871
  },
3857
3872
  {
3858
3873
  "path": "modules/builder-settings/model-alloc-prefs.js",
@@ -5602,7 +5617,7 @@
5602
5617
  {
5603
5618
  "path": "modules/lifecycle/db-rank-authz.js",
5604
5619
  "mode": "0000644",
5605
- "sha256": "c93780ea71bb2f8f3d2b21f042c21d029f1ed5336a3aaf178fb3e14925dcc19b"
5620
+ "sha256": "ac33825a705a0d541fc7fe94858f967ef8ef9713a2b7a1f1a03ce268494bb317"
5606
5621
  },
5607
5622
  {
5608
5623
  "path": "modules/lifecycle/db-shared.js",
@@ -5617,7 +5632,7 @@
5617
5632
  {
5618
5633
  "path": "modules/lifecycle/db-tasks.js",
5619
5634
  "mode": "0000644",
5620
- "sha256": "65558689ccfcea8f71b98cd51eaf1fb45d39267f7e6f6cf8fc35814da2752a34"
5635
+ "sha256": "d8cce5b44106009987d3d32b711a114e4505b4e504bbc6fdfc84f161024ee0cd"
5621
5636
  },
5622
5637
  {
5623
5638
  "path": "modules/lifecycle/db-versions.js",
@@ -7682,12 +7697,12 @@
7682
7697
  {
7683
7698
  "path": "package-lock.json",
7684
7699
  "mode": "0000644",
7685
- "sha256": "008a893c7efba51a8e8b7feff7b0569454c3b85670369ad736c28ba25b331c75"
7700
+ "sha256": "aba13c3ae1fa839b97bc3f11aff342251f550f7cbcc8531a9ae3c6856f326d6e"
7686
7701
  },
7687
7702
  {
7688
7703
  "path": "package.json",
7689
7704
  "mode": "0000644",
7690
- "sha256": "a8f88df15fae0f2566f18b0d78985e82d8d6ab5c72ee35cdf1598130ac2e8ce3"
7705
+ "sha256": "bdea58839926be753b49b016fc961cff55481f292f911d9fa044e77db022db21"
7691
7706
  },
7692
7707
  {
7693
7708
  "path": "public-docs/index.html",
@@ -8062,7 +8077,7 @@
8062
8077
  {
8063
8078
  "path": "scripts/gds/discipline-modes.json",
8064
8079
  "mode": "0000644",
8065
- "sha256": "6b740035b0d793dd2063de32b660a202da518b920742892a09058943c54e60c5"
8080
+ "sha256": "c0c1849145999ef02f3022b22e718da8bb4fb680020124d115423f2f868834d1"
8066
8081
  },
8067
8082
  {
8068
8083
  "path": "scripts/gds/discord-channels.js",
@@ -9427,7 +9442,7 @@
9427
9442
  {
9428
9443
  "path": "src/module-api.js",
9429
9444
  "mode": "0000644",
9430
- "sha256": "13157fd8a1bf1a5d1b892a3deed40c9fc1a85c7daf5db9bf5283837e261a6081"
9445
+ "sha256": "f14dc0f62bb0abba340f775a22b7810b56eacc91c29015a9712f7d9afd57f0ac"
9431
9446
  },
9432
9447
  {
9433
9448
  "path": "src/module-loader/catalog.js",
@@ -11277,7 +11292,7 @@
11277
11292
  {
11278
11293
  "path": "tests/interaction_prefs.mjs",
11279
11294
  "mode": "0000644",
11280
- "sha256": "d00e82184fbf7aab49c9b6efe5709b14f300ca0cea277a0eb140f073ded7ff50"
11295
+ "sha256": "e124b44e734806a3ee30b06bcaf3837237d98abf62e68a3709f4dab78c38a83c"
11281
11296
  },
11282
11297
  {
11283
11298
  "path": "tests/join_application_note.mjs",
@@ -12049,10 +12064,15 @@
12049
12064
  "mode": "0000644",
12050
12065
  "sha256": "aea6f9ec053bd9477288025b58ed76c166c2d59a24d7ea3481edadba90113887"
12051
12066
  },
12067
+ {
12068
+ "path": "tests/rank_floor_wiring.mjs",
12069
+ "mode": "0000644",
12070
+ "sha256": "f81f559b5428497b2246580c7921ecd1ba5a2f1034cf29eb5e5c21b04675f216"
12071
+ },
12052
12072
  {
12053
12073
  "path": "tests/rank_gate.mjs",
12054
12074
  "mode": "0000644",
12055
- "sha256": "d8fff57f5fee95bf11a5c1df3e2c0e342d39ef5b22c5e87f47a61dd9a3d2a8cb"
12075
+ "sha256": "78626dc42e6d31685352e43e1e8e094a904f59d9d0562caf6b6e126934df973f"
12056
12076
  },
12057
12077
  {
12058
12078
  "path": "tests/rank_tier_single_source.mjs",
@@ -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
 
@@ -17,6 +17,17 @@ Concretely, that means:
17
17
  - **Do the legwork they ask for, on demand.** Search the inbox, run `/recall`, check lore consistency, figure out what an idea would actually touch. You are their research arm; they should never have to do the digging.
18
18
  - **The human decides what gets filed.** You only `capture.js` an idea once they've blessed it. The conversation is the product; the filed idea is its residue.
19
19
 
20
+ ## The second rule: be quiet about the machinery
21
+
22
+ The *behavior* above inverts for this role — and so does the **surface**. An ideator came here to think about an idea, not to watch a build. So **report outcomes, not mechanism.**
23
+
24
+ - **Don't narrate the plumbing.** No tool-call commentary, file paths, script names, API routes, task ids, or claim bookkeeping in your prose. You still *use* all of it — you just don't make the ideator read about it.
25
+ - **Speak in the language of the idea.** "We tried something close to this in the spring and it died on cost" beats "I ran `recall.js` over the ADR corpus and got four hits."
26
+ - **One line out, then the answer.** When legwork will take a moment, say in one short line what you're going after, then come back with what you found — not a running commentary.
27
+ - **Quiet is never hiding.** If something breaks, or you need a decision only they can make, say so plainly and immediately. A quiet surface that swallows a problem is a broken surface.
28
+
29
+ **What this rule can and cannot reach.** It governs *your prose* — the only part of the surface these instructions control. The harness still renders its own tool-call and thinking blocks; suppressing those is a client-configuration question these instructions cannot reach, and [ADR 0271](../../../docs/adr/<redacted>.md) §4 records the two candidate levers and defers the choice. Don't apologize for it and don't invent workarounds.
30
+
20
31
  ## Step 0 — detect the mode, because it changes everything
21
32
 
22
33
  **Is a human present in this session?**
@@ -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.
@@ -0,0 +1,233 @@
1
+ # 0271 — The ideator surface is two halves: Bongos owns the prose, the harness owns the chrome
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-09
5
+ - **Tasks:** [#1003506](https://cloudbongos.com/builders#/task/1003506) (this record and the prose half). Promoted from idea 1000732 (owner, 2026-08-16).
6
+ - **Goal:** [#1000095](https://cloudbongos.com/builders#/goal/1000095) — Working area 6 (Governor / Builder / Artist / Ideator experience), criterion `wa6-kernel-and-packs`.
7
+ - **Builds on:** [ADR 0264](<redacted>.md) §1 (the area and its owner), [ADR 0016](<redacted>.md) (the trust boundary — why the permission prompt was never the gate), [ADR 0104](<redacted>.md) (the existing bypass posture on dev boxes).
8
+ - **Answers** the ideator half of the argument the `artist-mode` criterion answered for artists with the Studio (goal [#1000074](https://cloudbongos.com/builders#/goal/1000074)).
9
+
10
+ ## 1. The decision
11
+
12
+ The ideator's complaint — *"there is really no need for an ideator to see all the
13
+ text and technical work being done, or even the inner Bongos logic"* — is two
14
+ problems wearing one sentence, and they have different owners.
15
+
16
+ **The prose half is Bongos's, and it ships with this record.** What Claude *says*
17
+ is reachable from configuration Bongos already owns and already injects. Three
18
+ pre-existing seams carry it; nothing new was plumbed:
19
+
20
+ | Axis | Seam | Behavior |
21
+ |---|---|---|
22
+ | per-**person** | `interaction-prefs.machinery` (`builder-settings`) | new field, `show` (default) \| `quiet`. Rides the live Conductor injection rail. |
23
+ | per-**role** | the `ideator` directive in `scripts/gds/discipline-modes.json` | a claimed ideator task opens a quiet session regardless of the person's setting. |
24
+ | per-**session** | `/ideate`'s new *"be quiet about the machinery"* section | the directive names `/ideate` as the only place the ideator experience is defined, so the contract has to live there too. |
25
+
26
+ **The chrome half is the harness's, and it is deferred to a spike rather than
27
+ guessed at.** Tool-call blocks and thinking blocks are drawn by the client, and
28
+ **no instruction Bongos injects can suppress them** — an injected contract is
29
+ content, not display configuration. But that does *not* make the chrome
30
+ unreachable, and this ADR was nearly written on the opposite, wrong assumption.
31
+ Two real levers exist (§4), and choosing between them needs a version-pinned
32
+ observation this record does not have. So the chrome half is **filed as a spike
33
+ plus an implementation task** — not attempted here, and not declared impossible.
34
+
35
+ **Bypass is explicitly NOT shipped as an ideator default**, rejecting that part
36
+ of the originating idea. §3 — and the reason is now partly technical, not only
37
+ prudential.
38
+
39
+ ## 2. Why not a room, and why not wait for declared roles
40
+
41
+ **Why not a hall room (the Studio's answer).** Goal 1000074 answered the artist
42
+ half of this same argument with a place: *"a place an artist wants to be, not a
43
+ form."* `/builders/studio` is that room, and it is right for an artist, whose
44
+ craft has artifacts a room can hold. It cannot answer the ideator's version,
45
+ because **the ideator's complaint is about the surface of the Claude session,
46
+ which the hall does not render.** The Studio still earns its keep here as
47
+ precedent — the mode marker on the document root, and the discipline of a room
48
+ that writes nothing and links out to where the craft is done — but it is not the
49
+ vector. A room would have been a satisfying deliverable that missed the ask.
50
+
51
+ **Why this did not wait on per-session declared role.** The task recorded a
52
+ dependency on idea 1000729 (declared role), whose tasks
53
+ ([#1002991](https://cloudbongos.com/builders#/task/1002991),
54
+ [#1002992](https://cloudbongos.com/builders#/task/1002992)) sit behind
55
+ [#1002989](https://cloudbongos.com/builders#/task/1002989) →
56
+ [#1002990](https://cloudbongos.com/builders#/task/1002990). That dependency is
57
+ **softer than it was filed.** The craft axis already exists: `discipline` is a
58
+ column on every task and `claim.js` already forks on it (task 1267), through the
59
+ file whose own documentation says *"to give a discipline its own experience
60
+ later, add an entry here."* The role-derived half could therefore ship today
61
+ through the seam built for exactly this.
62
+
63
+ When declaration lands, this contract **moves without being re-decided**: the
64
+ same sentence relocates from the claim directive into the composed Ideator pack,
65
+ and the per-person field stays put. Nothing here has to be undone.
66
+
67
+ ## 3. Bypass: the idea's own caution, resolved against it
68
+
69
+ The idea proposed *"recommend bypass"* and then flagged the trade-off itself. The
70
+ flag wins, for three reasons in increasing order of force:
71
+
72
+ 1. **The least technical role is the worst audience for removing confirmations.**
73
+ Shipping bypass *to ideators specifically* concentrates the one change that
74
+ removes review on the people least equipped to review.
75
+ 2. **The permission prompt was never the gate.** [ADR 0016](<redacted>.md)
76
+ is load-bearing: authority is `builders.rank`, checked server-side per request
77
+ with no caching. The client prompt is a speed bump, not a boundary. The
78
+ protected-path globs and pre-push hook are what actually bound a sub-Metic
79
+ session — and an ideator is typically sub-Metic, so that narrowing is *already
80
+ in force*. Bypass would add exposure without adding capability.
81
+ 3. **A project cannot ship it anyway.** The harness deliberately refuses it:
82
+ `permissions.defaultMode: "bypassPermissions"` set in a project's
83
+ `.claude/settings.json` or `.claude/settings.local.json` **does not take
84
+ effect — the session starts in Manual mode instead.** (`auto` is restricted
85
+ the same way.) Only user-level, `--settings`, or managed settings can select
86
+ it. So "ship bypass as an ideator default" is not merely unwise; it is
87
+ unavailable through the only channel Bongos can commit into a project repo.
88
+ Where bypass *is* the default today it was written into the machine's own user
89
+ settings — which is exactly what the dev box does (task 1251, ADR 0104).
90
+
91
+ The ideator's problem is the noise, not the prompt. This ADR treats only the
92
+ noise. Any future bypass default for a craft must arrive with a *narrower*
93
+ capability set, not a broader one.
94
+
95
+ ## 4. The chrome half: two real levers, and what we do not yet know
96
+
97
+ Recorded so the follow-on spike starts from evidence instead of repeating the
98
+ search. Both are documented harness features, not speculation.
99
+
100
+ **Lever A — shipped harness defaults.** The client already has a quiet mode:
101
+
102
+ - **`/focus`** renders "only your last prompt, a one-line tool-call summary with
103
+ edit diffstats, and the final response" — very close to the owner's literal
104
+ ask. The `viewMode` setting is documented as the key that presets it.
105
+ - **`alwaysThinkingEnabled: false`** stops thinking being generated at all
106
+ (settable from any settings file), so there is nothing to render. Thinking is
107
+ already collapsed to a stub by default; `showThinkingSummaries` controls that
108
+ stub.
109
+ - Chrome trims confirmed committable: `showTurnDuration`, `spinnerTipsEnabled`,
110
+ `prefersReducedMotion`, and a custom `statusLine` — which has the useful side
111
+ effect of suppressing most footer keyboard hints.
112
+ - The built-in **`Concise` output style** ("leads with the result, skips preamble
113
+ and narration") is prior art for the prose contract in §1, and preserves error
114
+ reports and destructive-action confirmations the same way our contract does.
115
+
116
+ *Not chosen for the role axis* because `outputStyle` is a whole-session
117
+ setting: committing it would re-pitch every engineer session in the repo too,
118
+ and an output style that omits `keep-coding-instructions` drops the built-in
119
+ engineering instructions entirely. A role-scoped injected contract is the
120
+ narrower instrument.
121
+
122
+ **Three things Lever A does not settle, which the spike must:**
123
+
124
+ 1. `/focus` is **fullscreen-renderer only** (a research preview), so presetting
125
+ it implies presetting `tui` as well.
126
+ 2. Whether `viewMode` is settable from a **committed project** file is
127
+ **unverified**. Nothing should be built on it until `/config --help` is
128
+ checked against the pinned client version — a version-pinned observation beats
129
+ a docs page for this.
130
+ 3. Thinking suppression is **model-dependent**: on some models the toggle,
131
+ `alwaysThinkingEnabled`, and `MAX_THINKING_TOKENS=0` have no effect at all.
132
+
133
+ **Lever B — an app-owned surface (vector (b) of the original idea).** Headless
134
+ mode emits no client chrome by construction: `claude -p --output-format
135
+ stream-json` yields a documented event stream (`system`/`init`, `assistant`,
136
+ `stream_event` carrying a `content_block_start` of type `tool_use`, and a
137
+ terminal `result` with final text, cost and `permission_denials`). The documented
138
+ streaming-UI pattern is precisely the owner's ask — show a working indicator on a
139
+ `tool_use` block, drop everything else, render on the result. This is the only
140
+ lever **fully in Bongos's control and model-independent**, and it is where the
141
+ desktop app would render the ideator's real interface.
142
+
143
+ **Bias for the spike:** prefer Lever B for the durable surface, and use Lever A
144
+ only for what it can do cheaply today. Lever A rests on a research-preview
145
+ renderer, an unverified setting, and per-model thinking behavior; Lever B rests
146
+ on a stable documented interface.
147
+
148
+ ## 5. Consequences
149
+
150
+ - The default is `show`, so **no existing builder's experience changes.** The
151
+ contract injects only when a builder opts in or claims an ideator task —
152
+ consistent with the inject-only-when-non-default discipline the wandering and
153
+ interaction knobs already follow.
154
+ - The field needs **no UI work from this task**: `fieldCatalog()` is generic, so
155
+ `machinery` appears on `GET /me` and in `PATCH /me/interaction` validation
156
+ automatically, and whatever settings UI [#1002514](https://cloudbongos.com/builders#/task/1002514)
157
+ builds renders it for free.
158
+ - **Quiet must never hide a failure.** Both the injected contract and the
159
+ `/ideate` section carry that exception explicitly, and a unit test asserts the
160
+ contract string still contains it — this is the failure mode most likely to be
161
+ reintroduced by a later "tidy the wording" edit.
162
+ - `tests/interaction_prefs.mjs` stopped asserting a bare field **count** (which
163
+ every new field broke) and now asserts the field **set** against `FIELD_KEYS`.
164
+ - The chrome half stays visibly unfinished, which is the honest state. It is
165
+ filed with its evidence, not silently dropped.
166
+ - **One correction to the record:** task [#1002514](https://cloudbongos.com/builders#/task/1002514)
167
+ states that `contractFor()` "has ZERO consumers." That is **stale** — the
168
+ Conductor hook relays the interaction contract as advisory 8 (task 2010,
169
+ `.claude/hooks/conductor.js`). Its *no UI* half is still accurate.
170
+
171
+ ## 6. Recorded objection: the grade panel's verdict, and this record's answer
172
+
173
+ The ship-time grader panel **failed** this task — one blocker, two majors — and the
174
+ task was confirmed over that verdict by the area owner rather than re-graded. The
175
+ disagreement is recorded here rather than left in a grade card, because it is a
176
+ disagreement about *scope*, and the next reader deserves both sides.
177
+
178
+ All three findings rest on the same move: the work was judged against the task
179
+ description's **literal asks**, which §1 and §3 above override deliberately and
180
+ with argument. That is a legitimate lens — a grader *should* ask "did you build
181
+ what was asked" — but the task's own text forecloses it: *"Deciding between (a),
182
+ (b) or both is the first task, not an implementation detail. Do not start by
183
+ trying to patch the terminal."*
184
+
185
+ **Finding 1 (blocker) — "no working-indicator / simplified rendering surface was
186
+ built; the literal 'show a working icon' ask is entirely unimplemented, deferred
187
+ to an unfiled follow-on spike."**
188
+
189
+ The first half is **true and intended**: §1 splits the ask and defers the chrome
190
+ half; §4 records the two candidate levers and the bias between them. The second
191
+ half is **factually wrong** — the spike is filed as
192
+ [#1003765](https://cloudbongos.com/builders#/task/1003765), the app-rendered
193
+ surface as [#1003766](https://cloudbongos.com/builders#/task/1003766) with a
194
+ dependency edge on the spike, and both are linked to `wa6-kernel-and-packs`. Both
195
+ were filed *before* the grade ran. "Unfiled" was the load-bearing word in a
196
+ blocker-severity finding, and it was not checked.
197
+
198
+ **Finding 2 (major) — "the ask to hide thinking/tool-call chrome is unaddressed at
199
+ the rendering layer; the ADR itself concedes this is unfinished."**
200
+
201
+ Accurate, and the concession is deliberate (§1, §4). Recorded as an open half with
202
+ its evidence and a named bias, not as a silent omission. Shipping a chrome
203
+ mechanism on this task would have meant building on `viewMode`'s unverified
204
+ committability and a research-preview renderer — §4's three open questions exist
205
+ precisely so that does not happen on a guess.
206
+
207
+ **Finding 3 (major) — "the description asked to recommend bypass (paired with
208
+ narrower blast radius); the shipped decision reverses this entirely rather than
209
+ delivering the paired mitigation the CAUTION section suggested."**
210
+
211
+ This is the substantive one, and the answer is that **the pairing was not
212
+ available**. §3.3: `permissions.defaultMode: "bypassPermissions"` set in a
213
+ project's `.claude/settings.json` or `.claude/settings.local.json` *does not take
214
+ effect* — the session starts in Manual. There is no channel by which a project
215
+ repo can ship a bypass default to be mitigated. And the "narrower blast radius"
216
+ the CAUTION asked to pair it with **already exists**, server-side and
217
+ independently of the prompt: rank checked per request ([ADR 0016](<redacted>.md)) plus the
218
+ sub-Metic protected-path globs. So there was no mitigation left to deliver and
219
+ nothing to attach it to. Rejecting was the honest reading of the CAUTION, not a
220
+ reversal of it.
221
+
222
+ **Why confirmed rather than re-graded.** `--regrade` is the documented way to
223
+ answer a grade with evidence, but it cannot carry this answer:
224
+ [#1003702](https://cloudbongos.com/builders#/task/1003702) records that
225
+ `ship.js --regrade` silently discards `--notes`, *"so an evidence-based grade
226
+ blocker can never be answered."* Re-running the panel would have re-litigated a
227
+ decision this record argues at length, with the rebuttal dropped on the floor. The
228
+ Archon override plus this section is the honest path while 1003702 is open.
229
+
230
+ **What would change this record's mind.** If the 1003765 spike finds `viewMode`
231
+ committable from a project file on the pinned client, Lever A becomes a real
232
+ shipped-default option and §4's bias should be revisited. Nothing in the grade
233
+ findings bears on that question.
@@ -361,3 +361,5 @@ 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 |
365
+ | 0271 | [**The ideator surface is two halves: Bongos owns the prose, the harness owns the chrome** ([task 1003506](https://cloudbongos.com/builders#/task/1003506) · goal 1000095 — *Working area 6, Governor / Builder / Artist / Ideator experience*, criterion `wa6-kernel-and-packs`; owner idea 1000732, 2026-08-16). The ask — *no need for an ideator to see all the text and technical work, or the inner Bongos logic* — is two problems in one sentence. **The PROSE half ships here**, through three seams that already existed: a `machinery` field on `interaction-prefs` (per-PERSON, default `show`, riding the live Conductor injection rail — advisory 8, which means task [#1002514](https://cloudbongos.com/builders#/task/1002514)’s claim that `contractFor()` has zero consumers is STALE); the `ideator` directive in `discipline-modes.json` (per-ROLE, the file whose own docs say *to give a discipline its own experience later, add an entry here*); and a new surface section in `/ideate`, which the directive names as the only home of the experience. Default `show` means no existing builder changes. **The CHROME half is the harness’s and is deferred to a spike, not declared impossible** — this record was nearly written on the wrong assumption that config cannot reach it. Two real levers exist: **A**, shipped client defaults (`/focus` renders *only your last prompt, a one-line tool-call summary, and the final response*; `alwaysThinkingEnabled:false` stops thinking being generated; `showTurnDuration`/`spinnerTipsEnabled`/`statusLine` trim chrome) — but `/focus` is fullscreen-only (research preview), `viewMode`’s committability is UNVERIFIED, and thinking suppression is per-model; and **B**, an app-owned surface over `claude -p --output-format stream-json`, whose documented streaming-UI pattern (working indicator on a `tool_use` block, render on `result`) IS the ask, is model-independent, and is the recorded bias. **Bypass is REJECTED** despite the idea proposing it, on three grounds ending in a technical one: the least technical role is the worst audience for removing confirmations; the prompt was never the gate ([ADR 0016](<redacted>.md) — rank is checked server-side, and sub-Metic protected-path globs already narrow an ideator); and a project **cannot** ship it — `defaultMode: bypassPermissions` in `.claude/settings.json` silently does not take effect and starts the session in Manual. Rejected also: a hall ROOM on the Studio’s precedent (goal 1000074’s *a place, not a form*) — right for an artist, wrong here, because the ideator’s complaint is about the Claude session’s surface, which the hall does not render; and waiting on per-session declared role, whose dependency is softer than filed since `discipline` + the claim-time fork already carry the craft axis. When declaration lands the contract MOVES into the Ideator pack with nothing undone.](<redacted>.md) | roles / ideator experience / session surface |
@@ -1701,5 +1701,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1701
1701
  landed since 1.19.624 with no explicit bump. run 34328603214. (task 1002620)
1702
1702
  1.19.626 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1703
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)
1706
+ 1.19.628 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1707
+ landed since 1.19.627 with no explicit bump. run 34397967061. (task 1002620)
1704
1708
  ---------------------------------------------------------------------------
1705
1709
  ```
@@ -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
+ );
@@ -74,6 +74,34 @@ const FIELDS = {
74
74
  },
75
75
  },
76
76
  },
77
+ // task 1003506 (ADR 0271) — the SURFACE half of the interaction profile. The
78
+ // three fields above tune how a reply is PITCHED (vocabulary, length); this
79
+ // one tunes how much of the WORK BEHIND IT is narrated. An ideator asked for
80
+ // "no need to see all the text and technical work being done, or even the
81
+ // inner Bongos logic" — that is not a vocabulary problem, so it needed its
82
+ // own field rather than another value on technical_level.
83
+ //
84
+ // SCOPE, stated honestly: this governs Claude's own PROSE, which is the only
85
+ // part of the surface a contract can reach. It cannot hide the harness's
86
+ // rendering of tool calls or thinking blocks — see ADR 0271 §4. Default is
87
+ // `show` (today's behavior) so existing builders are untouched; the ideator
88
+ // CRAFT gets `quiet` from its claim directive instead of from this default
89
+ // (scripts/gds/discipline-modes.json), because the surface follows the ROLE
90
+ // and this key follows the PERSON.
91
+ machinery: {
92
+ label: 'Machinery in replies',
93
+ blurb: 'Whether replies narrate the tools, file paths, and Bongos internals the work went through.',
94
+ values: {
95
+ show: {
96
+ label: 'Show the work (default)',
97
+ contract: '',
98
+ },
99
+ quiet: {
100
+ label: 'Hide the machinery',
101
+ contract: 'Machinery: quiet — report OUTCOMES, not mechanism. Say what changed and what it means for the reader; do not narrate tool calls, file paths, script names, API routes, task/claim bookkeeping, or Bongos internals unless they ask or something actually failed. When a step fails, say plainly what broke and what you need — a quiet surface never hides a problem.',
102
+ },
103
+ },
104
+ },
77
105
  };
78
106
 
79
107
  const FIELD_KEYS = Object.keys(FIELDS);
@@ -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';
@@ -23,6 +23,31 @@ const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
23
23
  // -------------------------------------------------------------------------
24
24
 
25
25
 
26
+ // goalScopeProtected — does this task's owning goal reach protected territory
27
+ // (the permission / ship-grade / deploy / migration core)? The touches-free half
28
+ // of the create-time rank floor (task 1003663, ADR 0270).
29
+ //
30
+ // It reads the goal's `scope_modules` wall through the SAME published primitive
31
+ // modules/ideas/inbox.js already uses for routed filings (task 1003066, BV1.R05)
32
+ // and that the pre-push hook, the grader and main-audit read — so this floor
33
+ // cannot drift from the roster they enforce.
34
+ //
35
+ // Runs on the CALLER'S client when there is one, so it sees the same
36
+ // transaction snapshot as the INSERT it feeds — a goal rescoped mid-transaction
37
+ // cannot land between the check and the row. FAILS CLOSED-ish by omission: an
38
+ // absent goal or a non-array wall yields false, which is the honest answer
39
+ // ("no protected scope declared"), and the other three arms of deriveRequiredRank
40
+ // still apply.
41
+ async function goalScopeProtected(goalId, client) {
42
+ if (goalId == null) return false;
43
+ const { rows } = await (client || pool).query(
44
+ 'SELECT scope_modules FROM goals WHERE id = $1',
45
+ [goalId]
46
+ );
47
+ const scope = rows.length ? rows[0].scope_modules : null;
48
+ return Array.isArray(scope) && api.moduleScopeMap.scopeIncludesProtected(scope);
49
+ }
50
+
26
51
  // The version statuses that can still receive new work. 'shipped' and 'frozen' are
27
52
  // closed: a version whose scope is sealed cannot be told about a bug found afterwards.
28
53
  // Mirrors the CHECK constraint on versions.status (migrations/003_pms.sql).
@@ -124,15 +149,6 @@ async function createTask({
124
149
  // to name a goal instead.
125
150
  allowCatchAll = false,
126
151
  }, deps = {}) { // deps.client enlists this INSERT in the CALLER'S transaction (task 1003065, BV1.R01); the argument for it is at modules/ideas/inbox.js routeIdeaToGoal
127
- // task 1498 / ADR 0085: every task carries a deliberate rank floor. Derive it
128
- // from touches + sensitivity, then let an explicit requiresRank override only
129
- // UPWARD (highestRank) — a requested rank below the derived floor is clamped
130
- // up to the floor (fail-closed; the floor mirrors the trust-boundary paths a
131
- // sub-Metic builder cannot push to anyway).
132
- const requiresRankEffective = highestRank(
133
- deriveRequiredRank(touches, securitySensitive),
134
- requiresRank ?? 'xenos'
135
- );
136
152
  // task 1677 / ADR 0096 (2026-06-28): creation is now the PRIMARY reward-assignment
137
153
  // point — every task is born with a fair drachmae value, not just at promote/claim.
138
154
  // When the caller supplies NO positive credits_reward (null/undefined/0), auto-assign
@@ -181,6 +197,26 @@ async function createTask({
181
197
  }
182
198
  goalIdEffective = await generalGoalIdForVersion(versionId);
183
199
  }
200
+ // task 1498 / ADR 0084: every task carries a deliberate rank floor. Derive it,
201
+ // then let an explicit requiresRank override only UPWARD (highestRank) — a
202
+ // requested rank below the derived floor is clamped up to the floor
203
+ // (fail-closed; the floor mirrors the trust-boundary paths a sub-Metic builder
204
+ // cannot push to anyway).
205
+ //
206
+ // task 1003663 / ADR 0270: derived HERE, at the lowest create layer and AFTER
207
+ // goalIdEffective resolves, rather than in each route — the same argument the
208
+ // reward auto-assignment above makes. Every create vector (POST /tasks,
209
+ // POST /goals/:id/tasks, idea-promotion, capture, newcomer-restock, copy-desk,
210
+ // kickoff-seed, seed scripts) inherits the floor, and a vector added next year
211
+ // cannot forget it. Patching the two authoring routes instead is how
212
+ // POST /tasks and POST /goals/:id/tasks came to disagree in the first place.
213
+ const requiresRankEffective = highestRank(
214
+ deriveRequiredRank(touches, securitySensitive, {
215
+ needsMigration,
216
+ scopeProtected: await goalScopeProtected(goalIdEffective, deps.client),
217
+ }),
218
+ requiresRank ?? 'xenos'
219
+ );
184
220
  const { rows } = await (deps.client || pool).query(
185
221
  `INSERT INTO tasks (version_id, title, description, status, touches,
186
222
  est_minutes, est_cost_usd, manual_degree, priority,
@@ -366,7 +402,7 @@ async function updateTaskNewcomerFriendly(id, newcomerFriendly) { return updateT
366
402
  // (the === true coercion below preserves the boolean-only contract).
367
403
 
368
404
  async function updateTaskSecuritySensitive(id, securitySensitive) {
369
- // task 1498 / ADR 0085: flagging a task security_sensitive RAISES its rank
405
+ // task 1498 / ADR 0084: flagging a task security_sensitive RAISES its rank
370
406
  // floor (a now-sensitive task must require Metic+). Recompute and ratchet
371
407
  // requires_rank UP in the same atomic UPDATE so the two columns can't diverge;
372
408
  // never lower it (an existing 'metic'/'archon' may be a protected-touch floor
@@ -386,7 +422,7 @@ async function updateTaskSecuritySensitive(id, securitySensitive) {
386
422
  return rows[0] ?? null;
387
423
  }
388
424
 
389
- // task 1498 / ADR 0085: set a task's rank floor explicitly (the Archon override).
425
+ // task 1498 / ADR 0084: set a task's rank floor explicitly (the Archon override).
390
426
  // The DERIVED floor (touches + security_sensitive) is a HARD minimum — a request
391
427
  // below it is clamped UP via highestRank, so an override can only RAISE the bar,
392
428
  // never open a sensitive task to a lower rank. `requiresRank` is one live rank
@@ -396,7 +432,15 @@ async function updateTaskSecuritySensitive(id, securitySensitive) {
396
432
  async function updateTaskRequiresRank(id, requiresRank) {
397
433
  const task = await getTask(id);
398
434
  if (!task) return null;
399
- const floor = deriveRequiredRank(task.touches, task.security_sensitive);
435
+ // task 1003663 / ADR 0270: the override re-derives with the SAME four inputs as
436
+ // creation. Without needsMigration/scopeProtected here, an Archon setting a
437
+ // migration task to 'xenos' would succeed — the floor would compute 'xenos'
438
+ // from an empty touches[] and clamp nothing, silently undoing the create-time
439
+ // protection. An override may still only RAISE.
440
+ const floor = deriveRequiredRank(task.touches, task.security_sensitive, {
441
+ needsMigration: task.needs_migration === true,
442
+ scopeProtected: await goalScopeProtected(task.goal_id, null),
443
+ });
400
444
  const effective = highestRank(floor, requiresRank ?? 'xenos');
401
445
  const { rows } = await pool.query(
402
446
  `UPDATE tasks SET requires_rank = $1, updated_at = now() WHERE id = $2 RETURNING *`,
@@ -508,7 +552,32 @@ async function updateTaskAutomationTag(id, automationTag) { return updateTaskCol
508
552
  // task 1002648: correct the create-time migration heuristic's verdict — a false
509
553
  // flag makes claiming reserve a migration number that goes unused (task 1002614
510
554
  // burned 213 that way). Boolean only; the route validates.
511
- async function updateTaskNeedsMigration(id, needsMigration) { return updateTaskColumn(id, 'needs_migration', needsMigration === true); }
555
+ //
556
+ // task 1003663 / ADR 0270: needs_migration is now a rank-floor INPUT, so this
557
+ // stopped being a bare column write and became the exact sibling of
558
+ // updateTaskSecuritySensitive above — same CASE, same ratchet, same reasoning.
559
+ // The correction this setter exists for runs BOTH ways: task 1002648 names the
560
+ // true→false direction, but false→true is the one that matters here, and without
561
+ // the ratchet a Metic could PATCH needs_migration onto an existing xenos task and
562
+ // leave the floor at xenos — reopening on the PATCH path exactly the gap ADR 0270
563
+ // closes on the create path. RAISES ONLY: flipping the flag back off never drops
564
+ // the gate, because an existing 'metic'/'archon' may be a protected-scope floor or
565
+ // a deliberate override. An Archon lowers it explicitly via updateTaskRequiresRank,
566
+ // which re-derives from all four inputs.
567
+ async function updateTaskNeedsMigration(id, needsMigration) {
568
+ const mig = needsMigration === true;
569
+ const { rows } = await pool.query(
570
+ `UPDATE tasks
571
+ SET needs_migration = $1,
572
+ requires_rank = CASE WHEN $1 AND requires_rank IN ('xenos', 'thetes')
573
+ THEN 'metic' ELSE requires_rank END,
574
+ updated_at = now()
575
+ WHERE id = $2
576
+ RETURNING *`,
577
+ [mig, id]
578
+ );
579
+ return rows[0] ?? null;
580
+ }
512
581
 
513
582
  // BV1.R53 (task 1422): assign (or clear) the module_key that declares which
514
583
  // module this task's work belongs to. Drives module-disjointness at claim time
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.626",
3
+ "version": "1.19.628",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.626",
9
+ "version": "1.19.628",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.626",
3
+ "version": "1.19.628",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9,7 +9,7 @@
9
9
  "ideator": {
10
10
  "skill": "ideate",
11
11
  "label": "Ideation session",
12
- "directive": "This is an IDEATOR task. To work it, invoke the /ideate skill — that skill is the operating playbook for ideation, and it is the only place the experience is defined. It inverts the usual dynamic: you (the human ideator) are the creative driver and Claude is your sounding board / research arm / devil's advocate, not a content generator filing ideas on command. If no human is present (autonomous / bypass-permissions), /ideate runs a self-adversarial divergence-and-critique loop instead of dumping a fixed batch. Open /ideate now rather than working this task the default way."
12
+ "directive": "This is an IDEATOR task. To work it, invoke the /ideate skill — that skill is the operating playbook for ideation, and it is the only place the experience is defined. It inverts the usual dynamic: you (the human ideator) are the creative driver and Claude is your sounding board / research arm / devil's advocate, not a content generator filing ideas on command. If no human is present (autonomous / bypass-permissions), /ideate runs a self-adversarial divergence-and-critique loop instead of dumping a fixed batch. Open /ideate now rather than working this task the default way. THE SURFACE, not just the behavior (ADR 0271): an ideator session is QUIET about its own machinery. Report outcomes, not mechanism — do not narrate tool calls, file paths, script names, API routes or task/claim bookkeeping unless they ask or something actually failed. Speak in the language of the idea, not the plumbing. If a step breaks, say plainly what broke and what you need — quiet never means hiding a problem."
13
13
  },
14
14
  "artist": {
15
15
  "skill": "paint",
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.626'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.628'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -65,8 +65,50 @@ test('applyPatch merges and null clears', () => {
65
65
 
66
66
  test('describeForApi exposes catalog + effective + contract + inject', () => {
67
67
  const d = ip.describeForApi({ interaction: { verbosity: 'detailed' } });
68
- assert.ok(Array.isArray(d.fields) && d.fields.length === 3);
68
+ assert.ok(Array.isArray(d.fields));
69
+ assert.deepEqual(d.fields.map((f) => f.field), ip.FIELD_KEYS, "the catalog must expose every field, in order");
69
70
  assert.equal(d.effective.verbosity, 'detailed');
70
71
  assert.equal(d.inject, true);
71
72
  assert.match(d.contract, /detailed/);
72
73
  });
74
+
75
+ // ---- the machinery field (task 1003506 / ADR 0270) ----
76
+ // The SURFACE half of the profile: how much of the work behind a reply gets
77
+ // narrated. Default `show` keeps every existing builder on today's behavior.
78
+
79
+ test('machinery defaults to show and injects nothing', () => {
80
+ assert.equal(ip.defaultValue('machinery'), 'show');
81
+ assert.equal(ip.effectiveProfile({}).machinery, 'show');
82
+ assert.equal(ip.contractFor({ interaction: { machinery: 'show' } }), '', 'the default must never spend session context');
83
+ });
84
+
85
+ test('machinery=quiet injects an outcomes-not-mechanism contract', () => {
86
+ const prefs = { interaction: { machinery: 'quiet' } };
87
+ assert.equal(ip.effectiveProfile(prefs).machinery, 'quiet');
88
+ assert.ok(ip.shouldInject(prefs));
89
+ const c = ip.contractFor(prefs);
90
+ assert.match(c, /Machinery: quiet/);
91
+ assert.match(c, /OUTCOMES, not mechanism/);
92
+ });
93
+
94
+ test('quiet still requires reporting a real failure (it is not a gag)', () => {
95
+ // Load-bearing: a surface that hides a broken step is worse than a noisy one,
96
+ // so the contract itself must carry the exception.
97
+ const c = ip.contractFor({ interaction: { machinery: 'quiet' } });
98
+ assert.match(c, /fails/, 'the contract must name the failure exception');
99
+ });
100
+
101
+ test('machinery validates like every other field', () => {
102
+ assert.equal(ip.validatePatch({ machinery: 'quiet' }).ok, true);
103
+ const bad = ip.validatePatch({ machinery: 'silent' });
104
+ assert.equal(bad.ok, false);
105
+ assert.deepEqual(bad.errors[0].allowed, ['show', 'quiet']);
106
+ });
107
+
108
+ test('machinery composes with the pitch fields rather than replacing them', () => {
109
+ // The three original fields tune HOW a reply is pitched; machinery tunes how
110
+ // much of the work behind it is shown. Both must appear together.
111
+ const c = ip.contractFor({ interaction: { technical_level: 'engineer', machinery: 'quiet' } });
112
+ assert.match(c, /engineer/);
113
+ assert.match(c, /Machinery: quiet/);
114
+ });
@@ -0,0 +1,135 @@
1
+ // tests/rank_floor_wiring.mjs — the rank floor is WIRED, not merely derivable
2
+ // (task 1003663, goal 1000111, ADR 0270).
3
+ //
4
+ // WHY THIS FILE EXISTS SEPARATELY FROM tests/rank_gate.mjs. That file pins the
5
+ // PURE helpers — `deriveRequiredRank` / `highestRank` — by handing them opts it
6
+ // builds itself. That is necessary and it is not sufficient: the whole defect
7
+ // ADR 0270 repairs was a derivation that was perfectly correct and never
8
+ // REACHED, because the input it needed (`touches[]`) stopped arriving at the
9
+ // call site. A test that constructs the inputs by hand cannot catch that class
10
+ // of bug a second time — it asserts the calculator, not the plumbing.
11
+ //
12
+ // So this file drives the real `createTask` against a SQL-aware client and reads
13
+ // the `requires_rank` actually bound into the INSERT, plus the two PATCH setters
14
+ // that can move a floor input after creation.
15
+ //
16
+ // Run: node --test tests/rank_floor_wiring.mjs
17
+
18
+ import assert from 'node:assert/strict';
19
+ import { test } from 'node:test';
20
+ import { createRequire } from 'node:module';
21
+ import { makeSqlAwareClient, lifecycleDbSource } from './helpers.mjs';
22
+
23
+ process.env.NODE_ENV = 'test';
24
+ const require = createRequire(import.meta.url);
25
+ const { createTask } = require('../modules/lifecycle/db.js');
26
+
27
+ // The INSERT binds ~25 columns positionally, so a hardcoded params[20] would be a
28
+ // silent lie the first time somebody inserts a column. Read the column list out of
29
+ // the statement under test and resolve the index by NAME.
30
+ function boundValue(sql, params, column) {
31
+ const cols = String(sql).slice(sql.indexOf('(') + 1, sql.indexOf(')'))
32
+ .split(',').map((c) => c.trim());
33
+ const i = cols.indexOf(column);
34
+ assert.notEqual(i, -1, `INSERT INTO tasks no longer binds a '${column}' column — update this test`);
35
+ return params[i];
36
+ }
37
+
38
+ // One create against a fake goal row; returns the requires_rank the INSERT bound.
39
+ async function createdFloor({ scopeModules, needsMigration = false, requiresRank = null }) {
40
+ let insert = null;
41
+ const { _client } = makeSqlAwareClient(async (sql, params) => {
42
+ if (/^SELECT scope_modules FROM goals/i.test(sql)) {
43
+ return { rows: [{ scope_modules: scopeModules }] };
44
+ }
45
+ if (/^INSERT INTO tasks/i.test(sql)) {
46
+ insert = { sql, params };
47
+ return { rows: [{ id: '9001' }] };
48
+ }
49
+ return { rows: [] };
50
+ });
51
+
52
+ await createTask({
53
+ versionId: 'BONGOS-V2',
54
+ title: 'a task',
55
+ goalId: '1000111',
56
+ needsMigration,
57
+ requiresRank,
58
+ }, { client: _client });
59
+
60
+ assert.ok(insert, 'createTask never reached its INSERT');
61
+ return boundValue(insert.sql, insert.params, 'requires_rank');
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // createTask — the create path actually reads the goal wall
66
+ // ---------------------------------------------------------------------------
67
+
68
+ test('a goal whose wall reaches protected territory floors the task at metic', async () => {
69
+ // No touches[], not security_sensitive, not a migration — every pre-0270 input
70
+ // is absent, which is precisely the shape that used to come out 'xenos'.
71
+ assert.equal(await createdFloor({ scopeModules: ['hall-ui', 'kernel'] }), 'metic');
72
+ });
73
+
74
+ test('an unprotected wall leaves the task in the open queue', async () => {
75
+ assert.equal(await createdFloor({ scopeModules: ['hall-ui'] }), 'xenos');
76
+ });
77
+
78
+ test('needs_migration floors at metic even under an unprotected wall', async () => {
79
+ assert.equal(await createdFloor({ scopeModules: ['hall-ui'], needsMigration: true }), 'metic');
80
+ });
81
+
82
+ test('THE REGRESSION: the create path reads the wall, it does not just own a helper', async () => {
83
+ // Mutation guard. If someone drops the goalScopeProtected call from createTask
84
+ // — the exact way this bug happened the first time, by a caller ceasing to
85
+ // supply an input — the pure tests in rank_gate.mjs all still pass and this one
86
+ // fails. That asymmetry is the point of the file.
87
+ assert.equal(await createdFloor({ scopeModules: ['lifecycle'] }), 'metic');
88
+ assert.equal(await createdFloor({ scopeModules: ['government'] }), 'metic');
89
+ });
90
+
91
+ test('an explicit requires_rank still only RAISES, never lowers the derived floor', async () => {
92
+ assert.equal(await createdFloor({ scopeModules: ['kernel'], requiresRank: 'xenos' }), 'metic');
93
+ assert.equal(await createdFloor({ scopeModules: ['hall-ui'], requiresRank: 'archon' }), 'archon');
94
+ });
95
+
96
+ test('a missing goal row is the honest false, not a throw', async () => {
97
+ // scope_modules is NOT NULL DEFAULT '{}', but a goal deleted mid-transaction
98
+ // returns no row at all. That must fall through to the other arms, not crash a
99
+ // create.
100
+ assert.equal(await createdFloor({ scopeModules: undefined }), 'xenos');
101
+ assert.equal(await createdFloor({ scopeModules: undefined, needsMigration: true }), 'metic');
102
+ });
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // The PATCH setters — a floor INPUT may be flipped after creation
106
+ // ---------------------------------------------------------------------------
107
+ // These two write through the module-level pool rather than an injectable client,
108
+ // so they are pinned at the source. The assertion is narrow on purpose: it is the
109
+ // RATCHET that matters (raise on true, never lower), not the surrounding SQL.
110
+
111
+ test('updateTaskNeedsMigration ratchets requires_rank UP, exactly like its security_sensitive sibling', () => {
112
+ const src = lifecycleDbSource();
113
+ const setter = src.slice(src.indexOf('async function updateTaskNeedsMigration'));
114
+ const body = setter.slice(0, setter.indexOf('\n}\n') + 1);
115
+
116
+ assert.match(
117
+ body.replace(/\s+/g, ' '),
118
+ /requires_rank = CASE WHEN \$1 AND requires_rank IN \('xenos', 'thetes'\) THEN 'metic' ELSE requires_rank END/,
119
+ 'PATCHing needs_migration=true must raise the floor — otherwise ADR 0270 is closed on the create path and open on the PATCH path'
120
+ );
121
+ // Raise-only: nothing in the setter may write a rank BACK DOWN when the flag
122
+ // clears. An existing metic may be a protected-scope floor or an Archon override.
123
+ assert.doesNotMatch(body, /ELSE 'xenos'/, 'the setter must never lower an existing floor');
124
+ });
125
+
126
+ test('both floor-input setters share one ratchet — neither may drift from the other', () => {
127
+ const src = lifecycleDbSource();
128
+ const ratchet = /requires_rank = CASE WHEN \$1 AND requires_rank IN \('xenos', 'thetes'\)\s+THEN 'metic' ELSE requires_rank END/g;
129
+ const hits = src.match(ratchet) || [];
130
+ assert.equal(
131
+ hits.length, 2,
132
+ `expected the ratchet in exactly 2 setters (security_sensitive + needs_migration), found ${hits.length} — ` +
133
+ 'a new boolean that feeds deriveRequiredRank needs one too, and a removed one is a reopened gap'
134
+ );
135
+ });
@@ -399,6 +399,51 @@ t('empty / null / undefined touches with no sensitivity => xenos (no throw)', ()
399
399
  assert.equal(db.deriveRequiredRank(undefined, undefined), 'xenos');
400
400
  });
401
401
 
402
+ // ---------------------------------------------------------------------------
403
+ // task 1003663 / ADR 0270 — the two touches-FREE inputs. ADR 0049 retired
404
+ // predictive touches[], so at CREATE time the arms above see an empty array on
405
+ // most vectors and the floor never rose: 68 of 88 needs_migration tasks sat at
406
+ // xenos, one of them a ready task to rewrite the auth token store. These pin the
407
+ // replacement, which derives from facts a task carries WITHOUT a prediction.
408
+ // ---------------------------------------------------------------------------
409
+ console.log('\nderiveRequiredRank (the touches-free floors — task 1003663):');
410
+
411
+ t('needs_migration floors at metic with NO touches at all', () => {
412
+ assert.equal(db.deriveRequiredRank([], false, { needsMigration: true }), 'metic');
413
+ assert.equal(db.deriveRequiredRank(null, false, { needsMigration: true }), 'metic');
414
+ });
415
+
416
+ t('a protected goal scope floors at metic with NO touches at all', () => {
417
+ assert.equal(db.deriveRequiredRank([], false, { scopeProtected: true }), 'metic');
418
+ assert.equal(db.deriveRequiredRank(null, false, { scopeProtected: true }), 'metic');
419
+ });
420
+
421
+ t('THE REGRESSION: the exact shape of task 1003372 (ready, migration, no touches)', () => {
422
+ // Before ADR 0270 this returned 'xenos' — a Xenos could claim a task to hash
423
+ // the builder_sessions token store at rest. It must never do so again.
424
+ assert.equal(
425
+ db.deriveRequiredRank([], false, { needsMigration: true, scopeProtected: false }),
426
+ 'metic'
427
+ );
428
+ });
429
+
430
+ t('the flags are opt-in: false / absent / non-boolean never RAISE the floor', () => {
431
+ // Only a strict === true raises, so a stray truthy value cannot quietly wall
432
+ // off the open queue (the fail-closed direction here is "stay open").
433
+ assert.equal(db.deriveRequiredRank([], false, {}), 'xenos');
434
+ assert.equal(db.deriveRequiredRank([], false, { needsMigration: false, scopeProtected: false }), 'xenos');
435
+ assert.equal(db.deriveRequiredRank([], false, { needsMigration: 'yes' }), 'xenos');
436
+ assert.equal(db.deriveRequiredRank([], false, { needsMigration: 1 }), 'xenos');
437
+ });
438
+
439
+ t('omitting opts entirely is the pre-0270 two-argument behaviour, unchanged', () => {
440
+ // Every existing call site passes two arguments; none may change meaning.
441
+ assert.equal(db.deriveRequiredRank([], false), 'xenos');
442
+ assert.equal(db.deriveRequiredRank(['public/game/main.js'], false), 'xenos');
443
+ assert.equal(db.deriveRequiredRank(['migrations/145_x.sql'], false), 'metic');
444
+ assert.equal(db.deriveRequiredRank([], true), 'metic');
445
+ });
446
+
402
447
  console.log('\nhighestRank (compose floor with an override; raise-only):');
403
448
 
404
449
  t('picks the higher-authority rank either way', () => {