@mindrian_os/install 1.13.0-beta.16 → 1.13.0-beta.19

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.
Files changed (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/commands/act.md +1 -0
  4. package/commands/admin.md +1 -0
  5. package/commands/analyze-needs.md +2 -0
  6. package/commands/analyze-systems.md +2 -0
  7. package/commands/analyze-timing.md +2 -0
  8. package/commands/auto-explore.md +2 -0
  9. package/commands/beautiful-question.md +2 -0
  10. package/commands/brain-derive.md +2 -0
  11. package/commands/build-knowledge.md +2 -0
  12. package/commands/build-thesis.md +2 -0
  13. package/commands/causal.md +2 -0
  14. package/commands/challenge-assumptions.md +2 -0
  15. package/commands/compare-ventures.md +2 -0
  16. package/commands/dashboard.md +2 -1
  17. package/commands/deep-grade.md +2 -0
  18. package/commands/diagnose.md +21 -1
  19. package/commands/diagnostics.md +14 -3
  20. package/commands/doctor.md +4 -1
  21. package/commands/dogfood-flush.md +92 -0
  22. package/commands/dominant-designs.md +2 -0
  23. package/commands/explain-decision.md +2 -0
  24. package/commands/explore-domains.md +2 -0
  25. package/commands/explore-futures.md +2 -0
  26. package/commands/explore-trends.md +2 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +2 -0
  29. package/commands/file-meeting.md +4 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +2 -0
  32. package/commands/find-connections.md +2 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +4 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +22 -170
  38. package/commands/help.md +54 -334
  39. package/commands/hmi-status.md +23 -144
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +2 -0
  42. package/commands/lean-canvas.md +2 -0
  43. package/commands/macro-trends.md +2 -0
  44. package/commands/map-unknowns.md +2 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +2 -0
  48. package/commands/mos.md +139 -0
  49. package/commands/mullins.md +2 -0
  50. package/commands/mva-brief.md +58 -0
  51. package/commands/mva-option.md +91 -0
  52. package/commands/new-project.md +4 -0
  53. package/commands/onboard.md +22 -7
  54. package/commands/operator.md +1 -0
  55. package/commands/opportunities.md +1 -0
  56. package/commands/organize.md +22 -469
  57. package/commands/persona.md +1 -0
  58. package/commands/pipeline.md +2 -0
  59. package/commands/present.md +1 -0
  60. package/commands/publish.md +2 -0
  61. package/commands/query.md +24 -102
  62. package/commands/radar.md +2 -0
  63. package/commands/reanalyze.md +1 -0
  64. package/commands/research.md +2 -0
  65. package/commands/room.md +2 -0
  66. package/commands/rooms.md +1 -0
  67. package/commands/root-cause.md +2 -0
  68. package/commands/rs-experts.md +1 -0
  69. package/commands/rs-explain.md +1 -0
  70. package/commands/rs-fetch.md +1 -0
  71. package/commands/rs-thesis.md +1 -0
  72. package/commands/scenario-plan.md +2 -0
  73. package/commands/scheduled-tasks.md +1 -0
  74. package/commands/score-innovation.md +2 -0
  75. package/commands/scout.md +1 -0
  76. package/commands/setup.md +2 -0
  77. package/commands/snapshot.md +2 -0
  78. package/commands/speakers.md +1 -0
  79. package/commands/splash.md +5 -2
  80. package/commands/status.md +1 -0
  81. package/commands/structure-argument.md +2 -0
  82. package/commands/suggest-next.md +2 -0
  83. package/commands/systems-thinking.md +2 -0
  84. package/commands/think-hats.md +2 -0
  85. package/commands/update.md +2 -0
  86. package/commands/user-needs.md +2 -0
  87. package/commands/validate.md +2 -0
  88. package/commands/value-proposition.md +2 -0
  89. package/commands/vault.md +2 -0
  90. package/commands/visualize.md +24 -29
  91. package/commands/whitespace.md +2 -1
  92. package/commands/wiki.md +1 -0
  93. package/hooks/hooks.json +31 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/agents/mva/brain-classic-traps.cjs +77 -0
  96. package/lib/agents/mva/brain-cross-domain.cjs +79 -0
  97. package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
  98. package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
  99. package/lib/agents/mva/index.cjs +42 -0
  100. package/lib/agents/mva/six-hats-red-black.cjs +137 -0
  101. package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
  102. package/lib/agents/mva/test-all-six-agents.cjs +467 -0
  103. package/lib/conversation/operator.cjs +64 -0
  104. package/lib/conversation/operator.test.cjs +160 -0
  105. package/lib/core/breakthrough/canary.cjs +134 -0
  106. package/lib/core/breakthrough/canary.test.cjs +136 -0
  107. package/lib/core/breakthrough/detectors.cjs +359 -0
  108. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  109. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  110. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  111. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  112. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  113. package/lib/core/breakthrough/review-queue.cjs +154 -0
  114. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  115. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  116. package/lib/core/breakthrough/scanner.cjs +426 -0
  117. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  118. package/lib/core/breakthrough/schema.cjs +164 -0
  119. package/lib/core/breakthrough/schema.test.cjs +256 -0
  120. package/lib/core/breakthrough/scoring.cjs +293 -0
  121. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  122. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  123. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  124. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  125. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  126. package/lib/core/first-touch-version-stamper.cjs +113 -0
  127. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  128. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  129. package/lib/core/llm-name-suggester.cjs +194 -0
  130. package/lib/core/llm-name-suggester.test.cjs +132 -0
  131. package/lib/core/mva-agent-contract.cjs +170 -0
  132. package/lib/core/mva-agent-contract.test.cjs +169 -0
  133. package/lib/core/mva-budget.cjs +75 -0
  134. package/lib/core/mva-budget.test.cjs +68 -0
  135. package/lib/core/mva-classifier.cjs +370 -0
  136. package/lib/core/mva-classifier.test.cjs +248 -0
  137. package/lib/core/mva-deck-builder.cjs +452 -0
  138. package/lib/core/mva-deck-builder.test.cjs +287 -0
  139. package/lib/core/mva-detect.smoke.test.cjs +197 -0
  140. package/lib/core/mva-dispatcher.cjs +110 -0
  141. package/lib/core/mva-dispatcher.test.cjs +216 -0
  142. package/lib/core/mva-option-router.cjs +292 -0
  143. package/lib/core/mva-option-router.test.cjs +483 -0
  144. package/lib/core/mva-orchestrator.cjs +365 -0
  145. package/lib/core/mva-orchestrator.test.cjs +908 -0
  146. package/lib/core/mva-progressive-renderer.cjs +194 -0
  147. package/lib/core/mva-progressive-renderer.test.cjs +157 -0
  148. package/lib/core/mva-rule-linter.cjs +213 -0
  149. package/lib/core/mva-rule-linter.test.cjs +336 -0
  150. package/lib/core/mva-state.cjs +159 -0
  151. package/lib/core/mva-telemetry.cjs +58 -0
  152. package/lib/core/mva-telemetry.test.cjs +196 -0
  153. package/lib/core/mva-vercel-deploy.cjs +168 -0
  154. package/lib/core/mva-vercel-deploy.test.cjs +239 -0
  155. package/lib/core/navigation/dashboard-helpers.cjs +145 -0
  156. package/lib/core/navigation/edges.cjs +35 -0
  157. package/lib/core/navigation/memory-events.cjs +126 -0
  158. package/lib/core/navigation.cjs +11 -0
  159. package/lib/core/resolve-vercel-key.cjs +107 -0
  160. package/lib/core/resolve-vercel-key.test.cjs +137 -0
  161. package/lib/core/room-auto-create.cjs +318 -0
  162. package/lib/core/room-auto-create.test.cjs +198 -0
  163. package/lib/core/room-discard-cascade.cjs +225 -0
  164. package/lib/core/room-discard-cascade.test.cjs +135 -0
  165. package/lib/core/room-name-validator.cjs +132 -0
  166. package/lib/core/room-name-validator.test.cjs +156 -0
  167. package/lib/core/room-naming-selector.cjs +357 -0
  168. package/lib/core/room-naming-selector.test.cjs +277 -0
  169. package/lib/core/room-receipt-emit.cjs +63 -0
  170. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  171. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  172. package/lib/core/stale-copy-scanner.cjs +190 -0
  173. package/lib/core/state-aware-router.cjs +78 -0
  174. package/lib/core/telemetry/schema.cjs +168 -0
  175. package/lib/core/telemetry/schema.test.cjs +124 -0
  176. package/lib/core/telemetry/validator.cjs +197 -0
  177. package/lib/core/telemetry/validator.test.cjs +188 -0
  178. package/lib/core/telemetry/writer.cjs +141 -0
  179. package/lib/core/telemetry/writer.test.cjs +331 -0
  180. package/lib/core/terminal-capability.cjs +88 -0
  181. package/lib/core/venture-shape-nudge.cjs +163 -0
  182. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  183. package/lib/core/visual-ops.cjs +70 -2
  184. package/lib/hmi/selector-dispatcher.cjs +90 -1
  185. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  186. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  187. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  188. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  189. package/lib/memory/first-touch-version.test.cjs +198 -0
  190. package/lib/memory/help-coverage.test.cjs +108 -0
  191. package/lib/memory/help-renderer.test.cjs +145 -0
  192. package/lib/memory/palette-consistency.test.cjs +127 -0
  193. package/lib/memory/pending-tension-store.cjs +80 -0
  194. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  195. package/lib/memory/run-feynman-tests.cjs +240 -0
  196. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  197. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  198. package/lib/memory/soft-alias.test.cjs +144 -0
  199. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  200. package/lib/memory/state-aware-router.test.cjs +90 -0
  201. package/lib/memory/statusline-two-row.test.cjs +338 -0
  202. package/lib/memory/terminal-capability.test.cjs +155 -0
  203. package/lib/render/ROOM.md +74 -22
  204. package/lib/sessionstart/budget-compressor.cjs +130 -0
  205. package/lib/sessionstart/contributor-interface.cjs +134 -0
  206. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  207. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  208. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  209. package/lib/statusline/two-row-renderer.cjs +186 -0
  210. package/lib/statusline/version-resolver.cjs +81 -0
  211. package/package.json +1 -1
  212. package/references/visual/ROOM.md +55 -0
  213. package/references/visual/palette.json +54 -0
  214. package/skills/larry-personality/SKILL.md +34 -0
  215. package/skills/mva-pipeline/SKILL.md +129 -0
  216. package/skills/ui-system/SKILL.md +109 -1
  217. package/skills/ui-system/rules/dual-palette.md +156 -0
  218. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  219. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,338 @@
1
+ // statusline-two-row.test.cjs -- Phase 121.5-03 Task 2
2
+ //
3
+ // 12 behavioral tests covering the two-row renderer + version resolver +
4
+ // visual-ops palette derivation + migrate-stale-user-settings bare-path
5
+ // detection.
6
+ //
7
+ // Bash only. No emoji. No em-dashes.
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const { spawnSync } = require('child_process');
15
+
16
+ const REPO_ROOT = path.join(__dirname, '..', '..');
17
+ const RENDERER = path.join(REPO_ROOT, 'lib', 'statusline', 'two-row-renderer.cjs');
18
+ const VERSION_RESOLVER = path.join(REPO_ROOT, 'lib', 'statusline', 'version-resolver.cjs');
19
+ const TRUNCATOR = path.join(REPO_ROOT, 'lib', 'statusline', 'governing-thought-truncator.cjs');
20
+ const VISUAL_OPS = path.join(REPO_ROOT, 'lib', 'core', 'visual-ops.cjs');
21
+ const MIGRATOR = path.join(REPO_ROOT, 'scripts', 'migrate-stale-user-settings.cjs');
22
+
23
+ const renderer = require(RENDERER);
24
+ const versionResolver = require(VERSION_RESOLVER);
25
+
26
+ let passed = 0;
27
+ let failed = 0;
28
+
29
+ function ok(name) {
30
+ console.log(' ok ' + name);
31
+ passed++;
32
+ }
33
+ function fail(name, msg) {
34
+ console.error(' FAIL ' + name + (msg ? ' -- ' + msg : ''));
35
+ failed++;
36
+ }
37
+ function assert(cond, name, msg) {
38
+ if (cond) ok(name);
39
+ else fail(name, msg);
40
+ }
41
+
42
+ console.log('test statusline-two-row');
43
+
44
+ // Test 1: renderRow1 shape with default state.
45
+ try {
46
+ const out = renderer.renderRow1({
47
+ current_version: '1.13.0-beta.18',
48
+ brain_tier: 'BRAIN',
49
+ ctx_pct: 47,
50
+ });
51
+ const hasBrand = out.indexOf('⬡ MindrianOS v1.13.0-beta.18') === 0;
52
+ const hasBrain = out.indexOf('🧠 BRAIN') >= 0;
53
+ const hasBar = out.indexOf('📊') >= 0 && out.indexOf('47%') >= 0;
54
+ const hasSeparators = (out.match(/ │ /g) || []).length === 2;
55
+ assert(hasBrand && hasBrain && hasBar && hasSeparators,
56
+ 'test1 renderRow1 shape',
57
+ 'got: ' + JSON.stringify(out));
58
+ } catch (e) {
59
+ fail('test1 renderRow1 shape', e.message);
60
+ }
61
+
62
+ // Test 2: renderRow1 OMITS the update glyph when update_available is false.
63
+ try {
64
+ const out = renderer.renderRow1({
65
+ current_version: '1.13.0-beta.18',
66
+ brain_tier: 'BRAIN',
67
+ ctx_pct: 30,
68
+ update_available: false,
69
+ });
70
+ const noGlyph = out.indexOf('🔄') === -1;
71
+ assert(noGlyph, 'test2 renderRow1 omits update glyph when not available',
72
+ 'got: ' + JSON.stringify(out));
73
+ } catch (e) {
74
+ fail('test2 renderRow1 omits update glyph', e.message);
75
+ }
76
+
77
+ // Test 3: renderRow1 INCLUDES `🔄 v<new>` when update_available is true.
78
+ try {
79
+ const out = renderer.renderRow1({
80
+ current_version: '1.13.0-beta.18',
81
+ brain_tier: 'LOCAL',
82
+ ctx_pct: 12,
83
+ update_available: true,
84
+ update_available_version: '1.13.0',
85
+ });
86
+ const hasGlyph = out.indexOf('🔄 v1.13.0') >= 0;
87
+ const hasLocal = out.indexOf('🧠 LOCAL') >= 0;
88
+ assert(hasGlyph && hasLocal,
89
+ 'test3 renderRow1 includes update glyph + LOCAL tier',
90
+ 'got: ' + JSON.stringify(out));
91
+ } catch (e) {
92
+ fail('test3 renderRow1 includes update glyph', e.message);
93
+ }
94
+
95
+ // Test 4: renderBar at >=80% renders ANSI blink-red + ⚠ compaction-imminent
96
+ // warning text; at <80% does not. Byte-identical to Phase 106-02 D-02
97
+ // contract (the legacy compaction-imminent text was preserved in the
98
+ // renderer extraction; see test-context-monitor-d02-broadcast.cjs Test 3).
99
+ try {
100
+ const lowBar = renderer.renderBar(45);
101
+ const highBar = renderer.renderBar(85);
102
+ const lowHasBlinkRed = lowBar.indexOf('\x1b[5;31m') >= 0;
103
+ const highHasBlinkRed = highBar.indexOf('\x1b[5;31m') >= 0;
104
+ const highHasWarn = highBar.indexOf('⚠ compaction-imminent') >= 0;
105
+ const lowHasWarn = lowBar.indexOf('⚠') >= 0;
106
+ // Bar geometry: 10 chars, 8 filled at 85, 4 filled at 45
107
+ const lowFilledCount = (lowBar.match(/█/g) || []).length;
108
+ const highFilledCount = (highBar.match(/█/g) || []).length;
109
+ assert(!lowHasBlinkRed && highHasBlinkRed && highHasWarn && !lowHasWarn &&
110
+ lowFilledCount === 4 && highFilledCount === 8,
111
+ 'test4 renderBar -- low=no blink-red/warn, high=blink-red+compaction-imminent, geometry correct',
112
+ 'low=' + JSON.stringify(lowBar) + ' high=' + JSON.stringify(highBar));
113
+ } catch (e) {
114
+ fail('test4 renderBar', e.message);
115
+ }
116
+
117
+ // Test 5: renderRow2 full shape at COLUMNS=120.
118
+ try {
119
+ const out = renderer.renderRow2({
120
+ room: 'MindrianOS-Plugin',
121
+ section: 'product-evolution',
122
+ jtbd: 'find-problem',
123
+ stage: 'ILL_DEFINED',
124
+ governing_thought: 'JTBD-invariant single shape wins',
125
+ operator: 'METHODOLOGY',
126
+ }, 120);
127
+ const hasRoom = out.indexOf('🏠 MindrianOS-Plugin') >= 0;
128
+ const hasSection = out.indexOf('📂 product-evolution') >= 0;
129
+ const hasJTBD = out.indexOf('🎯 find-problem') >= 0;
130
+ const hasStage = out.indexOf('🔍 ILL_DEFINED') >= 0;
131
+ const hasThought = out.indexOf('JTBD-invariant single shape wins') >= 0;
132
+ const hasOperator = out.indexOf('METHODOLOGY') >= 0;
133
+ assert(hasRoom && hasSection && hasJTBD && hasStage && hasThought && hasOperator,
134
+ 'test5 renderRow2 full shape at COLUMNS=120',
135
+ 'got: ' + JSON.stringify(out));
136
+ } catch (e) {
137
+ fail('test5 renderRow2 full shape', e.message);
138
+ }
139
+
140
+ // Test 6: renderRow2 governing-thought truncation at exactly 40 chars boundary.
141
+ try {
142
+ const longThought = 'a'.repeat(50); // 50 chars
143
+ const out = renderer.renderRow2({
144
+ room: 'r',
145
+ section: 's',
146
+ governing_thought: longThought,
147
+ }, 120);
148
+ // 37 'a's + '...'
149
+ const expected = 'a'.repeat(37) + '...';
150
+ const hasExpected = out.indexOf(expected) >= 0;
151
+ // And the un-truncated tail must NOT appear (the 50-char string)
152
+ const hasFull = out.indexOf(longThought) >= 0;
153
+ assert(hasExpected && !hasFull,
154
+ 'test6 governing-thought truncation > 40 chars yields 37 + ...',
155
+ 'got: ' + JSON.stringify(out));
156
+ } catch (e) {
157
+ fail('test6 governing-thought truncation', e.message);
158
+ }
159
+
160
+ // Test 7: renderRow2 at COLUMNS=60 drops the operator segment (60 < 80).
161
+ try {
162
+ const out = renderer.renderRow2({
163
+ room: 'r',
164
+ section: 's',
165
+ jtbd: 'j',
166
+ stage: 'st',
167
+ governing_thought: 'gt',
168
+ operator: 'METHODOLOGY',
169
+ }, 60);
170
+ const noOperator = out.indexOf('METHODOLOGY') === -1;
171
+ const hasStage = out.indexOf('🔍 st') >= 0;
172
+ const hasJTBD = out.indexOf('🎯 j') >= 0;
173
+ assert(noOperator && hasStage && hasJTBD,
174
+ 'test7 renderRow2 COLUMNS=60 drops operator, keeps stage + jtbd',
175
+ 'got: ' + JSON.stringify(out));
176
+ } catch (e) {
177
+ fail('test7 renderRow2 COLUMNS=60', e.message);
178
+ }
179
+
180
+ // Test 8: renderRow2 at COLUMNS=50 drops operator AND stage.
181
+ try {
182
+ const out = renderer.renderRow2({
183
+ room: 'r',
184
+ section: 's',
185
+ jtbd: 'j',
186
+ stage: 'st',
187
+ governing_thought: 'gt',
188
+ operator: 'METHODOLOGY',
189
+ }, 50);
190
+ const noOperator = out.indexOf('METHODOLOGY') === -1;
191
+ const noStage = out.indexOf('🔍') === -1;
192
+ const hasJTBD = out.indexOf('🎯 j') >= 0;
193
+ assert(noOperator && noStage && hasJTBD,
194
+ 'test8 renderRow2 COLUMNS=50 drops operator + stage, keeps jtbd',
195
+ 'got: ' + JSON.stringify(out));
196
+ } catch (e) {
197
+ fail('test8 renderRow2 COLUMNS=50', e.message);
198
+ }
199
+
200
+ // Test 9: renderRow2 at COLUMNS=40 drops operator, stage, AND jtbd.
201
+ try {
202
+ const out = renderer.renderRow2({
203
+ room: 'r',
204
+ section: 's',
205
+ jtbd: 'j',
206
+ stage: 'st',
207
+ governing_thought: 'gt',
208
+ operator: 'METHODOLOGY',
209
+ }, 40);
210
+ const noOperator = out.indexOf('METHODOLOGY') === -1;
211
+ const noStage = out.indexOf('🔍') === -1;
212
+ const noJTBD = out.indexOf('🎯') === -1;
213
+ const hasRoom = out.indexOf('🏠 r') >= 0;
214
+ const hasThought = out.indexOf('gt') >= 0;
215
+ assert(noOperator && noStage && noJTBD && hasRoom && hasThought,
216
+ 'test9 renderRow2 COLUMNS=40 drops operator + stage + jtbd, keeps room + thought',
217
+ 'got: ' + JSON.stringify(out));
218
+ } catch (e) {
219
+ fail('test9 renderRow2 COLUMNS=40', e.message);
220
+ }
221
+
222
+ // Test 10: renderStatusline emits EXACTLY two newline-separated rows.
223
+ try {
224
+ const out = renderer.renderStatusline({
225
+ current_version: '1.13.0-beta.18',
226
+ brain_tier: 'BRAIN',
227
+ ctx_pct: 50,
228
+ room: 'r',
229
+ section: 's',
230
+ jtbd: 'j',
231
+ stage: 'st',
232
+ governing_thought: 'gt',
233
+ operator: 'METHODOLOGY',
234
+ }, 120);
235
+ const lines = out.split('\n');
236
+ assert(lines.length === 2 && lines[0].length > 0 && lines[1].length > 0,
237
+ 'test10 renderStatusline emits exactly 2 newline-separated rows',
238
+ 'lines.length=' + lines.length);
239
+ } catch (e) {
240
+ fail('test10 renderStatusline two-row split', e.message);
241
+ }
242
+
243
+ // Test 11: visual-ops.cjs DS_HEX now sourced from palette.json. Sentinel test:
244
+ // override MINDRIAN_PALETTE_PATH to point at a synthetic palette with a
245
+ // sentinel hex; spawn a child node that requires visual-ops + reports DS_HEX.
246
+ try {
247
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'visual-ops-test-'));
248
+ const sentinelHex = '#abcdef';
249
+ const fakePalette = {
250
+ version: 1,
251
+ base: {
252
+ mondrian_red: sentinelHex,
253
+ mondrian_blue: '#1e3a6e',
254
+ mondrian_yellow: '#c8a43c',
255
+ mondrian_black: '#0d0d0d',
256
+ mondrian_white: '#f5f0e8',
257
+ cream: '#f5f0e8',
258
+ gray_meta: '#a09a90',
259
+ amethyst: '#6b4e8b',
260
+ success_green: '#2d6b4a',
261
+ },
262
+ palette_a_discovery: { muted_red: '#a63d2f', muted_green: '#2d6b4a', teal_accent: '#2a6b5e' },
263
+ palette_b_build: { saturated_red: '#a63d2f', saturated_blue: '#1e3a6e', saturated_yellow: '#c8a43c' },
264
+ extended: { sienna: '#b5602a', bg: '#0d0d0d', surface: '#1a1a1a', elevated: '#2a2a2a', gray: '#5c5a56' },
265
+ };
266
+ const fakePalettePath = path.join(tmpDir, 'fake-palette.json');
267
+ fs.writeFileSync(fakePalettePath, JSON.stringify(fakePalette, null, 2));
268
+
269
+ const child = spawnSync(process.execPath, [
270
+ '-e',
271
+ "const vo = require('" + VISUAL_OPS.replace(/\\/g, '\\\\') + "'); " +
272
+ "process.stdout.write(JSON.stringify(vo.DS_HEX));"
273
+ ], {
274
+ env: Object.assign({}, process.env, { MINDRIAN_PALETTE_PATH: fakePalettePath }),
275
+ encoding: 'utf8',
276
+ });
277
+ if (child.status !== 0) {
278
+ fail('test11 DS_HEX from palette.json -- child failed: ' + child.stderr);
279
+ } else {
280
+ const dshex = JSON.parse(child.stdout);
281
+ // The legacy DS_HEX key is `red` (not `mondrian_red`); the wiring should
282
+ // alias so DS_HEX.red maps to palette.base.mondrian_red. Test the
283
+ // sentinel hex propagation.
284
+ const hasSentinel = dshex.red === sentinelHex || dshex.mondrian_red === sentinelHex;
285
+ assert(hasSentinel,
286
+ 'test11 visual-ops DS_HEX sourced from palette.json sentinel',
287
+ 'got DS_HEX.red=' + dshex.red + ' / DS_HEX.mondrian_red=' + dshex.mondrian_red);
288
+ }
289
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
290
+ } catch (e) {
291
+ fail('test11 DS_HEX from palette.json', e.message);
292
+ }
293
+
294
+ // Test 12: migrate-stale-user-settings.cjs --auto detects bare-path context-monitor
295
+ // and proposes migration via SessionStart envelope.
296
+ try {
297
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-home-'));
298
+ const claudeDir = path.join(tmpHome, '.claude');
299
+ fs.mkdirSync(claudeDir, { recursive: true });
300
+ const fakeSettings = {
301
+ statusLine: {
302
+ type: 'command',
303
+ command: 'node /home/x/.claude/plugins/cache/mindrian-marketplace/mos/1.13.0-beta.5/scripts/context-monitor',
304
+ },
305
+ };
306
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify(fakeSettings, null, 2));
307
+
308
+ // Use --auto --quiet so stdout is JSON-only (no header lines). The QUIET
309
+ // flag was added in Phase 106-01 D-01 specifically for hook-friendly
310
+ // invocation; --auto without --quiet emits header lines + envelope.
311
+ const child = spawnSync(process.execPath, [MIGRATOR, '--auto', '--quiet'], {
312
+ env: Object.assign({}, process.env, { HOME: tmpHome, USERPROFILE: tmpHome }),
313
+ encoding: 'utf8',
314
+ });
315
+ if (child.status !== 0) {
316
+ fail('test12 migrate --auto detects bare-path', 'exit=' + child.status + ' stderr=' + child.stderr);
317
+ } else {
318
+ let envelope = null;
319
+ try { envelope = JSON.parse(child.stdout); } catch (_) {}
320
+ const hasAdditional =
321
+ envelope &&
322
+ envelope.hookSpecificOutput &&
323
+ typeof envelope.hookSpecificOutput.additionalContext === 'string' &&
324
+ envelope.hookSpecificOutput.additionalContext.indexOf('drift') >= 0;
325
+ assert(hasAdditional,
326
+ 'test12 migrate --auto --quiet emits SessionStart envelope with drift warning',
327
+ 'stdout=' + child.stdout);
328
+ }
329
+ try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch (_) {}
330
+ } catch (e) {
331
+ fail('test12 migrate --auto', e.message);
332
+ }
333
+
334
+ // Summary
335
+ console.log('');
336
+ console.log('statusline-two-row.test: ' + passed + ' passed, ' + failed + ' failed');
337
+ if (failed > 0) process.exit(1);
338
+ process.exit(0);
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 121.5-07 Task 1 -- Terminal capability probe + help_jtbd frontmatter
8
+ * sweep acceptance tests.
9
+ *
10
+ * Asserts:
11
+ * - lib/core/terminal-capability.cjs exists with probeCapability() dispatch
12
+ * covering 6 paths (truecolor, 256color via COLORTERM, 256color via TERM,
13
+ * ascii via non-TTY, ascii via no-COLORTERM-no-TERM, ascii via Desktop).
14
+ * - Every commands/<name>.md declares help_jtbd: frontmatter.
15
+ * - Every help_jtbd: value is a quoted string >= 8 and <= 120 chars.
16
+ *
17
+ * Canon references:
18
+ * Part 3 UI Ruling System -- bulletproof terminal coherence depends on a
19
+ * single capability probe shared across surfaces.
20
+ * Part 7 Reuse Before Build -- terminal-capability.cjs consolidates an
21
+ * inline check that would otherwise live in N command renderers;
22
+ * reusable across /mos:status, /mos:doctor, /mos:splash, etc.
23
+ * Part 8 Graph Boundary -- probe reads env-vars + isTTY only; zero
24
+ * network, zero Brain, zero telemetry egress.
25
+ *
26
+ * Test map (8 cases, one-to-one with PLAN Task 1 <behavior>):
27
+ *
28
+ * 1. probeCapability({isTTY:true, env:{COLORTERM:'truecolor'}}) -> 'truecolor'
29
+ * 2. probeCapability({isTTY:true, env:{COLORTERM:'256color'}}) -> '256color'
30
+ * 3. probeCapability({isTTY:true, env:{TERM:'xterm-256color'}}) -> '256color'
31
+ * 4. probeCapability({isTTY:false}) -> 'ascii'
32
+ * 5. probeCapability({isTTY:true, env:{}}) -> 'ascii'
33
+ * 6. probeCapability({isTTY:true, env:{CLAUDE_DESKTOP:'1'}}) -> 'ascii'
34
+ * 7. Every commands/*.md declares help_jtbd: frontmatter.
35
+ * 8. Every help_jtbd: value is quoted, 8..120 chars, no em-dashes.
36
+ */
37
+
38
+ const assert = require('node:assert/strict');
39
+ const fs = require('node:fs');
40
+ const path = require('node:path');
41
+
42
+ const REPO = path.resolve(__dirname, '..', '..');
43
+ const PROBE_PATH = path.join(REPO, 'lib', 'core', 'terminal-capability.cjs');
44
+ const COMMANDS_DIR = path.join(REPO, 'commands');
45
+
46
+ let passed = 0;
47
+ let failed = 0;
48
+ const failures = [];
49
+
50
+ function run(name, fn) {
51
+ try {
52
+ fn();
53
+ console.log(' PASS ' + name);
54
+ passed++;
55
+ } catch (err) {
56
+ console.error(' FAIL ' + name);
57
+ console.error(' ' + (err.message || err));
58
+ failed++;
59
+ failures.push(name);
60
+ }
61
+ }
62
+
63
+ // --- Test 1: truecolor dispatch via COLORTERM ---
64
+ run('Test 1: COLORTERM=truecolor + isTTY=true returns truecolor', () => {
65
+ assert.ok(fs.existsSync(PROBE_PATH), 'lib/core/terminal-capability.cjs must exist');
66
+ const { probeCapability } = require(PROBE_PATH);
67
+ const result = probeCapability({ isTTY: true, env: { COLORTERM: 'truecolor' } });
68
+ assert.equal(result, 'truecolor');
69
+ });
70
+
71
+ // --- Test 2: 256color via COLORTERM ---
72
+ run('Test 2: COLORTERM=256color + isTTY=true returns 256color', () => {
73
+ const { probeCapability } = require(PROBE_PATH);
74
+ const result = probeCapability({ isTTY: true, env: { COLORTERM: '256color' } });
75
+ assert.equal(result, '256color');
76
+ });
77
+
78
+ // --- Test 3: 256color via TERM fallback ---
79
+ run('Test 3: TERM=xterm-256color + isTTY=true (COLORTERM unset) returns 256color', () => {
80
+ const { probeCapability } = require(PROBE_PATH);
81
+ const result = probeCapability({ isTTY: true, env: { TERM: 'xterm-256color' } });
82
+ assert.equal(result, '256color');
83
+ });
84
+
85
+ // --- Test 4: non-TTY -> ascii ---
86
+ run('Test 4: isTTY=false returns ascii (piped/non-TTY)', () => {
87
+ const { probeCapability } = require(PROBE_PATH);
88
+ const result = probeCapability({ isTTY: false, env: { COLORTERM: 'truecolor' } });
89
+ assert.equal(result, 'ascii');
90
+ });
91
+
92
+ // --- Test 5: isTTY=true with neither COLORTERM nor TERM -> ascii ---
93
+ run('Test 5: isTTY=true with empty env returns ascii', () => {
94
+ const { probeCapability } = require(PROBE_PATH);
95
+ const result = probeCapability({ isTTY: true, env: {} });
96
+ assert.equal(result, 'ascii');
97
+ });
98
+
99
+ // --- Test 6: Desktop hard-override ---
100
+ run('Test 6: CLAUDE_DESKTOP=1 returns ascii (Desktop simulator)', () => {
101
+ const { probeCapability } = require(PROBE_PATH);
102
+ const result = probeCapability({
103
+ isTTY: true,
104
+ env: { CLAUDE_DESKTOP: '1', COLORTERM: 'truecolor' },
105
+ });
106
+ assert.equal(result, 'ascii');
107
+ });
108
+
109
+ // --- Test 7: every command has help_jtbd: ---
110
+ run('Test 7: every commands/*.md declares help_jtbd frontmatter', () => {
111
+ const files = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
112
+ assert.ok(files.length >= 80, 'expected >= 80 command files, found ' + files.length);
113
+ const missing = [];
114
+ for (const f of files) {
115
+ const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
116
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
117
+ if (!fm || !/^help_jtbd:/m.test(fm[1])) {
118
+ missing.push(f);
119
+ }
120
+ }
121
+ assert.deepEqual(missing, [], 'commands missing help_jtbd: ' + missing.join(', '));
122
+ });
123
+
124
+ // --- Test 8: help_jtbd value contract (quoted, 8..120 chars, no em-dash) ---
125
+ run('Test 8: every help_jtbd value is quoted, 8..120 chars, no em-dashes', () => {
126
+ const files = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
127
+ const violations = [];
128
+ for (const f of files) {
129
+ const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
130
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
131
+ if (!fm) continue;
132
+ const line = fm[1].split('\n').find((l) => /^help_jtbd:/.test(l));
133
+ if (!line) continue;
134
+ // Must be quoted with double-quotes
135
+ const m = line.match(/^help_jtbd:\s*"([^"]*)"\s*$/);
136
+ if (!m) {
137
+ violations.push(f + ': not double-quoted -> ' + line);
138
+ continue;
139
+ }
140
+ const val = m[1];
141
+ if (val.length < 8) violations.push(f + ': too short (' + val.length + ' chars)');
142
+ if (val.length > 120) violations.push(f + ': too long (' + val.length + ' chars)');
143
+ if (/—/.test(val)) violations.push(f + ': contains em-dash');
144
+ }
145
+ assert.deepEqual(violations, [], 'help_jtbd violations: ' + violations.join('\n '));
146
+ });
147
+
148
+ console.log('');
149
+ console.log('passed=' + passed + ' failed=' + failed);
150
+ if (failed > 0) {
151
+ console.error('Failed tests:');
152
+ for (const t of failures) console.error(' - ' + t);
153
+ process.exit(1);
154
+ }
155
+ process.exit(0);
@@ -9,51 +9,103 @@ The universal renderer translates a 4-zone payload + render mode + conversation
9
9
  ## Phase status
