@bongos/core 1.19.627 → 1.19.629

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.627",
6
- "core_contract": "1.19.627",
7
- "source_commit": "9132daa97509f28f38b53cf54a6311f0a43f788d",
5
+ "core_version": "1.19.629",
6
+ "core_contract": "1.19.629",
7
+ "source_commit": "80bf853bd558fa5134bd5bddcb0e4804579fc27e",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T18:37:19.003Z",
9
+ "built_at": "2026-09-09T22:26:18.376Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 465,
12
+ "docs_redacted": 466,
13
13
  "agent_docs_stubbed": 24,
14
14
  "functional_verbatim": 2098,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2587,
20
- "tree_sha256": "6f56a454e970eef9adac1d969b0e28f75d79cdc9843f666de389fd2766607e55",
19
+ "file_count": 2588,
20
+ "tree_sha256": "c5bd187e5ed644428b20c65f6fee6c2b6b122ae4952a3e36e19b84cfa2e4d722",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-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",
@@ -332,7 +332,7 @@
332
332
  {
333
333
  "path": ".gitattributes",
334
334
  "mode": "0000644",
335
- "sha256": "7f5faf22678ad182025b2f3f9107c209f92e461200e15665fa2feb4a3cd86858"
335
+ "sha256": "83c94b055dc619fdbf3bc1142fc5357810d901e4200644f1fb399e6939571c5f"
336
336
  },
337
337
  {
338
338
  "path": "CLAUDE.md",
@@ -1874,10 +1874,15 @@
1874
1874
  "mode": "0000644",
1875
1875
  "sha256": "a16ee8bb0d163fec8ffce252e165de4010954e45a7f61bc205163c70077451ee"
1876
1876
  },
1877
+ {
1878
+ "path": "docs/adr/0271-the-ideator-surface-is-two-halves.md",
1879
+ "mode": "0000644",
1880
+ "sha256": "0d4805df73af0b99c6dfd1c362debaa04d93a8a075f8e4b0cdf11af975bfc62c"
1881
+ },
1877
1882
  {
1878
1883
  "path": "docs/adr/README.md",
1879
1884
  "mode": "0000644",
1880
- "sha256": "26757599a787417d621bd760d61f156f41b12dc73a195d82b912cc6e956d5642"
1885
+ "sha256": "9b75ec13500719a639364bcc9436e9f0cde63119a032c805312153dbb006f390"
1881
1886
  },
