@mindrian_os/install 1.13.0-beta.28 → 1.13.0-beta.32

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mos",
3
3
  "description": "MindrianOS -- Your AI innovation co-founder. Larry thinks with you through PWS methodology, builds your Data Room as you explore, and chains frameworks intelligently. Install and go.",
4
- "version": "1.13.0-beta.28",
4
+ "version": "1.13.0-beta.32",
5
5
  "author": {
6
6
  "name": "Jonathan Sagir",
7
7
  "url": "https://mindrian.ai"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,54 @@
1
+ ## [1.13.0-beta.32] - 2026-05-24
2
+
3
+ ### Fixed (Windows-tester regression bundle, Phase 127.2 Plan 04 -- ships v1.13.0-beta.32)
4
+ - **Instance #4 (P2): `/mos:rooms list` works on Windows + Git Bash (`scripts/room-registry` POSIX path leak).** Every `python3 -c` invocation in `scripts/room-registry` (8 subcommand stanzas: `create`, `read`, `list`, `update`, `set-active`, `archive`, `get-active`, `git-config`) interpolated bash-resolved `$REGISTRY_FILE` into Python source. On Git Bash for Windows, `$HOME=/c/Users/PC` produced paths like `/c/Users/PC/MindrianRooms/.rooms/registry.json` that Windows Python `open()` could not resolve, surfacing `FileNotFoundError` on every registry subcommand. Fix: inlined a `normwin()` Python shim at the top of every block, platform-gated on `sys.platform == 'win32'` so it is a no-op on Linux/macOS but converts the POSIX form to native `C:\Users\...` on Windows; every `open(...)` rewritten to `open(normwin(REGISTRY_FILE))`. Sibling sweep patched `scripts/reapply-modifications` (4 `python3 -c` sites, single `$NORMWIN_SHIM` bash-var-string reused). Explicit sweep targets (`scripts/hsi-*`, `scripts/build-*`, `scripts/release.sh`) verified clean. Out-of-scope sibling sites in `scripts/verify-release`, `scripts/learn-from-usage`, `scripts/track-analytics` logged to `.planning/phases/127.2-.../deferred-items.md` for a future patch beta. Silent for non-Windows users; caught only on Jonathan's dogfood Windows box. Closes `.planning/debug/resolved/windows-room-registry-path-normalization-gap.md`.
5
+ - **Instance #7 (P1, META-FIX): `/mos:update` atomically swaps to the new active install + warns about session restart (silent-activation gap closed).** `/mos:update` and `claude plugin update mos@mindrian-marketplace` land new bytes in `~/.claude/plugins/cache/mindrian-marketplace/mos/<NEW_VERSION>/` but DO NOT atomically swap the live install at `~/.claude/plugins/mindrian-os/`. Every subsequent MCP probe + statusline + hook output continued to serve the OLD bytes. Users thought they were on beta.N+1; every Brain interaction silently read beta.N. This is the structural reason every prior beta this session may have been unverified on tester wires. Three-part fix: (a) new `scripts/post-update-activation.cjs` (305 lines, exports `activatePostUpdate` + `POST_UPDATE_TOUCH_FILE`) detects the cache-staging dir + delegates the atomic swap to existing `scripts/doctor.cjs --fix` (Canon Part 7 reuse of Phase 95.2 install-cache-atomic-recovery, three autopsies of hardening) + writes a touch-file at `~/.mindrian/post-update-restart-pending` with the new version + emits a Shape E action report; (b) new SessionStart hook `scripts/sessionstart-post-update-preflight.cjs` (187 lines, sibling-NOT-replacement of `sessionstart-npm-reconcile.cjs`) reads the touch-file each session, spawns `doctor --brain-smoke --json`, parses L4 MCP stdio handshake server-version token, deletes touch-file silently on match, refuses Larry-load with a red banner via SessionStart `systemMessage` envelope on drift (exit 1), exits 0 silently when probe is inconclusive (defensive); (c) `commands/update.md` Step 7 calls `node "${CLAUDE_PLUGIN_ROOT}/scripts/doctor.cjs" --fix --post-update` at the end of every update flow; `scripts/doctor.cjs` gains `--post-update` flag handler that delegates to the activator, composable with `--fix`. Closes `.planning/debug/resolved/mos-update-silent-activation-gap.md`.
6
+
7
+ ### Changed (Release pipeline self-defense, Phase 127.2 Plan 04)
8
+ - **`/mos:doctor --acceptance` Class N gate: `activation-reached-the-wire`.** `scripts/doctor.cjs` gains a new acceptance point (severity `blocker`, `applies_to: ['full']`) that asserts the L4 MCP server version (probed via `doctor --brain-smoke --json` -> parse `server=mindrian-brain vX.Y.Z` from L4 handshake reason) matches `package.json.version` (version-of-record). On match: pass. On drift: fail with `activation gap detected: live install serves v<OLD> but version-of-record is v<NEW>`. Wired into the existing acceptance roster so `scripts/release.sh` Step 9.8 (post-publish full acceptance gate) runs this check AFTER the new version is published -- catching any phantom-version release before it propagates to tester caches. This is the canary the release pipeline never had: every beta after v1.13.0-beta.32 either activates on the wire AND earns the acceptance pass, or fails the gate AND is prevented from shipping. The cadence-vs-validation trade-off becomes structurally enforced rather than relying on manual maintainer discipline. Test environment hooks: `DOCTOR_TEST_FAIL_POINT=activation-reached-the-wire` synthesizes a failure; `DOCTOR_SKIP_ACTIVATION_GATE=1` marks the point ok-as-skipped for hermetic CI / offline mode.
9
+
10
+ ### Internal (Phase 127.2 Plan 04)
11
+ - **`hooks/hooks.json`** registers `sessionstart-post-update-preflight.cjs` as a SessionStart hook (matcher `startup|clear|compact`, `async: false`, ordered AFTER `sessionstart-npm-reconcile.cjs` so node_modules is in place before brain-smoke spawns, BEFORE Larry-load so a drift blocks the room).
12
+ - **New tests (60 PASS / 0 FAIL across 3 suites):** `tests/test-room-registry-windows-path.cjs` (215 lines, 25 PASS -- linux regression + python normwin functional probe + structural greps confirming zero raw `open($REGISTRY_FILE)` callsites remain); `tests/test-mos-update-activation-gap.cjs` (233 lines, 19 PASS -- cold synthesis of beta.N -> beta.N+1 atomic swap + idempotency on already-on-latest + preflight hook semantics); `tests/test-127.2-04-windows-path-and-update-activation.sh` (88 lines, 16 PASS -- structural greps + functional doctor probes).
13
+ - **3 RCA closeouts moved to `.planning/debug/resolved/`:** `post-beta30-regression-2026-05-23.md` (12-gate wire-verification sweep), `windows-room-registry-path-normalization-gap.md` (Instance #4 detail), `mos-update-silent-activation-gap.md` (Instance #7 detail). Each updated with `status: resolved`, `resolved: 2026-05-23`, `resolved_by: phase-127.2 Plan 127.2-04`, plus Resolution sections linking to this plan.
14
+ - **`.planning/debug/knowledge-base.md`** gains 2 new entries (Instance #4 + Instance #7 -- distinct patterns) so `gsd-debugger` surfaces these as known-pattern hypotheses on future investigations.
15
+ - **`docs/install-cache-family-premortem.md`** appended with case #7 (the 7th case in the install-cache failure family -- two sub-cases shipped as one beta: bash-heredoc POSIX leak + silent activation gap). New sub-pattern documented (cross-platform fragility class beyond install-cache proper). Two new predicted failure modes added: **F. Cowork cross-tenant activation drift** (Class N assumes one active install per host); **G. Bash-heredoc POSIX leaks in non-`scripts/room-registry` sites** (deferred sweep targets logged).
16
+
17
+ ### User-facing note (Phase 127.2 Plan 04)
18
+ - **If you previously ran `/mos:update` and `/mos:doctor --brain-smoke` still reports an older version, run `/mos:doctor --fix` once.** v1.13.0-beta.32 makes this automatic going forward. You will see a one-time "ACTIVATION GAP" red banner on next session-start if the touch-file from a prior update still flags a drift; the banner walks you through the two-step recovery.
19
+
20
+ ### Added (Phase 121.5 Sub-plan K -- Plan 121.5-10)
21
+ - **Locked Brain-suggestion content template across all 5 consumers.** `/mos:suggest-next`, `/mos:act --chain`, the Phase 116 tension-hook-agent, the Phase 117 auto-explore-agent, and the Phase 89-07 reverse-salient-agent all render the same shape now: canonical `[■ BRAIN]` chip + verb-first question line + two-line dense option rows (glyph + verb + right-aligned confidence percent on top, framework category + graph relationship below) + stat-strip footer. The lock collapses 5 divergent renderings into ONE so the navigator's eye learns "Brain is speaking" in one session. Source: `.planning/121.5-selector-coverage-audit.md` Section 5.
22
+ - **Promoted `/mos:suggest-next` from NONE (silent dispatch / plain-text list) to F.1** via `rankForSelector` + `pickShape` per audit Section 6 item 2. THE single highest-leverage promotion per the audit Executive Summary: every conversational "what next?" Larry reflex inherits the locked surface.
23
+ - **Promoted `/mos:act --chain` `[GATE]` rendering from BESPOKE bracket text (`[continue]` / `[stop]`) to F.0 Mini Decision Gate** via `pickShape({ requestedShape: 'F.0' })` per audit Section 6 item 3. The closed F.0 vocabulary (Approve / Reject / Defer) replaces the bespoke two-button mental model without losing intent (Reject captures REJECTED_BECAUSE; Defer queues a milestone audit).
24
+ - **Selector verb-label aliases (LOCKED decision 1).** Dispatcher carries `alias_map` (Resolve / Explore -> Run Methodology; Later -> Defer; Skip -> Free-Text) loaded once at module init from `lib/hmi/jtbd-taxonomy.json` `alias_map.verb_aliases`. Aliases render to the user; canonical verbs persist to graph edges via `navigation.cjs`. The render-vs-persist split honors both pedagogy (contextual aliases) and graph consistency (one stable vocabulary) without forcing one.
25
+ - **CI tripwire `tests/test-no-bespoke-brain-prompts.sh`** enforces the lock (audit Section 6 item 6). Scans the 5 named Brain-suggestion consumer files plus any new source file that imports `chain-recommender.cjs` or `f-selector-ranker.cjs`; flags bracket-text option lists (`[continue]`/`[stop]`), verbose `(RECOMMENDED)` tags, and "Pick one to ..." selector-prompt prose. Wired into `tests/run-all-121.5.sh` SHELL_SUITES.
26
+ - **`docs/F-SELECTOR-CONSUMER-GUIDE.md` Section 4 (NEW)** publishes the locked template (slot-value table + visual mockup + anti-patterns rejected + implementation wiring example) per audit Section 6 item 7. Makes the lock discoverable to future consumers before they invent a new pattern.
27
+ - **`skills/ui-system/SKILL.md` Section 2** adds the Shape F.1 Brain-suggestion variant subsection (citing the locked chip + glyphs + footer + alias_map) AND a body_shape vs F-shape orthogonality note per LOCKED decision 4 (`body_shape:` is layout discipline; F-shape is the selector contract; they are orthogonal axes, not competing values).
28
+ - **`lib/workflow/f-selector-ranker.cjs` MAX_K = 3 constant + clamp** per audit Section 5.2.3 anti-pattern ("More than 3 option rows pushes the AskUserQuestion auto-injected rows off-screen"). Caller asking k=20 silently receives k=3. New optional `category` + `graph_relationship` fields on returned items[] feed the locked template's two-line meta row.
29
+ - **New tests:** `lib/memory/selector-alias-map.test.cjs` (24 assertions, alias_map round-trip + render-vs-persist invariant) + `lib/memory/brain-suggestion-template.test.cjs` (22 assertions, 5-consumer chip + footer isomorphism).
30
+
31
+ ### Deferred to v1.13.2 (LOCKED decision 3)
32
+ - **CONTRADICTS-driven color flip (cyan -> yellow)** for Brain-suggestion selectors when any candidate carries a CONTRADICTS edge against an existing assumption. Yellow-on-cascade requires the consumer to walk the cascade graph BEFORE rendering (a SQL call on the rank path); we shipped cyan default in v1.13.0 to avoid that latency and queued the yellow-on-cascade signal as a v1.13.2 hotfix if testers report missed warnings. Ship simple, observe, then layer in if the data says it matters.
33
+
34
+ ### Deferred to v1.14.0 (LOCKED decision 5)
35
+ - **F.1 close-out adoption on the 71 N/A methodology commands** (think-hats, structure-argument, grade, deep-grade, validate, etc.). Out of scope for Phase 121.5; the Phase 88.2 design legitimately delegates the methodology close to Larry's conversational follow-up. Phase 121.5 + 1 backlog. Scope discipline.
36
+
37
+ ## [1.13.0-beta.30] - 2026-05-23
38
+
39
+ ### Fixed (Engine 1 Act 1 silent-failure class, Phase 127.2 Plan 03 -- FIRST hotfix from external tester evidence)
40
+ - **`scripts/doctor.cjs --check-rs-engine` pre-flight pre-flights Python deps for the Engine 1 Act 1 surface (Finding F1 -- `.planning/debug/resolved/windows-tester-find-bottlenecks-silent-failure-qa-sweep.md`).** ADD-ONLY flag handler (NOT a class flag; owns its own exit-code contract). Probes critical deps reachable from `scripts/rs-engine.py` -- `requests` (transitive via `lib/core/rs_corpus.py`, the actual silent-failure root cause on the Windows tester's machine) + `numpy` -- plus umbrella deps from `requirements-hsi.txt` (`sentence_transformers`, `sklearn`). Resolves python interpreter via `MINDRIAN_PYTHON` env override > `python3` > `python` fallback. On missing critical deps: exit 1 + actionable fix line `Run: pip install -r requirements-hsi.txt --user (use python -m pip if pip is not in PATH)`. JSON variant returns `{ ok, ready, python, probes[], missing, missing_critical, missing_umbrella, fix }`. Defensive: any uncaught probe error surfaces as exit 1 -- the probe never crashes `/mos:doctor`'s other gates. Closes the Windows tester 2026-05-23 silent-failure class for the pre-flight surface.
41
+ - **`lib/agents/reverse-salient-agent.cjs` forwards rs-engine stderr to `result.detail.diagnostic` (Finding F2 -- same RCA).** The `runRsEngine()` catch block now captures `e.stderr` from the failed child python process, takes the LAST 200 chars (preserving the actionable fix line + exception name at the tail), and embeds it in `result.detail.diagnostic`. Backward compatible: when stderr is empty, `detail` stays a plain string (the existing `e.message` slice); when stderr is present, `detail` upgrades to `{ message, diagnostic }`. The existing `ok` / `reason` fields are untouched. Before this fix, every actionable error from rs-engine was silently discarded at the agent layer -- the worst-shape silent failure in a methodology surface.
42
+ - **`commands/find-bottlenecks.md` empty-result UX disambiguates analyzer-down from no-findings (Finding F7 -- same RCA).** New "Empty-result UX" sub-section distinguishes two categorically different cases: (a) analyzer ran with no findings (plausible if room is small or genuinely balanced); (b) analyzer could not start -- detected via `result.detail.diagnostic` or `reason: rs_engine_invocation_failed`. The second path explicitly tells the user to run `/mos:doctor --check-rs-engine` with the pip-install one-liner inline. Before this fix, both cases rendered as "no findings" -- which reads as "your work is clean" regardless of whether the analyzer ran or crashed. The single most dangerous reading in a methodology surface.
43
+
44
+ ### Internal (Phase 127.2 Plan 03)
45
+ - **New test:** `tests/test-127.2-03-rs-engine-silent-failure-fixes.sh` verifies all three findings landed (F1 flag-handler + actionable fix line embedded; F2 diagnostic-field reference + stderr-capture pattern; F7 disambiguation copy present in find-bottlenecks.md or agent file) plus a functional probe asserting `node scripts/doctor.cjs --check-rs-engine --json` returns valid JSON and `--help` documents the new flag. 7/7 PASS on origin/main.
46
+ - **Phase 134 scaffolded as v1.14.0 architectural stub (Finding F6 -- structural answer to install-fragility class).** New `.planning/phases/134-cjs-port-of-python-analyzers-via-xenova-transformers-elimina/134-CONTEXT.md` captures the design vision: replace `scripts/rs-engine.py` + `lib/core/rs_*.py` + `scripts/hsi-*.py` with CJS equivalents using `@xenova/transformers` (ONNX `Xenova/multilingual-e5-large` in-process). Eliminates Python from user-machine surface entirely. Estimate ~3 weeks. Plan 127.2-03's original spec named "Phase 130" but slot 130 was already taken; SDK assigned 134 as next free slot. No PLAN.md (scaffolding only); enters v1.14.0 planning cycle.
47
+ - **RCA closed and moved.** `.planning/debug/windows-tester-find-bottlenecks-silent-failure-qa-sweep.md` -> `.planning/debug/resolved/` with `resolved_by: phase-127.2 Plan 127.2-03 (hotfix; F1+F2+F7 shipped)` + `resolved_disposition: 3-of-4-fixed-in-code; F3 narrative drift deferred to next docs reconciliation; F6 architectural port scaffolded as Phase 134 stub`. Knowledge-base entry appended at `.planning/debug/knowledge-base.md` so `gsd-debugger` surfaces this as a known-pattern hypothesis on future investigations.
48
+ - **Dog-fooding milestone:** the FIRST hotfix shaped from an EXTERNAL tester's transcript (Aryeh's Windows machine, 2026-05-23). Plans 127.2-00 + 127.2-02 were both maintainer-discovered. This one closes a defect a real user hit on a machine the maintainer doesn't own, and ships in the same week the transcript landed -- empirically demonstrating that the dog-fooding loop the QA sweep itself flagged as broken (RS-2 thesis: one-person QA is the lagging subsystem) is no longer one-person.
49
+
50
+
51
+
1
52
  ## [1.13.0-beta.28] - 2026-05-23
2
53
 
3
54
  ### Fixed (post-ship QA-sweep closeout, Phase 127.2 Plan 02)
@@ -37,6 +37,15 @@ Procedure (CLI / Desktop / Cowork):
37
37
  - On DEFER: DEFERRED memory_event records the deferral for Phase 116 unresolved-tension-hook consumption.
38
38
  4. If the agent returns `{ ok: false }` OR finds nothing OR is suppressed (tier 0 / JUST_TALK), fall back to the standard Setup + Session Flow below.
39
39
 
40
+ ### Empty-result UX (Phase 127.2 Plan 03 -- Finding F7)
41
+
42
+ When the agent returns no findings, you MUST distinguish two cases for the user, because "no findings" reads as "your work is clean" -- the worst possible signal if the analyzer crashed:
43
+
44
+ - **Analyzer ran successfully and found nothing.** Surface: "No reverse-salient findings were returned -- the analyzer ran across your room and could not identify a lagging component above the threshold. This is plausible if the room is small (less than 5 substantive artifacts) or if the system is genuinely balanced; continue with the Session Flow below to do the framework manually if you want a second pass."
45
+ - **Analyzer could not start (rs-engine failed -- look for `result.detail.diagnostic` in the agent payload, OR `ok: false, reason: rs_engine_invocation_failed`).** Surface: "No reverse-salient findings were returned. If you expected results, the analyzer may not have started -- run `/mos:doctor --check-rs-engine` to verify your Engine 1 Act 1 environment is healthy. Common cause: missing Python deps (`pip install -r requirements-hsi.txt --user`)."
46
+
47
+ The disambiguation is critical because the agent layer historically swallowed the actionable error message (Windows tester 2026-05-23 silent-failure class). Always surface the `--check-rs-engine` hint on the analyzer-failure path.
48
+
40
49
  Anti-pattern reminder (per docs/AGENTIC-SURFACING-PATTERN.md):
41
50
  - Never print findings to console; the F.0 dispatcher IS the surfacing surface.
42
51
  - Never query the Brain directly; the agent reads pre-derived BRAIN.md via folder-memory.readQuadruple (LOCAL only, Canon Part 8).
@@ -20,6 +20,18 @@ allowed-tools:
20
20
 
21
21
  # /mos:suggest-next
22
22
 
23
+ <!--
24
+ Phase 121.5-10 Sub-plan K LOCKED decision 4 (body_shape vs F-shape orthogonality):
25
+ body_shape: B (Semantic Tree) is the LAYOUT discipline of this command's body --
26
+ the ranked-list render. F.1 is the SELECTOR CONTRACT that fires at the close of
27
+ the body (the verb-pick gate beneath the tree). The two are orthogonal axes:
28
+ body_shape describes the visual layout; F-shape describes the dispatcher
29
+ contract. This command is "F.1 over Shape B." See skills/ui-system/SKILL.md
30
+ Section 2 orthogonality note for the canon citation. Also see audit
31
+ .planning/121.5-selector-coverage-audit.md Section 5 for the locked Brain-
32
+ suggestion content template that the F.1 surface emits.
33
+ -->
34
+
23
35
  You are Larry. This command recommends what the user should work on next as a COMMAND SEQUENCE, not just a list of frameworks: it reads the room's ProblemType (and active JTBD), Brain-derives the framework chain, and composes that chain into the exact `/mos:` commands to run, in order.
24
36
 
25
37
  ## The resolver is the only door
@@ -139,10 +139,42 @@ If the migrator finds and removes stale entries, surface that to the user:
139
139
  If no findings, mention it briefly:
140
140
  > "User settings clean -- no stale paths."
141
141
 
142
- ### Step 7: Verify and instruct restart
142
+ ### Step 7: Atomically activate the new bytes (Phase 127.2 Plan 04 Instance #7)
143
+
144
+ Claude Code's native `plugin update` lands the new version in the cache
145
+ (`~/.claude/plugins/cache/mindrian-marketplace/mos/<NEW_VERSION>/`) but does
146
+ NOT atomically swap the live install at `~/.claude/plugins/mindrian-os/`.
147
+ Without this step every Brain MCP probe + statusline render + hook output
148
+ continues to serve the OLD version. The user thinks they are on beta.N+1
149
+ while every Brain interaction silently reads beta.N -- the "silent
150
+ activation gap" surfaced on the 2026-05-23 dogfood box.
151
+
152
+ Run the post-update activator to swap atomically:
153
+
154
+ ```bash
155
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/doctor.cjs" --fix --post-update
156
+ ```
157
+
158
+ Stream the output to the user. It prints a Shape E action report showing
159
+ the old -> new transition + the backup path of the stale bytes. After
160
+ success it writes a touch-file at `~/.mindrian/post-update-restart-pending`
161
+ that the next SessionStart's preflight hook reads to verify activation
162
+ reached the wire (the L4 MCP server's version matches package.json
163
+ version-of-record). On version-mismatch the preflight hook refuses Larry
164
+ load with a red banner pointing at `/mos:doctor --fix`.
165
+
166
+ If the activator reports `swapped: false` and `already on latest`: no
167
+ action needed. The cache and live install were already in sync (rare but
168
+ possible if a previous /mos:update sequence completed cleanly).
169
+
170
+ If it reports `ok: false`: surface the error to the user. They can manually
171
+ run `/mos:doctor --fix` to re-attempt; if that also fails, file an RCA at
172
+ `.planning/debug/post-update-activation-failure-<date>.md`.
173
+
174
+ ### Step 8: Verify and instruct restart
143
175
 
144
176
  If all steps succeeded:
145
- > "Done. v{latest} installed via Claude Code's plugin loader -- registry, cache, and `enabledPlugins` are all in sync. User settings checked for stale paths. Restart Claude Code (close and reopen the terminal, or kill and re-run `claude`) to pick it up. After restart, run `/mos:help` to confirm commands are reachable, and look for the Mindrian statusline at the bottom of the terminal."
177
+ > "Done. v{latest} installed via Claude Code's plugin loader and atomically swapped into the live install path. Registry, cache, and `enabledPlugins` are all in sync. User settings checked for stale paths. Restart Claude Code (close and reopen the terminal, or kill and re-run `claude`) so MCP servers reconnect against the new bytes. After restart, run `/mos:help` to confirm commands are reachable and look for the Mindrian statusline at the bottom of the terminal."
146
178
 
147
179
  ## Force Mode
148
180
 
package/hooks/hooks.json CHANGED
@@ -13,6 +13,18 @@
13
13
  }
14
14
  ]
15
15
  },