10
10
 
11
11
  - **Phase 99-03 (shipped):** contract surface + no-op pass-through stub. Callers can import `{ render }` today; envelope returns `rendered: false` + `_stub: 'phase-99-03'` provenance tag. Operator validation against the 5 canonical values (JUST_TALK / EXPLORE_CAPTURE / BUILD_ROOM / METHODOLOGY / DECISION_GATE) ships now so Phase 102 inherits the fence.
12
- - **Phase 102 (planned):** replaces the stub internals with real rendering logic per Phase 99 CONTEXT.md D-16. Same import surface; no caller changes.
12
+ - **Phase 102 (shipped):** replaces the stub internals with real rendering logic per Phase 99 CONTEXT.md D-16. Same import surface; no caller changes. The destructured `render({ zones, mode, operator, tier, jtbd, tokenBudget, roomDir, provenance })` API ships; the legacy 4-arg signature stays alive at `lib/render/render.cjs` as a thin shim that forwards to v2.
13
+ - **Phase 121.5-04 (shipped, Sub-plan E):** the long-open question of whether render-v2 should be wired into the ordinary Larry prose path is resolved -- see **Disposition** below.
14
+
15
+ ## Disposition (Sub-plan E, 2026-05-16)
16
+
17
+ > **render-v2 stays agent-surface-only by design.** SKILL.md remains the canonical prose contract for Larry's main conversation path. The v2 destructured `render()` entry is reserved for /mos:* command surfaces and the Shape F decision-gate output side that route through `lib/hmi/selector-dispatcher.cjs`.
18
+
19
+ This is the disposition that Phase 121.5 commits to for v1.13.0 final. The verdict was locked at plan-phase after the 2026-05-16 dual-graph review. Sub-plan E (this plan, 121.5-04) closes the open question; Sub-plan E ALSO closes the Phase 102 release loop by shipping `102-VERIFICATION.md` and flipping `102-VALIDATION.md` out of `status: draft`.
20
+
21
+ ### Allowed render-v2 consumers (current production set)
22
+
23
+ | Consumer | Role | Why allowed |
24
+ |----------|------|-------------|
25
+ | `lib/render/render.cjs` | v1 legacy-signature shim | Reuses v2 muscle for the 4-arg positional API per Phase 102 CONTEXT D-10. One renderer, two entry points (Canon Part 7). |
26
+ | `lib/hmi/selector-dispatcher.cjs` | Shape F / Shape E agentic dispatcher | The /mos:* command output side -- Canon Part 3 tri-context decision-gate enforcement surface. NOT the ordinary prose path. |
27
+
28
+ CI gate: `lib/memory/render-v2-disposition.test.cjs` asserts the allowlist matches this table on every test run. The operator-facing audit lives at `scripts/disposition-render-v2.cjs` (`--json` for machine-readable, default for human summary; exit 1 on any prose-path import).
29
+
30
+ ### Why not wire it into the prose path
31
+
32
+ Three reasons, in priority order:
33
+
34
+ 1. **Canon Part 7 (Reuse Before Build).** SKILL.md is the canonical prose contract today; it ships, it is exercised on every Larry turn, and the 4-zone format is enforced by skill instruction. Replacing instruction-side enforcement with code-side enforcement would be a parallel implementation rather than reuse, which is the bar the canon explicitly raises against.
35
+ 2. **Canon Part 3 (the gate output side).** The v2 destructured signature was designed for Shape F.x gate surfaces -- decision-gate output, not free prose. The compaction layer (102-02), JTBD-aware Zone 4 (102-03), provenance envelope (102-04), and color overlay (102-05) all assume gate-bounded payloads. Wrapping them around ordinary prose would silently change Larry's conversational shape -- a regression risk against Phase 88.1 + 88.2 + the Part 10 capstone work.
36
+ 3. **Phase 121.5 capstone scope.** Sub-plan C reconciles SKILL.md v2 with the shipped Shape F set + dual palette + the 🎯/JTBD disambiguation. After 121.5 lands, SKILL.md IS the source of truth -- a separate v1.14.0 phase can revisit whether code-side prose rendering still earns its keep against a freshly-reconciled SKILL.md.
37
+
38
+ ### Current rankable input signal types (additive expansion reserved)
39
+
40
+ The render-v2 entry today consumes a closed-by-version-but-open-by-future set of typed inputs:
41
+
42
+ | Signal | Source | Phase shipped |
43
+ |--------|--------|----------------|
44
+ | `operator` (5 canonical: JUST_TALK / EXPLORE_CAPTURE / BUILD_ROOM / METHODOLOGY / DECISION_GATE) | Phase 99 conversation operator state machine | 99-03 |
45
+ | `mode` ('A' / 'B' / 'tier-0') | Phase 90 brain-derivation tier-awareness | 102-04 |
46
+ | `tier` (0/1/2/3) | Phase 90 brain-derivation gating | 102-04 |
47
+ | `jtbd` (one of 13 canonical Phase 100 handles or null) | Phase 100 JTBD inference engine | 102-03 |
48
+ | `tokenBudget` ({used, total}) | Phase 102-02 compaction layer | 102-02 |
49
+ | `provenance` (caller-supplied; NOT folded into `_provenance`) | Phase 90 brain-derived inputs | 102-04 |
50
+
51
+ These are the input signal types render-v2 accepts **as of Phase 121.5-04** (additive expansion is reserved for future lens-aware variants once the dual-graph proposal lands -- ASSOCIATION_LENS + TRANSITION_LENS lens classes may add lens-derived signals). The set is current; it is not closed for all time. Per Canon Part 7 language discipline: "current" / "as of" / "today" framing is the convention -- never "final" / "complete" / "closed set" framing.
13
52
 