1882
1887
  {
1883
1888
  "path": "docs/api-reference.md",
@@ -2762,7 +2767,7 @@
2762
2767
  {
2763
2768
  "path": "docs/module-api-changelog.md",
2764
2769
  "mode": "0000644",
2765
- "sha256": "ccefe35e8bc2cd1de16f6164b9d2782d9dbd6c5c8660ab75e7a0bf198078d2dc"
2770
+ "sha256": "aba3d7e763196a3052a53ee81f5327e0caad5274bd4de41b09b031b2277c82d2"
2766
2771
  },
2767
2772
  {
2768
2773
  "path": "docs/modules-contract.md",
@@ -3007,7 +3012,7 @@
3007
3012
  {
3008
3013
  "path": "docs/recipes/windows-builders.md",
3009
3014
  "mode": "0000644",
3010
- "sha256": "229da205775c6b84534633bc15be7355337d0d8cfacd81ff03860bc24bfc783f"
3015
+ "sha256": "b7d849d1786c7eca4156969d38343425839bef3bae19a10a94b5e54916e3ea09"
3011
3016
  },
3012
3017
  {
3013
3018
  "path": "docs/recipes/work-on-a-task-from-your-dev-box.md",
@@ -3862,7 +3867,7 @@
3862
3867
  {
3863
3868
  "path": "modules/builder-settings/interaction-prefs.js",
3864
3869
  "mode": "0000644",
3865
- "sha256": "5a1184ee4ef1db413358e41fd4ce3f88a1e99cfe4f2ff44bd9bcc4182bc3de64"
3870
+ "sha256": "64575386423570872fd0b10751cc1863cc813c992ef27d0be26b7b924d9c6f52"
3866
3871
  },
3867
3872
  {
3868
3873
  "path": "modules/builder-settings/model-alloc-prefs.js",
@@ -7692,12 +7697,12 @@
7692
7697
  {
7693
7698
  "path": "package-lock.json",
7694
7699
  "mode": "0000644",
7695
- "sha256": "72fea99ed1a6a192978e5b257da8b6c819a649e210da1395d7fdace85b35cd4d"
7700
+ "sha256": "41fdf5ef9ec5b4cb8a9f7a85f0efeba2595895fc5688c3c78702652fe4002d67"
7696
7701
  },
7697
7702
  {
7698
7703
  "path": "package.json",
7699
7704
  "mode": "0000644",
7700
- "sha256": "22510ead9bfe323615390297a28fab238a3a053defb3849e071f7ad66b84aa0d"
7705
+ "sha256": "c8bb2a1526d2abc3ba1bcb859d83ca86a081fd7c89a178f45f5b39a481ecaebe"
7701
7706
  },
7702
7707
  {
7703
7708
  "path": "public-docs/index.html",
@@ -8007,7 +8012,7 @@
8007
8012
  {
8008
8013
  "path": "scripts/gds/copy-inventory.js",
8009
8014
  "mode": "0000644",
8010
- "sha256": "8c798359b51abdd749233e65aa5906732bccc5b9088dae32513cde7949404b6c"
8015
+ "sha256": "7269164648fc1d00e1bf3272a82f933a5f8da228e562e00848dbc29c671d3e94"
8011
8016
  },
8012
8017
  {
8013
8018
  "path": "scripts/gds/coreB-carve-proof.sh",
@@ -8072,7 +8077,7 @@
8072
8077
  {
8073
8078
  "path": "scripts/gds/discipline-modes.json",
8074
8079
  "mode": "0000644",
8075
- "sha256": "6b740035b0d793dd2063de32b660a202da518b920742892a09058943c54e60c5"
8080
+ "sha256": "c0c1849145999ef02f3022b22e718da8bb4fb680020124d115423f2f868834d1"
8076
8081
  },
8077
8082
  {
8078
8083
  "path": "scripts/gds/discord-channels.js",
@@ -9437,7 +9442,7 @@
9437
9442
  {
9438
9443
  "path": "src/module-api.js",
9439
9444
  "mode": "0000644",
9440
- "sha256": "a28b94a91c283d898bed5fe0810e029e31b3ab33760b2c132a320058d3692bff"
9445
+ "sha256": "46df738e31dab69636774474ec6714365bf625a395df0a0e5ab4ecbacf178b16"
9441
9446
  },
9442
9447
  {
9443
9448
  "path": "src/module-loader/catalog.js",
@@ -10132,7 +10137,7 @@
10132
10137
  {
10133
10138
  "path": "tests/copy_inventory.mjs",
10134
10139
  "mode": "0000644",
10135
- "sha256": "97ff5dac1f7bef96cf0f80082e8f6ebb2cea30f3c371c3ab080e9189818fb5fd"
10140
+ "sha256": "716aa03c5e93b71aec0c7e6fe646e5abd702b8b6ffda0ef6235a804d2c17a0ca"
10136
10141
  },
10137
10142
  {
10138
10143
  "path": "tests/cosmos_token_contrast.mjs",
@@ -10512,7 +10517,7 @@
10512
10517
  {
10513
10518
  "path": "tests/git_merge_regen.mjs",
10514
10519
  "mode": "0000644",
10515
- "sha256": "1d5f97589b84b493f7406fba3b162443a70fbf0ad24190049d054dbd2fe17273"
10520
+ "sha256": "c6cb1bda6809d8d384a9c35b376995af038d18c26b1b935cb5c0faebb163b72e"
10516
10521
  },
10517
10522
  {
10518
10523
  "path": "tests/github_push_ancestry.mjs",
@@ -11287,7 +11292,7 @@
11287
11292
  {
11288
11293
  "path": "tests/interaction_prefs.mjs",
11289
11294
  "mode": "0000644",
11290
- "sha256": "d00e82184fbf7aab49c9b6efe5709b14f300ca0cea277a0eb140f073ded7ff50"
11295
+ "sha256": "e124b44e734806a3ee30b06bcaf3837237d98abf62e68a3709f4dab78c38a83c"
11291
11296
  },
11292
11297
  {
11293
11298
  "path": "tests/join_application_note.mjs",
@@ -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?**
package/.gitattributes CHANGED
@@ -85,8 +85,18 @@ src/bongos/routes/CLAUDE.md merge=otb-regen
85
85
  # files. Same otb-regen shape as docs/session-log-index.md above (whole-file
86
86
  # rebuild, all three merge sides ignored); REGENERABLE in git-merge-regen.js
87
87
  # carries the matching rule.
88
+ #
89
+ # The `text eol=lf` on the .md (task 1003619) is NOT cosmetic — it is what keeps
90
+ # `copy-inventory.js --check` honest on Windows. The generator writes LF and the
91
+ # check string-compares its output against the worktree file, so under the default
92
+ # `* text=auto` above (index LF, worktree CRLF) the check reported STALE forever on
93
+ # every Windows checkout while the committed content was already correct. The .json
94
+ # never had the bug because `*.json text eol=lf` above already pins it; the .md fell
95
+ # through to text=auto because no `*.md` rule exists (and none should — pinning all
96
+ # 685 tracked .md files would renormalize the whole docs tree for one generated file).
97
+ # `*.md` deliberately stays unpinned; only the GENERATED artifact is pinned here.
88
98
  docs/copy-registry.json merge=otb-regen
89
- docs/copy-inventory.md merge=otb-regen
99
+ docs/copy-inventory.md merge=otb-regen text eol=lf
90
100
 
91
101
  # Append-only index tables (task 1385 / ADR 0082). docs/adr/README.md is a
92
102
  # hand-maintained ADR index whose ONLY recurring conflict is two branches each
@@ -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.
@@ -362,3 +362,4 @@ This keeps the decision history honest and traceable.
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
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 |
@@ -1703,5 +1703,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1703
1703
  landed since 1.19.625 with no explicit bump. run 34363350286. (task 1002620)
1704
1704
  1.19.627 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1705
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)
1708
+ 1.19.629 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1709
+ landed since 1.19.628 with no explicit bump. run 34412159317. (task 1002620)
1706
1710
  ---------------------------------------------------------------------------
1707
1711
  ```
@@ -147,3 +147,4 @@ caught a real bug during task 1003548, so read its assertion before blaming the
147
147
 
148
148
  - Several hall files (`people.css`, `people.html`, `style.css`, the privacy spec) and every `.claude/skills/*/SKILL.md` are CRLF; `scripts/gds/*.js` and the tests are LF. An exact-string patch must detect the file's EOL before joining lines (`text.includes('\r\n') ? '\r\n' : '\n'`), or the find matches nothing and the edit is a silent no-op (session log 2026-08-24).
149
149
  - A `git diff --stat` that touches every line of a file you barely edited is an EOL flip — check with `git diff --stat --ignore-cr-at-eol`, and restore the original EOL before committing.
150
+ - **`copy-inventory.js --check` no longer reds on line endings alone** (task 1003619). It used to: the generator emits LF and the check byte-compared that against a CRLF worktree file, so `/ship-check` showed one permanent red line on every Windows checkout while the committed content was already correct — and a builder who "fixed" it committed a whole-file CRLF diff. `docs/copy-inventory.md` is now pinned `text eol=lf` in `.gitattributes` (its `.json` sibling always was, via `*.json`), and the check compares EOL-blind so an already-CRLF worktree passes without a re-clone. If it reports STALE now, the content really is stale — regenerate, don't reach for `--ignore-cr-at-eol`. Note `*.md` is deliberately NOT pinned repo-wide; only that one generated artifact is.
@@ -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);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.627",
3
+ "version": "1.19.629",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.627",
9
+ "version": "1.19.629",
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.627",
3
+ "version": "1.19.629",
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",
@@ -485,6 +485,32 @@ function renderReport(reg) {
485
485
  return L.join('\n');
486
486
  }
487
487
 
488
+ // Line-ending-blind view of a file's text, for the --check compare ONLY (task 1003619).
489
+ // Why: the generator emits LF, and --check string-compares that against the file on
490
+ // disk. A Windows checkout hands back CRLF, so the compare reported STALE forever on
491
+ // content that was byte-identical in the index — a permanently red /ship-check gate,
492
+ // which teaches builders to wave past the one check that catches a REAL stale artifact.
493
+ // .gitattributes now pins this pair to eol=lf, but that only governs FUTURE checkouts:
494
+ // a file already sitting CRLF in an existing worktree stays CRLF until renormalized,
495
+ // so the checker must not care either way. Freshness is a question about CONTENT.
496
+ // Not used on the write path — generated files are still written with LF endings.
497
+ function lf(s) {
498
+ return s.replace(/\r\n/g, '\n');
499
+ }
500
+
501
+ // Is the artifact on disk still what the generator would write? Freshness is a
502
+ // question about CONTENT, so CRLF-vs-LF is not a difference — see lf() above for
503
+ // why that mattered. A missing file (`have === null`) is stale by definition.
504
+ // Only \r\n is folded, NOT a lone \r: no tool in this repo emits classic-Mac
505
+ // endings, and treating a bare \r as a newline would let a genuinely corrupt file
506
+ // read as fresh. Pure + exported so the decision is unit-testable without writing
507
+ // to the committed artifacts (same shape as shouldRefuseSessionClaim in
508
+ // modules/lifecycle/routes/claims.js).
509
+ function matchesGenerated(have, want) {
510
+ if (have === null || have === undefined) return false;
511
+ return lf(have) === lf(want);
512
+ }
513
+
488
514
  function write({ check = false } = {}) {
489
515
  const reg = buildRegistry();
490
516
  const json = `${JSON.stringify(reg, null, 2)}\n`;
@@ -495,7 +521,7 @@ function write({ check = false } = {}) {
495
521
  const stale = [];
496
522
  for (const [p, want] of [[jsonPath, json], [mdPath, md]]) {
497
523
  const have = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null;
498
- if (have !== want) stale.push(path.relative(REPO_ROOT, p));
524
+ if (!matchesGenerated(have, want)) stale.push(path.relative(REPO_ROOT, p));
499
525
  }
500
526
  return { reg, stale };
501
527
  }
@@ -525,5 +551,5 @@ if (require.main === module) {
525
551
  module.exports = {
526
552
  SURFACES, COPY_ATTRS, BLIND_SPOTS, REGISTRY_REL, REPORT_REL,
527
553
  cmp, stableId, looksLikeCopy, lineIndex, lineAt, stripEmbedded, fromHtml, fromJs, filesFor,
528
- buildRegistry, renderReport, write,
554
+ buildRegistry, renderReport, write, lf, matchesGenerated,
529
555
  };
@@ -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.627'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.629'; // 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');
Binary file
@@ -176,9 +176,24 @@ function withBlockBody(src, body) {
176
176
  // .gitattributes, one in REGENERABLE) — nothing checked that they still matched, so
177
177
  // docs/copy-inventory.md + docs/copy-registry.json could be (and were) added to
178
178
  // neither and merge like ordinary text. This pins the two lists as one set.
179
+ // A .gitattributes line may carry SEVERAL attributes for one path — git reads
180
+ // `docs/copy-inventory.md merge=otb-regen text eol=lf` exactly as it reads the
181
+ // bare `merge=otb-regen` form. The original matcher here was end-anchored
182
+ // (/^(\S+)\s+merge=otb-regen\s*$/), so adding any second attribute made this
183
+ // check stop SEEING the entry and report it as missing (task 1003619, where the
184
+ // .md gained an eol pin). Under-inclusive in the dangerous direction too: a
185
+ // regenerable path declared with extra attributes would slip the REGENERABLE
186
+ // cross-check entirely. Tokenize the attribute list instead of pattern-matching
187
+ // the whole line, so the set equality below is about PATHS, not line formatting.
179
188
  {
180
189
  const attrsText = fs.readFileSync(path.join(repo, '.gitattributes'), 'utf8');
181
- const otbRegenPaths = [...attrsText.matchAll(/^(\S+)\s+merge=otb-regen\s*$/gm)].map((m) => m[1]);
190
+ const otbRegenPaths = attrsText
191
+ .split(/\r?\n/)
192
+ .map((line) => line.trim())
193
+ .filter((line) => line && !line.startsWith('#'))
194
+ .map((line) => line.split(/\s+/))
195
+ .filter((tokens) => tokens.slice(1).includes('merge=otb-regen'))
196
+ .map((tokens) => tokens[0]);
182
197
  assert.ok(otbRegenPaths.length > 0, 'expected at least one merge=otb-regen line in .gitattributes');
183
198
  for (const p of otbRegenPaths) {
184
199
  assert.ok(regen.REGENERABLE.has(p), `.gitattributes declares merge=otb-regen for ${p} but REGENERABLE (git-merge-regen.js) does not know it`);
@@ -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
+ });