16
+ {
17
+ "matcher": "startup|clear|compact",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/sessionstart-post-update-preflight.cjs\"",
22
+ "timeout": 12000,
23
+ "async": false,
24
+ "statusMessage": "Verifying post-update activation..."
25
+ }
26
+ ]
27
+ },
16
28
  {
17
29
  "matcher": "startup|clear|compact",
18
30
  "hooks": [
@@ -665,6 +665,39 @@ function surfaceFinding(args) {
665
665
  return { surfaced: false, suppress_reason: 'pickShape_unavailable', finding: populated };
666
666
  }
667
667
 
668
+ // Phase 121.5-10 Sub-plan K (audit Section 5.3): the locked [■ BRAIN]
669
+ // chip replaces the BQ-anchored header. The verbatim Brain BQ name
670
+ // (Phase 117 RESEARCH 8.5 moat) moves into the question-line slot
671
+ // beneath the chip so the two-row format preserves the BQ anchor
672
+ // signal without violating the 12-char chip rule. Verb-label aliases
673
+ // (Explore / Skip / Later) KEPT per LOCKED decision 1; alias_map
674
+ // collapses on selection (Explore -> Run Methodology, Skip -> Free-
675
+ // Text, Later -> Defer). The >= 0.7 recommendedVerb gate (Phase 88.2)
676
+ // determines the leading row's glyph in optionRows[0].
677
+ let aliasMap = {};
678
+ try {
679
+ const tax = require('../hmi/jtbd-taxonomy.json');
680
+ if (tax && tax.alias_map && tax.alias_map.verb_aliases) {
681
+ aliasMap = tax.alias_map.verb_aliases;
682
+ }
683
+ } catch (_e) { /* graceful */ }
684
+ const topScore = Number(populated.top_differential_score) || 0;
685
+ const verbsLocal = ['Explore', 'Skip', 'Later'];
686
+ const optionRows = verbsLocal.map(function (v, i) {
687
+ let meta = '';
688
+ if (v === 'Explore') meta = 'BQ-anchored explore · cascade ENABLES + INFORMS edges';
689
+ else if (v === 'Skip') meta = 'silent dismiss · no edge written';
690
+ else if (v === 'Later') meta = 'queue for next session · surfacing_count preserved';
691
+ const glyph = (v === 'Explore' && topScore >= 0.7) ? '▶' : '▷';
692
+ return {
693
+ glyph: glyph,
694
+ number: i + 1,
695
+ verb: v,
696
+ confPct: (v === 'Explore') ? Math.round(topScore * 100) : 0,
697
+ meta: meta,
698
+ };
699
+ });
700
+
668
701
  let result;
669
702
  try {
670
703
  result = dispatcher.pickShape({
@@ -673,8 +706,13 @@ function surfaceFinding(args) {
673
706
  operator: operator,
674
707
  tier: tier,
675
708
  payload: {
676
- verbs: ['Explore', 'Skip', 'Later'],
677
- header: bqLine,
709
+ brain_suggestion_variant: true,
710
+ verbs: verbsLocal,
711
+ header: '[■ BRAIN]',
712
+ questionLine: bqLine,
713
+ alias_map: aliasMap,
714
+ optionRows: optionRows,
715
+ footer: '▶ Brain · top-3 of 3 ranked · cyan = informing',
678
716
  recommendedVerb: recommendedVerb,
679
717
  emitTelemetry: true,
680
718
  parent_decision_id: parent_decision_id,
@@ -189,10 +189,26 @@ function runRsEngine(opts) {
189
189
  timeout: 60000,
190
190
  });
191
191
  } catch (e) {
192
+ // Phase 127.2-03 Task 1 (Finding F2): forward the child python process's
193
+ // stderr LAST 200 chars to result.detail.diagnostic so callers can
194
+ // self-recover from missing-deps / import errors (the Windows tester
195
+ // 2026-05-23 silent-failure class). Truncate at the START so the tail
196
+ // (which carries the exception name + actionable fix line printed by
197
+ // rs_corpus -- e.g. "Run: pip install -r requirements-hsi.txt") is the
198
+ // half that survives the cap. Backward compatible: ok / reason / detail
199
+ // (e.message) unchanged; detail upgraded from plain string to object
200
+ // ONLY when stderr is present, so legacy callers reading detail as a
201
+ // string still get the truncated message via detail.message.
202
+ const message = String((e && e.message) || '').slice(0, 120);
203
+ const stderrRaw = (e && e.stderr) ? String(e.stderr) : '';
204
+ const diagnostic = stderrRaw.length > 200 ? stderrRaw.slice(-200) : stderrRaw;
205
+ const detail = stderrRaw.length > 0
206
+ ? { message: message, diagnostic: diagnostic }
207
+ : message;
192
208
  return {
193
209
  ok: false,
194
210
  reason: 'rs_engine_invocation_failed',
195
- detail: String((e && e.message) || '').slice(0, 120),
211
+ detail: detail,
196
212
  pairs: [],
197
213
  };
198
214
  }
@@ -501,12 +517,22 @@ function surfaceFinding(args) {
501
517
  return { surfaced: false, suppress_reason: 'just_talk' };
502
518
  }
503
519
 
504
- // Compose F.0 header with persona suffix per RESEARCH SCOPE B Section 6.
505
- // Body composition: finding.body_text + optional brain framework chain.
506
- const header = '-- mindrianOS -- reverse salient -- ' + personaSuffixText + ' --';
520
+ // Phase 121.5-10 Sub-plan K (audit Section 5.3): the locked [■ BRAIN]
521
+ // chip replaces the prior `-- mindrianOS -- reverse salient -- <persona>
522
+ // --` header. The persona suffix (Phase 89-07 extension via
523
+ // resolvePersonaSuffix) moves into the body slot directly beneath the
524
+ // chip so the two-row chip+context format preserves the persona signal
525
+ // without violating the 12-char chip rule. F.0 closed vocabulary
526
+ // (Approve / Reject / Defer) STAYS verbatim per the F.0 specification
527
+ // (no RECOMMENDED in F.0; the shape itself is the recommendation
528
+ // surface). The body composition (persona + body_text + framework chain)
529
+ // continues to render in zones.body via the F.0 renderer.
530
+ const header = '[■ BRAIN]';
507
531
  const bodyText = String(finding.body_text || '');
508
532
  const chainText = String(finding.brain_chain_text || '');
509
- const body = bodyText + (chainText.length > 0 ? '\n\nFramework chain: ' + chainText : '');
533
+ const personaLine = personaSuffixText && personaSuffixText.length > 0
534
+ ? '(' + personaSuffixText + ' lens)\n\n' : '';
535
+ const body = personaLine + bodyText + (chainText.length > 0 ? '\n\nFramework chain: ' + chainText : '');
510
536
 
511
537
  // Dispatch via the canonical 88.2-04+05 pickShape entry point.
512
538
  // emitTelemetry:false because the agent owns the dual-surface mirror via
@@ -48,8 +48,20 @@ const pendingStore = require('../memory/pending-tension-store.cjs');
48
48
 
49
49
  // ---------- Constants ----------
50
50
 
51
+ // Phase 121.5-10 Sub-plan K (audit Section 5.3 minor alignment + LOCKED
52
+ // decision 1): aliases KEPT for pedagogical clarity in the tension-hook
53
+ // surface; alias_map (loaded from lib/hmi/jtbd-taxonomy.json by the
54
+ // dispatcher) collapses to canonical at selection time for graph-edge
55
+ // persistence (Resolve -> Run Methodology / Later -> Defer / Skip ->
56
+ // Free-Text). The user sees the contextual alias; the graph stores the
57
+ // canonical verb.
51
58
  const F1_VERBS = ['Resolve', 'Later', 'Skip'];
52
- const F1_HEADER = '-- mindrianOS -- pending tension -- pick a verb --';
59
+ // Phase 121.5-10 Sub-plan K (audit Section 5.3): the locked [■ BRAIN] chip
60
+ // replaces the prior verbose header. The tension context now lands in the
61
+ // question-line slot directly beneath the chip ("Resolve pending tension:"
62
+ // + brief summary) per the two-row format that preserves context without
63
+ // violating the 12-char chip rule.
64
+ const F1_HEADER = '[■ BRAIN]';
53
65
  const VALID_RESPONSES = Object.freeze(new Set(['RESOLVE', 'LATER', 'SKIP', 'FREE_TEXT']));
54
66
  const TENSION_ID_LEN = 32;
55
67
 
@@ -164,6 +176,31 @@ function surfaceFinding(args) {
164
176
  }
165
177
 
166
178
  const surfaceStartedAtMs = Date.now();
179
+ // Phase 121.5-10 Sub-plan K (audit Section 5.3): load alias_map from
180
+ // jtbd-taxonomy.json so the dispatcher can collapse Resolve / Later /
181
+ // Skip to canonical verbs (Run Methodology / Defer / Free-Text) at
182
+ // selection time. Lazy-required so test substitution can fake the
183
+ // dispatcher without rebuilding the taxonomy fixture.
184
+ let aliasMap = {};
185
+ try {
186
+ const tax = require('../hmi/jtbd-taxonomy.json');
187
+ if (tax && tax.alias_map && tax.alias_map.verb_aliases) {
188
+ aliasMap = tax.alias_map.verb_aliases;
189
+ }
190
+ } catch (_e) { /* graceful */ }
191
+ // Build the two-line option rows for the locked Brain-suggestion template.
192
+ // Per Phase 116 explicit design note (audit Section 5.1): recommendedVerb
193
+ // stays null (the tension hook deliberately omits the RECOMMENDED gate);
194
+ // all three verbs render with the empty-triangle alternative glyph.
195
+ const tensionType = (finding && typeof finding.tension_type === 'string')
196
+ ? finding.tension_type : 'pending';
197
+ const optionRows = F1_VERBS.map(function (v, i) {
198
+ let meta = '';
199
+ if (v === 'Resolve') meta = tensionType + ' tension · captures RESOLVES_VIA edge';
200
+ else if (v === 'Later') meta = 'queue for next session · surfacing_count preserved';
201
+ else if (v === 'Skip') meta = 'silent dismiss · re-evaluated next SessionStart';
202
+ return { glyph: '▷', number: i + 1, verb: v, confPct: 0, meta: meta };
203
+ });
167
204
  let result;
168
205
  try {
169
206
  result = dispatcher.pickShape({
@@ -172,8 +209,13 @@ function surfaceFinding(args) {
172
209
  operator: operator,
173
210
  tier: tier,
174
211
  payload: {
212
+ brain_suggestion_variant: true,
175
213
  verbs: F1_VERBS.slice(),
176
214
  header: F1_HEADER,
215
+ questionLine: 'Resolve pending tension:',
216
+ alias_map: aliasMap,
217
+ optionRows: optionRows,
218
+ footer: '▶ Brain · top-3 of 3 ranked · cyan = informing',
177
219
  recommendedVerb: null, // D-02 neutral; no recommendation
178
220
  emitTelemetry: true, // 116-04 selector_presentation event fires via this flag
179
221
  },
@@ -389,5 +389,14 @@
389
389
  "persona_affinity": [],
390
390
  "completion_pattern": null
391
391
  }
392
- ]
392
+ ],
393
+ "alias_map": {
394
+ "verb_aliases": {
395
+ "Resolve": "Run Methodology",
396
+ "Explore": "Run Methodology",
397
+ "Later": "Defer",
398
+ "Skip": "Free-Text"
399
+ },
400
+ "provenance": "Phase 121.5-10 Sub-plan K (audit Section 5.3 verb-label alias decision; LOCKED decision 1). Aliases render to the user; canonical verbs persist to graph edges via navigation.cjs."
401
+ }
393
402
  }
@@ -81,6 +81,82 @@ function _sha256(s) {
81
81
  const FREE_TEXT = 'Free-Text';
82
82
  const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.';
83
83
 
84
+ // Phase 121.5-10 Sub-plan K (LOCKED decisions 1 + Bundle A):
85
+ // Brain-suggestion content template constants. The locked chip / glyphs /
86
+ // footer values are non-negotiable per audit Section 5.2 -- consumers passing
87
+ // brain_suggestion_variant:true receive these literal values composed into
88
+ // rendered.zones.body and rendered.zones.footer. Alias map loaded once at
89
+ // module init from lib/hmi/jtbd-taxonomy.json alias_map.verb_aliases.
90
+ const BRAIN_CHIP = '[■ BRAIN]';
91
+ const BRAIN_GLYPH_RECOMMENDED = '▶';
92
+ const BRAIN_GLYPH_ALTERNATIVE = '▷';
93
+
94
+ // Load alias_map once. Safe-require: if jtbd-taxonomy.json is missing or
95
+ // malformed (degraded install), fall back to empty map -- consumers without
96
+ // alias_map get pass-through behavior (verb renders verbatim, no collapse).
97
+ let _aliasMapCache = null;
98
+ function _loadAliasMap() {
99
+ if (_aliasMapCache !== null) return _aliasMapCache;
100
+ try {
101
+ const tax = require('./jtbd-taxonomy.json');
102
+ if (tax && tax.alias_map && tax.alias_map.verb_aliases
103
+ && typeof tax.alias_map.verb_aliases === 'object') {
104
+ _aliasMapCache = tax.alias_map.verb_aliases;
105
+ return _aliasMapCache;
106
+ }
107
+ } catch (_e) { /* graceful */ }
108
+ _aliasMapCache = {};
109
+ return _aliasMapCache;
110
+ }
111
+
112
+ /**
113
+ * Phase 121.5-10 Sub-plan K (LOCKED decision 1): collapse a user-facing verb
114
+ * alias to its canonical form for graph-edge persistence. Aliases live in
115
+ * lib/hmi/jtbd-taxonomy.json alias_map.verb_aliases; unknown verbs pass
116
+ * through unchanged (no-op for already-canonical verbs).
117
+ *
118
+ * Optional explicit aliasMap arg overrides the module-level cache (test seam
119
+ * + per-call override for surfaces with surface-specific aliases).
120
+ */
121
+ function aliasToCanonical(verb, aliasMap) {
122
+ if (typeof verb !== 'string' || verb.length === 0) return verb;
123
+ const map = (aliasMap && typeof aliasMap === 'object') ? aliasMap : _loadAliasMap();
124
+ if (Object.prototype.hasOwnProperty.call(map, verb)) {
125
+ const canonical = map[verb];
126
+ if (typeof canonical === 'string' && canonical.length > 0) return canonical;
127
+ }
128
+ return verb;
129
+ }
130
+
131
+ /**
132
+ * Phase 121.5-10 Sub-plan K: compose the locked Brain-suggestion option-row
133
+ * block from a normalized optionRows array. Each row is two lines:
134
+ * Row 1: `<glyph> <N>. <verb>` left-padded, `<conf>%` right-aligned to 80c
135
+ * Row 2: 5-space indent, `<meta>`
136
+ * Per audit Section 5.2.1. Returns the composed string (one block).
137
+ */
138
+ function composeBrainOptionRows(optionRows) {
139
+ if (!Array.isArray(optionRows) || optionRows.length === 0) return '';
140
+ const COL_WIDTH = 80;
141
+ const blocks = [];
142
+ for (let i = 0; i < optionRows.length; i += 1) {
143
+ const r = optionRows[i] || {};
144
+ const glyph = (r.glyph === BRAIN_GLYPH_RECOMMENDED || r.glyph === BRAIN_GLYPH_ALTERNATIVE)
145
+ ? r.glyph : BRAIN_GLYPH_ALTERNATIVE;
146
+ const num = Number.isInteger(r.number) ? r.number : (i + 1);
147
+ const verb = (typeof r.verb === 'string' && r.verb.length > 0) ? r.verb : '';
148
+ const confPct = (Number.isInteger(r.confPct)) ? r.confPct : 0;
149
+ const meta = (typeof r.meta === 'string') ? r.meta : '';
150
+ const left = glyph + ' ' + String(num) + '. ' + verb;
151
+ const right = String(confPct) + '%';
152
+ const padCount = Math.max(1, COL_WIDTH - left.length - right.length);
153
+ const row1 = left + ' '.repeat(padCount) + right;
154
+ const row2 = ' ' + meta;
155
+ blocks.push(row1 + '\n' + row2);
156
+ }
157
+ return blocks.join('\n\n');
158
+ }
159
+
84
160
  // Phase 88.2-04: F.* sub-shape registry and JUST_TALK refuse vocabulary.
85
161
  // Phase 88.2-05: 'F.0' Mini Decision Gate prepended (closed-vocab; freeTextOffered:false carve-out).
86
162
  // Phase 88.2-06: 'F.6' Plan Review Round appended (closed-vocab; routes to the
@@ -301,6 +377,74 @@ function appendAskUserQuestionTrailer(rendered, subShape) {
301
377
  return rendered;
302
378
  }
303
379
 
380
+ /**
381
+ * Phase 121.5-10 Sub-plan K: overlay the locked Brain-suggestion content
382
+ * template onto a rendered F.* envelope per audit Section 5.2. Mutates the
383
+ * rendered object in place. Locked slot values per LOCKED Bundle A decisions:
384
+ *
385
+ * - Header chip: [■ BRAIN] (payload.header overrides)
386
+ * - Question line: "Choose next move:" (payload.questionLine overrides;
387
+ * rendered as the first line of zones.body BEFORE the
388
+ * two-line option rows)
389
+ * - Option rows: Two-line dense per Section 5.2.1; composed from
390
+ * payload.optionRows via composeBrainOptionRows().
391
+ * - Footer: Stat-strip line `▶ Brain · top-K of N ranked · cyan
392
+ * = informing` (payload.footer overrides). Prepended
393
+ * to existing footer with a blank-line separator.
394
+ *
395
+ * alias_map: when payload.alias_map is set, contract.alias_map surfaces it
396
+ * for downstream consumers (the selection-time canonical-verb collapse runs
397
+ * in the consumer via aliasToCanonical(verb_chosen, contract.alias_map) per
398
+ * LOCKED decision 1).
399
+ */
400
+ function applyBrainSuggestionVariant(rendered, payloadObj) {
401
+ if (!rendered || typeof rendered !== 'object') return rendered;
402
+ if (!rendered.zones || typeof rendered.zones !== 'object') return rendered;
403
+
404
+ // Header: enforce the locked chip (allow caller override).
405
+ const header = (typeof payloadObj.header === 'string' && payloadObj.header.length > 0)
406
+ ? payloadObj.header : BRAIN_CHIP;
407
+ rendered.zones.header = header;
408
+
409
+ // Body: compose two-line dense option rows + optional question line on top.
410
+ // If optionRows missing, keep the renderer's body (graceful: a Mode B / no-
411
+ // packet caller may legitimately have no rows to render).
412
+ const questionLine = (typeof payloadObj.questionLine === 'string' && payloadObj.questionLine.length > 0)
413
+ ? payloadObj.questionLine : 'Choose next move:';
414
+ if (Array.isArray(payloadObj.optionRows) && payloadObj.optionRows.length > 0) {
415
+ const rowsBlock = composeBrainOptionRows(payloadObj.optionRows);
416
+ rendered.zones.body = questionLine + '\n\n' + rowsBlock;
417
+ }
418
+
419
+ // Footer: prepend the stat-strip caption ahead of any existing footer (the
420
+ // AskUserQuestion trailer is appended AFTER this overlay; see pickShape).
421
+ const footer = (typeof payloadObj.footer === 'string' && payloadObj.footer.length > 0)
422
+ ? payloadObj.footer : null;
423
+ if (footer !== null) {
424
+ const existing = rendered.zones.footer;
425
+ if (existing === null || existing === undefined || existing === '') {
426
+ rendered.zones.footer = footer;
427
+ } else {
428
+ rendered.zones.footer = footer + '\n\n' + String(existing);
429
+ }
430
+ }
431
+
432
+ // alias_map: surface on contract so downstream selection handlers can
433
+ // call aliasToCanonical(verb_chosen, contract.alias_map) per LOCKED
434
+ // decision 1 (aliases render to user, canonical persists to graph).
435
+ if (payloadObj.alias_map && typeof payloadObj.alias_map === 'object') {
436
+ if (!rendered.contract || typeof rendered.contract !== 'object') {
437
+ rendered.contract = {};
438
+ }
439
+ rendered.contract.alias_map = payloadObj.alias_map;
440
+ }
441
+
442
+ // Surface the locked chip as a scalar for introspection without zone parsing.
443
+ rendered.brain_suggestion_chip = BRAIN_CHIP;
444
+
445
+ return rendered;
446
+ }
447
+
304
448
  function dispatchShapeF(args) {
305
449
  const { roomDir, tier, mode, payload } = args;
306
450
  const payloadObj = (payload && typeof payload === 'object') ? payload : {};
@@ -561,6 +705,18 @@ function pickShape(args) {
561
705
  applyModeBPrefix(result.rendered);
562
706
  }
563
707
 
708
+ // Phase 121.5-10 Sub-plan K: Brain-suggestion variant overlay. Applied
709
+ // BEFORE the AskUserQuestion trailer so the trailer lands AFTER the
710
+ // stat-strip footer per audit Section 5.2. Only fires when payload
711
+ // carries brain_suggestion_variant === true; otherwise no-op (every
712
+ // existing F.* consumer is preserved byte-for-byte).
713
+ if (result && result.shape !== 'error' && !result.passthrough) {
714
+ const payloadObj = (opts.payload && typeof opts.payload === 'object') ? opts.payload : {};
715
+ if (payloadObj.brain_suggestion_variant === true && result.rendered) {
716
+ applyBrainSuggestionVariant(result.rendered, payloadObj);
717
+ }
718
+ }
719
+
564
720
  // Phase 88.2-04: AskUserQuestion structural-marker trailer + telemetry.
565
721
  // Both apply ONLY to successful Shape F.* presentations (umbrella resolves
566
722
  // to F.1/F.6; sub-shapes resolve to themselves). Error paths and G/H/A-E
@@ -619,6 +775,10 @@ function recordSelectorResponse(roomDir, record) {
619
775
  module.exports = {
620
776
  pickShape: pickShape,
621
777
  recordSelectorResponse: recordSelectorResponse,
778
+ // Phase 121.5-10 Sub-plan K: alias-to-canonical helper for graph-edge
779
+ // persistence per LOCKED decision 1 (aliases render to user; canonical
780
+ // verbs persist via navigation.cjs).
781
+ aliasToCanonical: aliasToCanonical,
622
782
  _internal: {
623
783
  resolveTier: resolveTier,
624
784
  modeFromTier: modeFromTier,
@@ -627,9 +787,14 @@ module.exports = {
627
787
  appendAskUserQuestionTrailer: appendAskUserQuestionTrailer,
628
788
  emitPresentationTelemetry: emitPresentationTelemetry,
629
789
  justTalkRefuse: justTalkRefuse,
790
+ applyBrainSuggestionVariant: applyBrainSuggestionVariant,
791
+ composeBrainOptionRows: composeBrainOptionRows,
630
792
  MODE_B_ZONE1_PREFIX: MODE_B_ZONE1_PREFIX,
631
793
  F_SUBSHAPES: F_SUBSHAPES.slice(),
632
794
  JUST_TALK: JUST_TALK,
633
795
  COMPACTION_VIOLATION_CODE: COMPACTION_VIOLATION_CODE,
796
+ BRAIN_CHIP: BRAIN_CHIP,
797
+ BRAIN_GLYPH_RECOMMENDED: BRAIN_GLYPH_RECOMMENDED,
798
+ BRAIN_GLYPH_ALTERNATIVE: BRAIN_GLYPH_ALTERNATIVE,
634
799
  },
635
800
  };
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
3
+ "material_id": "mat-test-001",
4
+ "source_pipeline": "domain",
5
+ "top_differential_score": 0,
6
+ "domain": "tech innovation",
7
+ "bq_subject": "autonomous robotics",
8
+ "top_differential": "<unspecified>: 0.000",
9
+ "semantic_surprise": "A whitespace gap in the canonical decomposition",
10
+ "category_errors_identified": []
11
+ }
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * Phase 121.5-10 Sub-plan K -- locked Brain-suggestion content template
6
+ * adoption tests. Asserts all 5 consumers render the same chip + question
7
+ * + footer shape per audit Section 5.2.
8
+ *
9
+ * Behavior contract (per plan Task 2 behavior block):
10
+ * T1: scripts/suggest-next-command.cjs invokes pickShape with the locked
11
+ * payload. Captured-stdout assertion: [BRAIN] chip + question line +
12
+ * footer substring all appear once.
13
+ * T2: scripts/act-command.cjs --chain (synthetic non-autonomous step)
14
+ * renders F.0 Mini Gate with [BRAIN] chip; NO [continue]/[stop]
15
+ * bracket text.
16
+ * T3: tension-hook-agent surfaceFinding returns rendered with header
17
+ * === [BRAIN]; questionLine === "Resolve pending tension:".
18
+ * T4: auto-explore-agent surfaceFinding returns header === [BRAIN]; BQ
19
+ * anchor appears in question-line slot.
20
+ * T5: reverse-salient-agent surfaceFinding returns header === [BRAIN];
21
+ * persona suffix appears in body slot beneath the chip.
22
+ * T6: Shape isomorphism -- all 5 consumers share the same chip header.
23
+ *
24
+ * No em-dash check applies.
25
+ *
26
+ * Hermetic: reads only the live repo files. No network. No db. No spawn
27
+ * (the consumer CLIs are invoked via direct require + function call, not
28
+ * child_process, to keep the test fast and deterministic).
29
+ */
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const { spawnSync } = require('child_process');
35
+
36
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
37
+
38
+ let pass = 0;
39
+ let fail = 0;
40
+ const failures = [];
41
+
42
+ function assert(cond, name, detail) {
43
+ if (cond) {
44
+ pass++;
45
+ console.log('PASS ' + name);
46
+ } else {
47
+ fail++;
48
+ failures.push(name + (detail ? ' -- ' + detail : ''));
49
+ console.log('FAIL ' + name + (detail ? ' -- ' + detail : ''));
50
+ }
51
+ }
52
+
53
+ const CHIP = '[■ BRAIN]'; // [■ BRAIN]
54
+
55
+ // --- T1: /mos:suggest-next renders the locked chip + footer ---
56
+ {
57
+ const result = spawnSync('node',
58
+ [path.join(REPO_ROOT, 'scripts', 'suggest-next-command.cjs')],
59
+ { encoding: 'utf8', cwd: REPO_ROOT });
60
+ const stdout = String(result.stdout || '');
61
+ assert(stdout.indexOf(CHIP) >= 0, 'T1a: suggest-next stdout contains the [BRAIN] chip', 'chip not found');
62
+ assert(stdout.indexOf('Choose next move:') >= 0,
63
+ 'T1b: suggest-next stdout contains "Choose next move:" question line');
64
+ assert(stdout.indexOf('▶ Brain · top-') >= 0,
65
+ 'T1c: suggest-next stdout contains the stat-strip footer prefix');
66
+ }
67
+
68
+ // --- T2: /mos:act --chain renders F.0 with chip; NO bracket text ---
69
+ {
70
+ const actCmd = require(path.join(REPO_ROOT, 'scripts', 'act-command.cjs'));
71
+ // Synthesize a non-autonomous chain step to force the F.0 gate path.
72
+ const workflow = [
73
+ { step: 1, command: '/mos:beautiful-question', framework: 'BQF' },
74
+ { step: 2, command: '/mos:think-hats', framework: 'Six Thinking Hats' },
75
+ ];
76
+ const autonomyReport = { runnable: false, blockers: [{ step: 2, reason: 'not autonomous_safe' }] };
77
+ const plan = actCmd.planChainRun(workflow, autonomyReport);
78
+ const rendered = actCmd.renderChainReport('test-seed', ['BQF', 'STH'], workflow, autonomyReport, plan);
79
+
80
+ assert(rendered.indexOf(CHIP) >= 0, 'T2a: act --chain gate contains the [BRAIN] chip');
81
+ assert(rendered.indexOf('[continue]') < 0,
82
+ 'T2b: act --chain gate has NO [continue] bracket text');
83
+ assert(rendered.indexOf('[stop]') < 0,
84
+ 'T2c: act --chain gate has NO [stop] bracket text');
85
+ // F.0 closed vocab markers
86
+ assert(/Approve/.test(rendered), 'T2d: F.0 Approve verb present');
87
+ assert(/Reject/.test(rendered), 'T2e: F.0 Reject verb present');
88
+ assert(/Defer/.test(rendered), 'T2f: F.0 Defer verb present');
89
+ }
90
+
91
+ // --- T3: tension-hook-agent surfaceFinding ---
92
+ {
93
+ const agent = require(path.join(REPO_ROOT, 'lib', 'agents', 'tension-hook-agent.cjs'));
94
+ assert(agent.F1_HEADER === CHIP, 'T3a: F1_HEADER constant is the [BRAIN] chip', agent.F1_HEADER);
95
+ assert(Array.isArray(agent.F1_VERBS) && agent.F1_VERBS.length === 3
96
+ && agent.F1_VERBS[0] === 'Resolve' && agent.F1_VERBS[1] === 'Later' && agent.F1_VERBS[2] === 'Skip',
97
+ 'T3b: F1_VERBS retained as [Resolve, Later, Skip] (LOCKED decision 1 aliases)');
98
+
99
+ const finding = agent.composeFinding({
100
+ tension_id: 'a'.repeat(32),
101
+ source_node_id: 'src-node',
102
+ target_node_id: 'tgt-node',
103
+ source_section: 'problem',
104
+ target_section: 'solution',
105
+ tension_type: 'CONTRADICTS',
106
+ });
107
+ const r = agent.surfaceFinding({
108
+ finding: finding,
109
+ roomDir: path.join(REPO_ROOT, 'lib', 'memory'),
110
+ operator: null,
111
+ tier: 2,
112
+ });
113
+ assert(r && r.surfaced === true, 'T3c: surfaced === true');
114
+ assert(r.rendered && r.rendered.zones && r.rendered.zones.header === CHIP,
115
+ 'T3d: rendered.zones.header === [BRAIN]', r.rendered && r.rendered.zones && r.rendered.zones.header);
116
+ assert(r.rendered && r.rendered.zones && typeof r.rendered.zones.body === 'string'
117
+ && r.rendered.zones.body.indexOf('Resolve pending tension:') === 0,
118
+ 'T3e: body starts with "Resolve pending tension:"');
119
+ }
120
+
121
+ // --- T4: auto-explore-agent surfaceFinding ---
122
+ {
123
+ const agent = require(path.join(REPO_ROOT, 'lib', 'agents', 'auto-explore-agent.cjs'));
124
+ // Build a minimal finding object. The agent's composeAutoExploreFinding is
125
+ // its full constructor; we synthesize a finding with the minimum fields
126
+ // surfaceFinding needs.
127
+ const finding = {
128
+ id: 'b'.repeat(32),
129
+ material_id: 'mat-test-001',
130
+ source_pipeline: 'domain',
131
+ top_differential_score: 0.85,
132
+ domain: 'tech innovation',
133
+ bq_subject: 'autonomous robotics',
134
+ };
135
+ const r = agent.surfaceFinding({
136
+ finding: finding,
137
+ roomDir: path.join(REPO_ROOT, 'lib', 'memory'),
138
+ operator: 'AUTONOMOUS',
139
+ tier: 2,
140
+ });
141
+ assert(r && r.surfaced === true, 'T4a: surfaced === true');
142
+ assert(r.rendered && r.rendered.zones && r.rendered.zones.header === CHIP,
143
+ 'T4b: rendered.zones.header === [BRAIN]', r.rendered && r.rendered.zones && r.rendered.zones.header);
144
+ // The BQ-anchored Larry voice line lands in the question-line slot beneath
145
+ // the chip; the body should NOT be empty.
146
+ assert(r.rendered && r.rendered.zones && typeof r.rendered.zones.body === 'string'
147
+ && r.rendered.zones.body.length > 0,
148
+ 'T4c: body slot is non-empty (carries the BQ-anchored line)');
149
+ }
150
+
151
+ // --- T5: reverse-salient-agent surfaceFinding ---
152
+ // reverse-salient-agent returns { surfaced, dispatchResult: { shape, rendered } }
153
+ // rather than the surfaced.rendered envelope used by tension-hook + auto-explore.
154
+ // This is the Phase 89-07 native shape; the test asserts against
155
+ // dispatchResult.rendered.zones to match the contract.
156
+ {
157
+ const agent = require(path.join(REPO_ROOT, 'lib', 'agents', 'reverse-salient-agent.cjs'));
158
+ const finding = {
159
+ id: 'c'.repeat(32),
160
+ body_text: 'rs-engine finding body text',
161
+ brain_chain_text: 'BQF -> STH',
162
+ };
163
+ const roleBlend = { primary_role: 'founder' };
164
+ const r = agent.surfaceFinding({
165
+ finding: finding,
166
+ roomDir: path.join(REPO_ROOT, 'lib', 'memory'),
167
+ operator: null,
168
+ tier: 2,
169
+ roleBlend: roleBlend,
170
+ });
171
+ assert(r && r.surfaced === true, 'T5a: surfaced === true');
172
+ const rendered = r && r.dispatchResult && r.dispatchResult.rendered;
173
+ assert(rendered && rendered.zones && rendered.zones.header === CHIP,
174
+ 'T5b: dispatchResult.rendered.zones.header === [BRAIN]',
175
+ rendered && rendered.zones && rendered.zones.header);
176
+ // Persona suffix moves into the body slot beneath the chip
177
+ assert(rendered && rendered.zones && typeof rendered.zones.body === 'string'
178
+ && rendered.zones.body.indexOf('lens') >= 0,
179
+ 'T5c: body slot carries the persona "lens" suffix');
180
+ }
181
+
182
+ // --- T6: Shape isomorphism across all 5 consumers ---
183
+ {
184
+ // All 5 consumers must render the same [BRAIN] chip header. The chip is
185
+ // exact: 9 chars including brackets, leading filled square + 5-char brand.
186
+ assert(CHIP.length === 9, 'T6a: CHIP is exactly 9 chars (audit Section 5.2.1)');
187
+ // T1 + T2 already covered the CLI consumers; T3 + T4 + T5 covered the 3
188
+ // agentic surfaces. Each test independently asserted the chip; the test
189
+ // file's GREEN exit implies isomorphism.
190
+ assert(true, 'T6b: 5 of 5 consumers asserted to carry the [BRAIN] chip (see T1..T5)');
191
+ }
192
+
193
+ console.log('');
194
+ console.log('brain-suggestion-template.test.cjs: ' + pass + ' passed, ' + fail + ' failed');
195
+ if (fail > 0) {
196
+ console.log('FAILURES:');
197
+ for (const f of failures) console.log(' - ' + f);
198
+ process.exit(1);
199
+ }
200
+ process.exit(0);
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * Phase 121.5-10 Sub-plan K -- selector alias_map unit tests.
6
+ *
7
+ * Behavior contract (per plan Task 1 behavior block):
8
+ * T1: jtbd-taxonomy.json parses with NEW top-level key alias_map.verb_aliases
9
+ * containing the 4 LOCKED entries (Resolve, Explore, Later, Skip) per
10
+ * LOCKED decision 1.
11
+ * T2: aliasToCanonical round-trip: 'Resolve' -> 'Run Methodology';
12
+ * 'Run Methodology' (already canonical) -> 'Run Methodology' (no-op).
13
+ * T3: pickShape with payload.alias_map: { 'Resolve': 'Run Methodology' }
14
+ * honors LOCKED decision 1 -- contract.alias_map is surfaced to
15
+ * consumers so they can collapse verb_chosen to verb_canonical at
16
+ * selection time.
17
+ * T4: Render-vs-persist invariant: when payload.verbs carries an alias
18
+ * like 'Resolve', the alias label appears in rendered.zones.body;
19
+ * the canonical 'Run Methodology' is what aliasToCanonical returns
20
+ * for graph-edge persistence.
21
+ * T5: Unknown alias passes through unchanged (no-op for non-aliased verbs).
22
+ *
23
+ * No em-dash check applies (this is a test file; no narrative prose).
24
+ *
25
+ * Hermetic: reads only the live repo files via require/fs.readFileSync. No
26
+ * network. No db. No spawn.
27
+ */
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
34
+ const TAXONOMY_PATH = path.join(REPO_ROOT, 'lib', 'hmi', 'jtbd-taxonomy.json');
35
+ const DISPATCHER = require(path.join(REPO_ROOT, 'lib', 'hmi', 'selector-dispatcher.cjs'));
36
+
37
+ let pass = 0;
38
+ let fail = 0;
39
+ const failures = [];
40
+
41
+ function assert(cond, name, detail) {
42
+ if (cond) {
43
+ pass++;
44
+ console.log('PASS ' + name);
45
+ } else {
46
+ fail++;
47
+ failures.push(name + (detail ? ' -- ' + detail : ''));
48
+ console.log('FAIL ' + name + (detail ? ' -- ' + detail : ''));
49
+ }
50
+ }
51
+
52
+ // --- T1: jtbd-taxonomy.json carries alias_map.verb_aliases with 4 LOCKED entries ---
53
+ {
54
+ const raw = fs.readFileSync(TAXONOMY_PATH, 'utf8');
55
+ let parsed;
56
+ try { parsed = JSON.parse(raw); } catch (e) { parsed = null; }
57
+ assert(parsed !== null, 'T1a: jtbd-taxonomy.json parses as JSON');
58
+ assert(parsed && parsed.alias_map && typeof parsed.alias_map === 'object',
59
+ 'T1b: top-level alias_map block present');
60
+ const va = parsed && parsed.alias_map && parsed.alias_map.verb_aliases;
61
+ assert(va && typeof va === 'object', 'T1c: alias_map.verb_aliases present');
62
+ assert(va && va.Resolve === 'Run Methodology',
63
+ 'T1d: Resolve -> Run Methodology', va && va.Resolve);
64
+ assert(va && va.Explore === 'Run Methodology',
65
+ 'T1e: Explore -> Run Methodology', va && va.Explore);
66
+ assert(va && va.Later === 'Defer',
67
+ 'T1f: Later -> Defer', va && va.Later);
68
+ assert(va && va.Skip === 'Free-Text',
69
+ 'T1g: Skip -> Free-Text', va && va.Skip);
70
+ }
71
+
72
+ // --- T2: aliasToCanonical round-trip ---
73
+ {
74
+ assert(DISPATCHER.aliasToCanonical('Resolve') === 'Run Methodology',
75
+ 'T2a: aliasToCanonical(Resolve) === Run Methodology');
76
+ assert(DISPATCHER.aliasToCanonical('Explore') === 'Run Methodology',
77
+ 'T2b: aliasToCanonical(Explore) === Run Methodology');
78
+ assert(DISPATCHER.aliasToCanonical('Later') === 'Defer',
79
+ 'T2c: aliasToCanonical(Later) === Defer');
80
+ assert(DISPATCHER.aliasToCanonical('Skip') === 'Free-Text',
81
+ 'T2d: aliasToCanonical(Skip) === Free-Text');
82
+ // Already-canonical pass-through (no-op)
83
+ assert(DISPATCHER.aliasToCanonical('Run Methodology') === 'Run Methodology',
84
+ 'T2e: aliasToCanonical(Run Methodology) is no-op');
85
+ assert(DISPATCHER.aliasToCanonical('Defer') === 'Defer',
86
+ 'T2f: aliasToCanonical(Defer) is no-op');
87
+ assert(DISPATCHER.aliasToCanonical('Free-Text') === 'Free-Text',
88
+ 'T2g: aliasToCanonical(Free-Text) is no-op');
89
+ }
90
+
91
+ // --- T3: pickShape carries alias_map onto rendered.contract.alias_map ---
92
+ {
93
+ const result = DISPATCHER.pickShape({
94
+ requestedShape: 'F.1',
95
+ tier: 2,
96
+ payload: {
97
+ brain_suggestion_variant: true,
98
+ header: '[■ BRAIN]',
99
+ questionLine: 'Choose next move:',
100
+ verbs: ['Resolve', 'Later', 'Skip'],
101
+ alias_map: { 'Resolve': 'Run Methodology', 'Later': 'Defer', 'Skip': 'Free-Text' },
102
+ optionRows: [
103
+ { glyph: '▷', number: 1, verb: 'Resolve', confPct: 70, meta: 'tension hook' },
104
+ { glyph: '▷', number: 2, verb: 'Later', confPct: 50, meta: 'queue for milestone' },
105
+ { glyph: '▷', number: 3, verb: 'Skip', confPct: 30, meta: 'silent dismiss' },
106
+ ],
107
+ footer: '▶ Brain · top-3 of 3 ranked · cyan = informing',
108
+ },
109
+ });
110
+ assert(result && result.shape === 'F.1', 'T3a: pickShape returned shape F.1');
111
+ assert(result && result.rendered && result.rendered.contract
112
+ && result.rendered.contract.alias_map
113
+ && result.rendered.contract.alias_map.Resolve === 'Run Methodology',
114
+ 'T3b: contract.alias_map carries the Resolve -> Run Methodology mapping');
115
+ }
116
+
117
+ // --- T4: Render-vs-persist invariant ---
118
+ {
119
+ const result = DISPATCHER.pickShape({
120
+ requestedShape: 'F.1',
121
+ tier: 2,
122
+ payload: {
123
+ brain_suggestion_variant: true,
124
+ header: '[■ BRAIN]',
125
+ verbs: ['Resolve', 'Later', 'Skip'],
126
+ alias_map: { 'Resolve': 'Run Methodology', 'Later': 'Defer', 'Skip': 'Free-Text' },
127
+ optionRows: [
128
+ { glyph: '▷', number: 1, verb: 'Resolve', confPct: 70, meta: 'tension hook' },
129
+ ],
130
+ },
131
+ });
132
+ const body = (result && result.rendered && result.rendered.zones && result.rendered.zones.body) || '';
133
+ // Alias label appears in rendered body (render to user).
134
+ assert(body.indexOf('Resolve') >= 0, 'T4a: alias label "Resolve" appears in rendered body');
135
+ // aliasToCanonical produces canonical for graph-edge persistence.
136
+ assert(DISPATCHER.aliasToCanonical('Resolve', result.rendered.contract.alias_map)
137
+ === 'Run Methodology',
138
+ 'T4b: aliasToCanonical("Resolve", contract.alias_map) === Run Methodology');
139
+ // The canonical "Run Methodology" string is what would persist to the graph edge,
140
+ // NOT the alias "Resolve" -- this is the render-vs-persist split.
141
+ }
142
+
143
+ // --- T5: Unknown alias passes through unchanged ---
144
+ {
145
+ assert(DISPATCHER.aliasToCanonical('NotAnAlias') === 'NotAnAlias',
146
+ 'T5a: unknown verb is a no-op');
147
+ assert(DISPATCHER.aliasToCanonical('') === '',
148
+ 'T5b: empty string is a no-op (input returned as-is)');
149
+ assert(DISPATCHER.aliasToCanonical(null) === null,
150
+ 'T5c: null is a no-op (input returned as-is)');
151
+ assert(DISPATCHER.aliasToCanonical(undefined) === undefined,
152
+ 'T5d: undefined is a no-op (input returned as-is)');
153
+ }
154
+
155
+ // --- T6: explicit aliasMap arg overrides the module-level cache ---
156
+ {
157
+ const customMap = { 'CustomAlias': 'Run Methodology' };
158
+ assert(DISPATCHER.aliasToCanonical('CustomAlias', customMap) === 'Run Methodology',
159
+ 'T6a: explicit aliasMap arg honored over module default');
160
+ // Module-level cache untouched -- Resolve still resolves via the default map.
161
+ assert(DISPATCHER.aliasToCanonical('Resolve') === 'Run Methodology',
162
+ 'T6b: module-level alias_map cache preserved after explicit-arg call');
163
+ }
164
+
165
+ console.log('');
166
+ console.log('selector-alias-map.test.cjs: ' + pass + ' passed, ' + fail + ' failed');
167
+ if (fail > 0) {
168
+ console.log('FAILURES:');
169
+ for (const f of failures) console.log(' - ' + f);
170
+ process.exit(1);
171
+ }
172
+ process.exit(0);
@@ -68,6 +68,14 @@ const TAXONOMY_PATH = path.join(__dirname, '..', 'hmi', 'jtbd-taxonomy.json');
68
68
  // cross-module coupling on the hot ranking path.
69
69
  const DEFAULT_SEED = 'Beautiful Question Framework';
70
70
 
71
+ // Phase 121.5-10 Sub-plan K (audit Section 5.2.3 anti-pattern "More than 3
72
+ // lines per option"): the locked Brain-suggestion template renders TWO lines
73
+ // per option row (glyph + verb + score line, then meta line). With the
74
+ // AskUserQuestion auto-injected "Type something" / "Chat about this" rows
75
+ // plus the footer stat-strip, the visual budget caps at 3 user-facing
76
+ // options. MAX_K = 3 clamps caller k to fit the locked mockup row budget.
77
+ const MAX_K = 3;
78
+
71
79
  // Per-process caches. The registry + taxonomy are generated artifacts that do
72
80
  // not change during a run; reading each once is the command-resolver.cjs
73
81
  // precedent. Tests can override the registry via _test._setRegistry.
@@ -284,6 +292,50 @@ function _scoreCommand({ cmd, packetOptional, roomState, investment_level }) {
284
292
  return numerator / denominator;
285
293
  }
286
294
 
295
+ // Phase 121.5-10 Sub-plan K: derive optional `category` field for the locked
296
+ // Brain-suggestion template meta row. Sourced from packetOptional's
297
+ // local_graph_summary.category_hint when present, falling back to the
298
+ // command's registry-declared `kind` field, falling back to '' (empty string
299
+ // is acceptable per the locked template -- the meta row renders blank).
300
+ function _categoryFromPacket(packetOptional, cmd) {
301
+ if (packetOptional && packetOptional.local_graph_summary
302
+ && typeof packetOptional.local_graph_summary.category_hint === 'string'
303
+ && packetOptional.local_graph_summary.category_hint.length > 0) {
304
+ return packetOptional.local_graph_summary.category_hint;
305
+ }
306
+ if (cmd && typeof cmd.kind === 'string' && cmd.kind.length > 0) return cmd.kind;
307
+ return '';
308
+ }
309
+
310
+ // Phase 121.5-10 Sub-plan K: derive optional `graph_relationship` field for
311
+ // the locked Brain-suggestion template meta row. Sourced from
312
+ // packetOptional's framework_chain_hint -- the highest-confidence edge that
313
+ // touches the framework gets its relationship rendered (e.g. "feeds Porter
314
+ // Five Forces"). Falls back to '' when no edge data exists.
315
+ function _graphRelationshipFromPacket(packetOptional, framework) {
316
+ if (!packetOptional || !packetOptional.local_graph_summary) return '';
317
+ const hint = packetOptional.local_graph_summary.framework_chain_hint;
318
+ if (!hint || !Array.isArray(hint.edges)) return '';
319
+ let bestEdge = null;
320
+ let bestConf = -1;
321
+ for (const e of hint.edges) {
322
+ if (!e) continue;
323
+ const touches = (e.from === framework) || (e.to === framework);
324
+ if (touches && typeof e.confidence === 'number' && e.confidence > bestConf) {
325
+ bestConf = e.confidence;
326
+ bestEdge = e;
327
+ }
328
+ }
329
+ if (!bestEdge) return '';
330
+ const rel = (typeof bestEdge.relationship === 'string' && bestEdge.relationship.length > 0)
331
+ ? bestEdge.relationship : 'feeds';
332
+ const target = (bestEdge.from === framework)
333
+ ? (typeof bestEdge.to === 'string' ? bestEdge.to : '')
334
+ : (typeof bestEdge.from === 'string' ? bestEdge.from : '');
335
+ if (target.length === 0) return rel;
336
+ return rel + ' ' + target;
337
+ }
338
+
287
339
  // Per-command source attribution per Test 5 (CONTEXT.md acceptance):
288
340
  // packet has framework_chain_hint with edges -> 'packet'
289
341
  // command has frameworks[] (chain-recommender-derivable)-> 'chain'
@@ -316,7 +368,13 @@ function rankForSelector(args) {
316
368
  const roomState = (o.roomState && typeof o.roomState === 'object') ? o.roomState : {};
317
369
  const packetOptional = (o.packetOptional && typeof o.packetOptional === 'object')
318
370
  ? o.packetOptional : null;
319
- const k = (typeof o.k === 'number' && o.k > 0) ? Math.floor(o.k) : 3;
371
+ // Phase 121.5-10 Sub-plan K: clamp k at MAX_K (3) per audit Section 5.2.3
372
+ // anti-pattern -- more than 3 option rows pushes the auto-injected
373
+ // AskUserQuestion "Type something" / "Chat about this" rows off-screen
374
+ // and breaks the locked template visual budget. Caller asking k=20
375
+ // receives k=MAX_K silently. Existing default k=3 unchanged.
376
+ const requestedK = (typeof o.k === 'number' && o.k > 0) ? Math.floor(o.k) : 3;
377
+ const k = Math.min(requestedK, MAX_K);
320
378
  const applyDecayWeight = (typeof o._applyDecayWeight === 'function')
321
379
  ? o._applyDecayWeight : null;
322
380
 
@@ -366,6 +424,14 @@ function rankForSelector(args) {
366
424
  const why = selectWhyContent(jtbd_summary, teaching, investment_level);
367
425
  const source = _sourceFor(packetOptional, cmd);
368
426
 
427
+ // Phase 121.5-10 Sub-plan K: optional category + graph_relationship
428
+ // fields for the locked Brain-suggestion template meta row (audit
429
+ // Section 5.2.1 Row 2). Non-breaking -- consumers that ignore them get
430
+ // the same shape they had before; consumers wiring the locked template
431
+ // (suggest-next, act --chain) read them to compose optionRows[].meta.
432
+ const category = _categoryFromPacket(packetOptional, cmd);
433
+ const graph_relationship = _graphRelationshipFromPacket(packetOptional, framework);
434
+
369
435
  scored.push({
370
436
  command: cmd.command,
371
437
  jtbd_label,
@@ -376,6 +442,8 @@ function rankForSelector(args) {
376
442
  why,
377
443
  source,
378
444
  investment_level,
445
+ category,
446
+ graph_relationship,
379
447
  });
380
448
  }
381
449
 
@@ -403,6 +471,10 @@ module.exports = {
403
471
  renderInvestmentBadge,
404
472
  renderSliceBadge,
405
473
  renderNoneFitAffordance,
474
+ // Phase 121.5-10 Sub-plan K: MAX_K constant exported so consumers wiring
475
+ // the locked Brain-suggestion template know the row-budget cap (audit
476
+ // Section 5.2.3 anti-pattern enforcement).
477
+ MAX_K,
406
478
  // Test seam (private; consumed only by lib/memory/f-selector-ranker.test.cjs).
407
479
  _test: {
408
480
  _resetCaches,
@@ -415,6 +487,9 @@ module.exports = {
415
487
  _applyDecay,
416
488
  _recencyDecay,
417
489
  _problemTypeBind,
490
+ _categoryFromPacket,
491
+ _graphRelationshipFromPacket,
418
492
  DEFAULT_SEED,
493
+ MAX_K,
419
494
  },
420
495
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindrian_os/install",
3
- "version": "1.13.0-beta.28",
3
+ "version": "1.13.0-beta.32",
4
4
  "description": "Install MindrianOS into Claude Code with one command -- `npx @mindrian_os/install`. Ships the MindrianOS plugin (Larry + PWS methodology + Data Room) plus a setup/diagnostics CLI (install/doctor/update).",
5
5
  "scripts": {
6
6
  "mcp": "node bin/mindrian-mcp-server.cjs",
@@ -39,6 +39,8 @@ Every output has exactly 4 zones in fixed order. No reordering. No invention.
39
39
 
40
40
  > This section documents the current Shape F sub-shape catalog as shipped through Phase 88.2. Today's seven sub-shapes (F.0 through F.6) are the shipped vocabulary as of Phase 121.5; additive expansion is reserved for future lens-aware variants (e.g. v1.14.0 dual-graph work). Treat the catalog as the current canon, not a closed terminal set.
41
41
 
42
+ > **Orthogonality note (Plan 121.5-10 LOCKED decision 4):** `body_shape:` frontmatter and Shape F sub-shape are ORTHOGONAL axes. `body_shape` describes the LAYOUT discipline of the command body (Shape A Mondrian Board, Shape B Semantic Tree, Shape C Room Card, Shape D Document View, Shape E Action Report). Shape F.x describes the SELECTOR CONTRACT that fires at the close of the body to capture the navigator's next-move decision. A command can carry `body_shape: B` (Semantic Tree layout) AND surface an F.1 selector at its close; these are not competing values -- they describe different surfaces of the same render. Example: `/mos:suggest-next` carries `body_shape: B` (renders the ranked list as a tree) plus an F.1 selector (the verb-pick gate beneath the tree). The `/mos:hmi-status` doctor check enforces body_shape coverage; the F-selector ranker enforces F-shape contracts. The two are not double-counted.
43
+
42
44
  ### Shape A: Mondrian Board
43
45
  **Used by:** `/mos:status`, `/mos:diagnose`, `/mos:radar`, `/mos:admin`
44
46
  Progress bars per section. 10-char: `filled` fill, `dot` empty. Section names left-aligned padded. Entry count + MINTO health (`checkmark`/`dot`/`--`). Summary line at bottom.
@@ -118,6 +120,18 @@ Keyboard: up-arrow / down-arrow (or J / K) to navigate, Enter to select, `?` to
118
120
 
119
121
  State-update hook: append to STATE.md Decisions section with timestamp + chosen verb + context snapshot. A typed edge is added to the local graph: (navigator) -[CHOSE {verb}]-> (current-artifact).
120
122
 
123
+ ##### Brain-suggestion variant (Plan 121.5-10 LOCKED)
124
+
125
+ When the F.1 selector consumes Brain-ranked next moves (the 5 surfaces: `/mos:suggest-next`, `/mos:act --chain` pre-gate, the Phase 116 tension-hook-agent, the Phase 117 auto-explore-agent, and the Phase 89-07 reverse-salient-agent), the renderer applies a LOCKED visual variant per the Phase 121.5 selector audit Section 5.2. The lock is non-negotiable; any future Brain-suggestion consumer MUST follow this shape (the `tests/test-no-bespoke-brain-prompts.sh` CI tripwire enforces it).
126
+
127
+ - **Header chip:** `[■ BRAIN]` (literal -- 9 chars including brackets). Distinct from `[GATE]` / `[CONTEXT]` / `[NEXT MOVE]` chips by the leading filled-square glyph.
128
+ - **Question line:** `Choose next move:` (default; per-surface variants land here per Section 5.3 adoption diffs -- tension surfaces use `Resolve pending tension:`; BQ surfaces use the verbatim Brain BQ name; reverse-salient surfaces carry the persona suffix in the body slot rather than question line because F.0 has no question slot).
129
+ - **Option rows:** TWO lines per row. Row 1 = `<glyph> <N>. <Run Verb>` left-padded + `<conf>%` right-aligned to the 80-col boundary. Row 2 = 5-space indent, `<framework category> · <graph relationship>`. Glyphs: `▶` (right-triangle-filled) for >= 0.7 confidence top pick; `▷` (right-triangle-empty) for sub-0.7 alternatives. The one-glyph hierarchy replaces the verbose `(RECOMMENDED)` tag without consuming horizontal space.
130
+ - **Footer:** Stat-strip line `▶ Brain · top-<K> of <N> ranked · <color> = informing`. Three signals in one line: provenance, scale, and color-legend reminding the navigator that the cyan rail means Brain is informing, not commanding.
131
+ - **Zone 1 left-rail color:** `cyan` default. Yellow-on-cascade for CONTRADICTS edges is DEFERRED to v1.13.2 hotfix per LOCKED decision 3 (audit Section 7 Open Question 3).
132
+ - **Verb-label aliases:** registered aliases ALLOWED per LOCKED decision 1 (audit Section 7 Open Question 1). The dispatcher carries `alias_map` (loaded from `lib/hmi/jtbd-taxonomy.json` `alias_map.verb_aliases`); aliases render to the user; canonical verbs persist to graph edges via `navigation.cjs`. Default 4 aliases: Resolve / Explore -> Run Methodology; Later -> Defer; Skip -> Free-Text.
133
+ - **Full template + slot-value table + anti-patterns rejected:** `docs/F-SELECTOR-CONSUMER-GUIDE.md` Section 4 (NEW; published as part of Plan 121.5-10).
134
+
121
135
  #### Shape F.2 - Path Control
122
136
 
123
137
  Purpose: When the navigator is choosing structure, not content. Plan / Replan variants. Ties to Claude Code Plan Mode.