14
53
  ## Files
15
54
 
16
55
  | File | Role |
17
56
  |------|------|
18
- | render-v2.cjs | Contract surface. Phase 99-03 stub today, Phase 102 implementation later. Exports `{ render, OPERATORS }`. |
19
- | render-v2.test.cjs | Contract tests: 8 IIFE scenarios (5-operator round-trip, JUST_TALK default for undefined and null, invalid-operator throw, envelope shape stable, mode passthrough, tier passthrough, OPERATORS frozen). Registered in lib/memory/run-feynman-tests.cjs. |
57
+ | render-v2.cjs | Contract surface + Phase 102 implementation. Exports `{ render, OPERATORS, composeZones, isCompact, applyCompaction, JTBD_CLI_COLOR, ANSI }`. |
58
+ | render.cjs | v1 legacy-signature shim. Forwards to render-v2 with defensive defaults; returns just the `rendered` string. |
59
+ | render-v2.test.cjs | Contract regression fence: 8 IIFE scenarios (5-operator round-trip, JUST_TALK default for undefined and null, unknown-operator tolerated, envelope shape stable, mode passthrough, tier passthrough, OPERATORS frozen). Registered in lib/memory/run-feynman-tests.cjs. |
60
+ | JTBD-PALETTES.md | CLI + Mondrian dual-palette tables for the 13 canonical JTBDs (Phase 102-05). |
20
61
  | ROOM.md | This file -- ICM Layer 0 identity per CLAUDE.md Decision #15. |
