@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 +41 -21
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/ideate/SKILL.md +11 -0
- package/docs/adr/0270-rank-floor-derives-without-a-prediction.md +59 -0
- package/docs/adr/0271-the-ideator-surface-is-two-halves.md +233 -0
- package/docs/adr/README.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_236_rank_floor_backfill.sql +56 -0
- package/modules/builder-settings/interaction-prefs.js +28 -0
- package/modules/lifecycle/db-rank-authz.js +27 -2
- package/modules/lifecycle/db-tasks.js +82 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/discipline-modes.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/interaction_prefs.mjs +43 -1
- package/tests/rank_floor_wiring.mjs +135 -0
- package/tests/rank_gate.mjs +45 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.628",
|
|
6
|
+
"core_contract": "1.19.628",
|
|
7
|
+
"source_commit": "4f99983713ad375f7f42c932c781c3c77a47bc66",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-09T19:56:16.788Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 466,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2098,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7700
|
+
"sha256": "aba13c3ae1fa839b97bc3f11aff342251f550f7cbcc8531a9ae3c6856f326d6e"
|
|
7686
7701
|
},
|
|
7687
7702
|
{
|
|
7688
7703
|
"path": "package.json",
|
|
7689
7704
|
"mode": "0000644",
|
|
7690
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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.
|
package/docs/adr/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
+
});
|
package/tests/rank_gate.mjs
CHANGED
|
@@ -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', () => {
|