21
62
 
22
- ## Render contract (Phase 99 CONTEXT.md D-16)
63
+ ## Render contract (Phase 99 CONTEXT.md D-16, refined in Phase 102)
23
64
 
24
65
  ```
25
- render(zones, mode, operator, tier) -> envelope
26
-
27
- operator == JUST_TALK -> emit prose only (Phase 102)
28
- operator == EXPLORE_CAPTURE -> prose; Shape E only on crystallization (Phase 102)
29
- operator == BUILD_ROOM -> full 4-zone anatomy (Phase 102)
30
- operator == METHODOLOGY -> no shape mid-session; Shape E at gate (Phase 102)
31
- operator == DECISION_GATE -> Shape F.x; keyboard only (Phase 102)
66
+ render({ zones, mode, operator, tier, jtbd, tokenBudget, roomDir, provenance })
67
+ -> { rendered: string, contract: object, _provenance: <non-enumerable, frozen> }
68
+
69
+ operator == JUST_TALK -> emit prose only; suppress envelope (Phase 102-01)
70
+ operator == EXPLORE_CAPTURE -> prose; Shape E only on crystallization (Phase 102)
71
+ operator == BUILD_ROOM -> full 4-zone anatomy + JTBD-aware Zone 4 (Phase 102-03)
72
+ operator == METHODOLOGY -> no Zone 4 mid-session; Shape E at gate (Phase 102-01)
73
+ operator == DECISION_GATE -> Shape F.x; keyboard only (Phase 102-03)
32
74
  ```
33
75
 
34
- Phase 99-03 stub returns `{ zones, mode, operator, tier, rendered: false, _stub: 'phase-99-03' }` with operator validated and defaulted to JUST_TALK when null/undefined.
76
+ The legacy 4-arg signature lives at `lib/render/render.cjs`:
77
+
78
+ ```
79
+ render(zones, mode, operator, tier) -> string
80
+ ```
35
81
 
36
82
  ## Canon refs
37
83
 
38
- - **Part 3 (Tri-Context Decision Gate):** DECISION_GATE locks Shape F.x; the renderer enforces this at output time (Phase 102).
39
- - **Part 4 (Every Choice Is Graph Data):** operator transitions written by Phase 99-01 are read by this renderer to pick shape (Phase 102).
40
- - **Part 7 (Reuse Before Build):** Phase 99-03 ships the seam, not the muscle. Phase 102 replaces the stub internals without touching callers.
84
+ - **Part 3 (Tri-Context Decision Gate):** DECISION_GATE locks Shape F.x; the renderer enforces this at output time. The v2 entry is reserved for this gate-output side.
85
+ - **Part 4 (Every Choice Is Graph Data):** operator transitions written by Phase 99-01 are read by this renderer to pick shape.
86
+ - **Part 7 (Reuse Before Build):** the v1 shim wraps v2 -- one renderer, two entry points. The Sub-plan E disposition (this section) is itself a Canon Part 7 decision: don't add a parallel prose-side renderer when SKILL.md is the canonical prose contract.
87
+ - **Part 8 (Graph Boundary):** the renderer is pure formatting; no Brain calls anywhere in the chain (D-09).
41
88
 
42
89
  ## Downstream consumers
43
90
 
44
- - **Phase 99-04 (hooks):** imports `{ render }` for SessionStart restore behavior and PostToolUse rendering.
45
- - **Phase 99-05 (`/mos:operator` command):** imports `{ render }` for Shape E inspection output.
46
- - **Phase 102:** owns the actual rendering implementation; the import surface is byte-stable.
91
+ - **lib/render/render.cjs (v1 shim):** the legacy 4-arg positional signature surface. Reuses v2 muscle by construction.
92
+ - **lib/hmi/selector-dispatcher.cjs:** the Shape F / Shape E agent-surface dispatcher; the /mos:* command output side. This is the canonical gate-output consumer.
93
+
94
+ Any consumer outside this set is a violation of the Sub-plan E disposition. The CI gate at `lib/memory/render-v2-disposition.test.cjs` catches drift; the operator-facing audit at `scripts/disposition-render-v2.cjs` answers the question at any moment.
47
95
 
48
96
  ## Constraints
49
97
 
50
98
  - Zero new runtime dependencies (Phase 87 invariant).
51
99
  - CJS only (Phase 87 invariant).
52
- - `render()` import surface MUST remain byte-stable across the Phase 102 swap.
100
+ - `render()` import surface MUST remain byte-stable across the Phase 102 swap (RENDER-102-06 fence).
53
101
  - Operator vocabulary frozen at 5 canonical values; any 6th operator requires a Gate 1 review per Phase 99 CONTEXT.md D-03.
102
+ - Disposition: agent-surface-only. Adding a new consumer requires updating the allowlist in `scripts/disposition-render-v2.cjs` AND this ROOM.md in the same commit -- the CI gate enforces parity.
54
103
 
55
104
  ## See also
56
105
 
57
- - `.planning/phases/99-conversation-operator-state-machine/99-CONTEXT.md` -- canonical context (operator taxonomy, transition table, renderer signature contract D-16, graceful degradation D-17).
58
- - `.planning/phases/99-conversation-operator-state-machine/99-03-PLAN.md` -- this plan.
59
- - `docs/MINDRIAN-CANON.md` -- North Star, Part 3 (Tri-Context Decision Gate), Part 4 (Every Choice Is Graph Data), Part 7 (Reuse Before Build).
106
+ - `.planning/phases/99-conversation-operator-state-machine/99-CONTEXT.md` -- operator taxonomy, transition table, renderer signature contract D-16, graceful degradation D-17.
107
+ - `.planning/phases/102-context-aware-rendering/102-CONTEXT.md` -- the Phase 102 implementation spec.
108
+ - `.planning/phases/102-context-aware-rendering/102-VALIDATION.md` -- the Phase 102 validation strategy (Sub-plan E flips this out of draft).
109
+ - `.planning/phases/102-context-aware-rendering/102-VERIFICATION.md` -- the Phase 102 verification record (shipped by Sub-plan E).
110
+ - `.planning/phases/121.5-terminal-coherence-capstone/121.5-CONTEXT.md` -- Sub-plan E disposition rationale.
111
+ - `docs/MINDRIAN-CANON.md` -- North Star, Part 3 (Tri-Context Decision Gate), Part 4 (Every Choice Is Graph Data), Part 7 (Reuse Before Build), Part 8 (Graph Boundary).