@mindrian_os/install 1.13.0-beta.17 → 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 (182) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +26 -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 +2 -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 +2 -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 +2 -0
  51. package/commands/mva-option.md +2 -0
  52. package/commands/new-project.md +2 -0
  53. package/commands/onboard.md +20 -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 +22 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/core/breakthrough/canary.cjs +134 -0
  96. package/lib/core/breakthrough/canary.test.cjs +136 -0
  97. package/lib/core/breakthrough/detectors.cjs +359 -0
  98. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  99. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  100. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  101. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  102. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  103. package/lib/core/breakthrough/review-queue.cjs +154 -0
  104. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  105. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  106. package/lib/core/breakthrough/scanner.cjs +426 -0
  107. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  108. package/lib/core/breakthrough/schema.cjs +164 -0
  109. package/lib/core/breakthrough/schema.test.cjs +256 -0
  110. package/lib/core/breakthrough/scoring.cjs +293 -0
  111. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  112. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  113. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  114. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  115. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  116. package/lib/core/first-touch-version-stamper.cjs +113 -0
  117. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  118. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  119. package/lib/core/llm-name-suggester.cjs +194 -0
  120. package/lib/core/llm-name-suggester.test.cjs +132 -0
  121. package/lib/core/mva-orchestrator.cjs +41 -0
  122. package/lib/core/mva-telemetry.cjs +31 -143
  123. package/lib/core/navigation/edges.cjs +35 -0
  124. package/lib/core/navigation/memory-events.cjs +126 -0
  125. package/lib/core/room-auto-create.cjs +318 -0
  126. package/lib/core/room-auto-create.test.cjs +198 -0
  127. package/lib/core/room-discard-cascade.cjs +225 -0
  128. package/lib/core/room-discard-cascade.test.cjs +135 -0
  129. package/lib/core/room-name-validator.cjs +132 -0
  130. package/lib/core/room-name-validator.test.cjs +156 -0
  131. package/lib/core/room-naming-selector.cjs +357 -0
  132. package/lib/core/room-naming-selector.test.cjs +277 -0
  133. package/lib/core/room-receipt-emit.cjs +63 -0
  134. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  135. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  136. package/lib/core/stale-copy-scanner.cjs +190 -0
  137. package/lib/core/state-aware-router.cjs +78 -0
  138. package/lib/core/telemetry/schema.cjs +168 -0
  139. package/lib/core/telemetry/schema.test.cjs +124 -0
  140. package/lib/core/telemetry/validator.cjs +197 -0
  141. package/lib/core/telemetry/validator.test.cjs +188 -0
  142. package/lib/core/telemetry/writer.cjs +141 -0
  143. package/lib/core/telemetry/writer.test.cjs +331 -0
  144. package/lib/core/terminal-capability.cjs +88 -0
  145. package/lib/core/venture-shape-nudge.cjs +163 -0
  146. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  147. package/lib/core/visual-ops.cjs +70 -2
  148. package/lib/hmi/selector-dispatcher.cjs +90 -1
  149. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  150. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  151. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  152. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  153. package/lib/memory/first-touch-version.test.cjs +198 -0
  154. package/lib/memory/help-coverage.test.cjs +108 -0
  155. package/lib/memory/help-renderer.test.cjs +145 -0
  156. package/lib/memory/palette-consistency.test.cjs +127 -0
  157. package/lib/memory/pending-tension-store.cjs +80 -0
  158. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  159. package/lib/memory/run-feynman-tests.cjs +213 -0
  160. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  161. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  162. package/lib/memory/soft-alias.test.cjs +144 -0
  163. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  164. package/lib/memory/state-aware-router.test.cjs +90 -0
  165. package/lib/memory/statusline-two-row.test.cjs +338 -0
  166. package/lib/memory/terminal-capability.test.cjs +155 -0
  167. package/lib/render/ROOM.md +74 -22
  168. package/lib/sessionstart/budget-compressor.cjs +130 -0
  169. package/lib/sessionstart/contributor-interface.cjs +134 -0
  170. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  171. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  172. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  173. package/lib/statusline/two-row-renderer.cjs +186 -0
  174. package/lib/statusline/version-resolver.cjs +81 -0
  175. package/package.json +1 -1
  176. package/references/visual/ROOM.md +55 -0
  177. package/references/visual/palette.json +54 -0
  178. package/skills/larry-personality/SKILL.md +34 -0
  179. package/skills/ui-system/SKILL.md +109 -1
  180. package/skills/ui-system/rules/dual-palette.md +156 -0
  181. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  182. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Phase 119-00 Wave 1 -- venture-shape-nudge module tests.
5
+ *
6
+ * Mirrors the lib/memory/selector-decisions.test.cjs fixture pattern
7
+ * (fs.mkdtempSync + openRoomDb). Tests are allowed to require room-db.cjs
8
+ * directly per the Phase 109-06 pre-commit chokepoint allow-list for tests/.
9
+ *
10
+ * Covers the 7 behavior axes from 119-00-PLAN.md Task 1:
11
+ * Test 1 -- cold-start floor (Test 5 in plan: no room.db -> surface false)
12
+ * Test 2 -- venture-shape accumulation to threshold (Test 6)
13
+ * Test 3 -- D-01 invariant: auto_explore_fired short-circuits (Test 7)
14
+ * Test 4 -- venture_classification_unavailable safe-default (Test 7b)
15
+ * Test 5 -- venture_classified present happy-path (Test 7c)
16
+ * Test 6 -- no Brain client require (Test 8; source-grep audit)
17
+ * Test 7 -- no-room-dir guard
18
+ */
19
+
20
+ const { test } = require('node:test');
21
+ const { strict: assert } = require('node:assert');
22
+ const fs = require('node:fs');
23
+ const os = require('node:os');
24
+ const path = require('node:path');
25
+
26
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
27
+ const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
28
+ const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
29
+ const ventureShapeNudge = require(path.join(REPO_ROOT, 'lib', 'core', 'venture-shape-nudge.cjs'));
30
+
31
+ function freshRoom() {
32
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p119-00-nudge-'));
33
+ fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
34
+ const db = openRoomDb(dir);
35
+ return { dir, db };
36
+ }
37
+
38
+ function closeQuiet(db) {
39
+ try { db.close(); } catch (_e) { /* graceful */ }
40
+ }
41
+
42
+ test('Test 1: cold-start floor -- no events in db returns surface:false with venture_classification_unavailable', () => {
43
+ const { dir, db } = freshRoom();
44
+ closeQuiet(db);
45
+ const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
46
+ assert.equal(r.surface, false);
47
+ assert.equal(r.turn_count, 0);
48
+ assert.equal(r.threshold, 3);
49
+ assert.equal(r.skip_reason, 'venture_classification_unavailable');
50
+ });
51
+
52
+ test('Test 2: 3 venture_classified=true events reach threshold (surface=true)', () => {
53
+ const { dir, db } = freshRoom();
54
+ for (let i = 0; i < 3; i++) {
55
+ const result = navigation.logMemoryEvent(db, 'f_selector_decision', {
56
+ venture_classified: true,
57
+ classification_source: 'haiku-4-5',
58
+ source_path: 'test:venture-shape-nudge',
59
+ created_by: 'system',
60
+ });
61
+ assert.equal(result.ok, true, 'logMemoryEvent should accept f_selector_decision; got reason=' + (result.reason || ''));
62
+ }
63
+ closeQuiet(db);
64
+ const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
65
+ assert.equal(r.surface, true);
66
+ assert.equal(r.turn_count, 3);
67
+ assert.equal(r.threshold, 3);
68
+ assert.equal(r.skip_reason, undefined);
69
+ });
70
+
71
+ test('Test 3: D-01 invariant -- auto_explore_fired short-circuits regardless of turn count', () => {
72
+ const { dir, db } = freshRoom();
73
+ // Seed 5 venture-shaped turns AND one auto_explore_fired event.
74
+ for (let i = 0; i < 5; i++) {
75
+ navigation.logMemoryEvent(db, 'f_selector_decision', {
76
+ venture_classified: true,
77
+ classification_source: 'haiku-4-5',
78
+ source_path: 'test:venture-shape-nudge',
79
+ });
80
+ }
81
+ navigation.logMemoryEvent(db, 'auto_explore_fired', {
82
+ material_id: 'abcd1234',
83
+ relative_file_path: 'docs/sample.md',
84
+ room_slug: 'test-room',
85
+ tier: 1,
86
+ surfacing_count: 0,
87
+ brain_baseline_present: false,
88
+ source_path: 'test:venture-shape-nudge',
89
+ });
90
+ closeQuiet(db);
91
+ const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
92
+ assert.equal(r.surface, false);
93
+ assert.equal(r.skip_reason, 'upload_path_active');
94
+ });
95
+
96
+ test('Test 4: venture_classification_unavailable safe-default when field absent', () => {
97
+ const { dir, db } = freshRoom();
98
+ // Seed events without venture_classified field at all.
99
+ for (let i = 0; i < 5; i++) {
100
+ navigation.logMemoryEvent(db, 'f_selector_decision', {
101
+ command: 'mos:test',
102
+ framework: 'test-framework',
103
+ source_path: 'test:venture-shape-nudge',
104
+ });
105
+ }
106
+ closeQuiet(db);
107
+ const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
108
+ assert.equal(r.surface, false);
109
+ assert.equal(r.skip_reason, 'venture_classification_unavailable');
110
+ assert.equal(r.turn_count, 0);
111
+ });
112
+
113
+ test('Test 5: mixed venture_classified true/false -- only true counts toward threshold', () => {
114
+ const { dir, db } = freshRoom();
115
+ // 3 true + 1 false; threshold=3 met.
116
+ for (let i = 0; i < 3; i++) {
117
+ navigation.logMemoryEvent(db, 'f_selector_decision', {
118
+ venture_classified: true,
119
+ classification_source: 'haiku-4-5',
120
+ source_path: 'test:venture-shape-nudge',
121
+ });
122
+ }
123
+ navigation.logMemoryEvent(db, 'f_selector_decision', {
124
+ venture_classified: false,
125
+ classification_source: 'haiku-4-5',
126
+ source_path: 'test:venture-shape-nudge',
127
+ });
128
+ closeQuiet(db);
129
+ const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
130
+ assert.equal(r.surface, true);
131
+ assert.equal(r.turn_count, 3);
132
+ });
133
+
134
+ test('Test 6: no Brain client require -- source-grep audit', () => {
135
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'venture-shape-nudge.cjs'), 'utf8');
136
+ // Canon Part 8: zero brain-client require, zero mcp-server-brain require,
137
+ // zero fetch to brain.mindrian.
138
+ assert.equal(/require\([^)]*brain-client/.test(src), false, 'brain-client require detected');
139
+ assert.equal(/require\([^)]*mcp-server-brain/.test(src), false, 'mcp-server-brain require detected');
140
+ assert.equal(/brain\.mindrian/.test(src), false, 'brain.mindrian fetch reference detected');
141
+ // Canon Part 9: zero direct room-db.cjs require.
142
+ assert.equal(/require\([^)]*room-db\.cjs/.test(src), false, 'direct room-db.cjs require detected');
143
+ });
144
+
145
+ test('Test 7: no-room-dir guard returns safe default', () => {
146
+ const r = ventureShapeNudge.shouldSurfaceNudge('', {});
147
+ assert.equal(r.surface, false);
148
+ assert.equal(r.skip_reason, 'no_room_dir');
149
+
150
+ const r2 = ventureShapeNudge.shouldSurfaceNudge(null, {});
151
+ assert.equal(r2.surface, false);
152
+
153
+ // Non-existent path -> no_room_db.
154
+ const r3 = ventureShapeNudge.shouldSurfaceNudge('/tmp/p119-nonexistent-' + Date.now(), {});
155
+ assert.equal(r3.surface, false);
156
+ assert.equal(r3.skip_reason, 'no_room_db');
157
+ });
158
+
159
+ test('Test 8: VENTURE_NUDGE_THRESHOLD constant exported and equals 3', () => {
160
+ assert.equal(ventureShapeNudge.VENTURE_NUDGE_THRESHOLD, 3);
161
+ });
@@ -173,8 +173,19 @@ function formatSectionHeader(sectionName, entryCount, stage) {
173
173
  // MERMAID DIAGRAM GENERATORS
174
174
  // ═══════════════════════════════════════════════════════════════
175
175
 
176
- // De Stijl hex colors for Mermaid style directives (no ANSI in diagrams)
177
- const DS_HEX = {
176
+ // De Stijl hex colors for Mermaid style directives (no ANSI in diagrams).
177
+ //
178
+ // Phase 121.5-03: DS_HEX is now SOURCED FROM references/visual/palette.json
179
+ // (the canonical De Stijl source-of-truth per ODD 3 resolution + Canon Part 7).
180
+ // The literal fallback below is graceful degradation only -- if palette.json
181
+ // fails to load (unlikely in a properly-installed plugin), the shipping
182
+ // values are frozen here verbatim from the pre-121.5 hardcoded DS_HEX so
183
+ // downstream Mermaid + dashboard rendering keeps working.
184
+ //
185
+ // Test seam: MINDRIAN_PALETTE_PATH env var overrides the default palette
186
+ // path. Used by lib/memory/statusline-two-row.test.cjs (test 11) to verify
187
+ // the wiring without touching the live palette.json.
188
+ const FALLBACK_DS_HEX = {
178
189
  red: '#A63D2F',
179
190
  blue: '#1E3A6E',
180
191
  yellow: '#C8A43C',
@@ -186,6 +197,63 @@ const DS_HEX = {
186
197
  bg: '#0D0D0D'
187
198
  };
188
199
 
200
+ /**
201
+ * Compose DS_HEX from references/visual/palette.json. Preserves the legacy
202
+ * key names (red / blue / yellow / green / sienna / amethyst / cream / muted
203
+ * / bg) so existing consumers (Mermaid generators, dashboard exporters) keep
204
+ * working byte-identical. Also exposes the palette tier keys via dot
205
+ * extension for new consumers (e.g. mondrian_red, muted_green, saturated_blue).
206
+ */
207
+ function loadPaletteDsHex() {
208
+ const path = require('path');
209
+ const fs = require('fs');
210
+ const candidate = process.env.MINDRIAN_PALETTE_PATH ||
211
+ path.join(__dirname, '..', '..', 'references', 'visual', 'palette.json');
212
+ try {
213
+ const palette = JSON.parse(fs.readFileSync(candidate, 'utf8'));
214
+ const base = palette.base || {};
215
+ const a = palette.palette_a_discovery || {};
216
+ const b = palette.palette_b_build || {};
217
+ const ext = palette.extended || {};
218
+ const composed = {
219
+ // Legacy DS_HEX keys (preserve backward compat for Mermaid + dashboard)
220
+ red: base.mondrian_red || FALLBACK_DS_HEX.red,
221
+ blue: base.mondrian_blue || FALLBACK_DS_HEX.blue,
222
+ yellow: base.mondrian_yellow || FALLBACK_DS_HEX.yellow,
223
+ green: base.success_green || FALLBACK_DS_HEX.green,
224
+ sienna: ext.sienna || FALLBACK_DS_HEX.sienna,
225
+ amethyst: base.amethyst || FALLBACK_DS_HEX.amethyst,
226
+ cream: base.cream || FALLBACK_DS_HEX.cream,
227
+ muted: base.gray_meta || FALLBACK_DS_HEX.muted,
228
+ bg: ext.bg || base.mondrian_black || FALLBACK_DS_HEX.bg,
229
+ // Tier-keyed mirrors (new consumers can use the explicit names)
230
+ mondrian_red: base.mondrian_red,
231
+ mondrian_blue: base.mondrian_blue,
232
+ mondrian_yellow: base.mondrian_yellow,
233
+ mondrian_black: base.mondrian_black,
234
+ mondrian_white: base.mondrian_white,
235
+ gray_meta: base.gray_meta,
236
+ success_green: base.success_green,
237
+ muted_red: a.muted_red,
238
+ muted_green: a.muted_green,
239
+ teal_accent: a.teal_accent,
240
+ saturated_red: b.saturated_red,
241
+ saturated_blue: b.saturated_blue,
242
+ saturated_yellow: b.saturated_yellow,
243
+ };
244
+ return composed;
245
+ } catch (e) {
246
+ // Graceful degradation per Canon Part 7. One warning to stderr, then
247
+ // fall back to the frozen literal.
248
+ if (process.env.MINDRIAN_PALETTE_DEBUG) {
249
+ try { process.stderr.write('[visual-ops] palette.json load failed: ' + e.message + '\n'); } catch (_) {}
250
+ }
251
+ return Object.assign({}, FALLBACK_DS_HEX);
252
+ }
253
+ }
254
+
255
+ const DS_HEX = loadPaletteDsHex();
256
+
189
257
  // Map stage names to hex colors for Mermaid
190
258
  const STAGE_HEX = {
191
259
  preopportunity: DS_HEX.muted,
@@ -70,6 +70,14 @@
70
70
 
71
71
  'use strict';
72
72
 
73
+ // Phase 121-02 D-04: unified-stream selector_pick emit chokepoint.
74
+ // require()-loaded lazily inside emitSelectorPickUnified() so a missing writer
75
+ // module (or a stripped-down install) cannot crash the dispatcher at load.
76
+ const crypto = require('node:crypto');
77
+ function _sha256(s) {
78
+ return crypto.createHash('sha256').update(String(s || '')).digest('hex');
79
+ }
80
+
73
81
  const FREE_TEXT = 'Free-Text';
74
82
  const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.';
75
83
 
@@ -79,7 +87,12 @@ const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.
79
87
  // collision-safe lib/hmi/shape-f6-plan-review-renderer.cjs path; the umbrella 'F'
80
88
  // branch continues to route to Phase 101-01's lib/hmi/shape-f6-renderer.cjs via
81
89
  // JTBD logic preserved byte-stable per R1 invariant).
82
- const F_SUBSHAPES = ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6'];
90
+ // Phase 120-01: 'F.7' Breakthrough Surface appended (CONTEXT.md D-07..D-10 + D-20). Closed-vocab
91
+ // 5 verbs verbatim [Explore deeper]/[Confirm]/[File as decision]/[Dismiss]/[Back]; mandatory
92
+ // [Dismiss] guard (D-10). Routes to lib/hmi/shape-f7-breakthrough-renderer.cjs via the
93
+ // explicit requestedShape:'F.7' branch in dispatchShapeFSubShape. The umbrella 'F' branch
94
+ // does NOT resolve to F.7 -- breakthrough surfacing is opt-in via explicit dispatch.
95
+ const F_SUBSHAPES = ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6', 'F.7'];
83
96
  const F_UMBRELLA = 'F';
84
97
  const JUST_TALK = 'JUST_TALK';
85
98
  const COMPACTION_VIOLATION_CODE = 'render_v2_compaction_violation';
@@ -175,6 +188,58 @@ function justTalkRefuse(requestedShape) {
175
188
  };
176
189
  }
177
190
 
191
+ /**
192
+ * Phase 121-02 D-04: emit selector_pick event into the unified
193
+ * ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl stream (Plan 121-00
194
+ * writer chokepoint). Wrapped in try/catch -- telemetry NEVER fails Larry's
195
+ * turn. Canon Part 8: room slug is sha256-hashed; payload fields are scalar
196
+ * enums / numbers / booleans only. The emit is gated by validateEventPayload
197
+ * inside writer.emit() so adversarial fixtures (Cypher in verb_chosen, etc.)
198
+ * cannot leak. Non-blocking: any thrown error is swallowed so the pickShape
199
+ * dispatch resolution still returns to the caller.
200
+ */
201
+ function emitSelectorPickUnified(roomDir, subShape, rendered, payloadIn) {
202
+ try {
203
+ let writer;
204
+ try {
205
+ writer = require('../core/telemetry/writer.cjs');
206
+ } catch (_e) {
207
+ return; // missing writer module -- soft skip, do not crash dispatch
208
+ }
209
+ if (!writer || typeof writer.emit !== 'function') return;
210
+ const contract = (rendered && rendered.contract) ? rendered.contract : {};
211
+ const verbs = Array.isArray(contract.verbs) ? contract.verbs : [];
212
+ const mode = (typeof contract.mode === 'string') ? contract.mode : 'A';
213
+ const recommended = contract.recommended;
214
+ const payload = (payloadIn && typeof payloadIn === 'object') ? payloadIn : {};
215
+ const rankerConfidence = (typeof payload.rankerConfidence === 'number')
216
+ ? payload.rankerConfidence
217
+ : 0;
218
+ const recommendedRendered = (recommended !== null && recommended !== undefined)
219
+ ? true
220
+ : Boolean(payload.recommendedRendered);
221
+ const verbChosen = (typeof payload.verb_chosen === 'string' && payload.verb_chosen.length > 0)
222
+ ? payload.verb_chosen
223
+ : (typeof contract.recommended === 'string' ? contract.recommended : '');
224
+ const slugSource = (typeof roomDir === 'string' && roomDir.length > 0)
225
+ ? String(roomDir).split('/').filter(Boolean).pop() || roomDir
226
+ : (String(process.cwd()).split('/').filter(Boolean).pop() || 'default-room');
227
+ writer.emit('selector_pick', {
228
+ sub_shape: String(subShape || '').slice(0, 64),
229
+ mode: String(mode || 'A').slice(0, 64),
230
+ ranker_confidence: rankerConfidence,
231
+ recommended_rendered: Boolean(recommendedRendered),
232
+ options_count: Number.isInteger(verbs.length) ? verbs.length : 0,
233
+ room_slug_sha256: _sha256(slugSource),
234
+ verb_chosen: String(verbChosen || '').slice(0, 64),
235
+ });
236
+ } catch (_e) {
237
+ // Canon Part 8 validation failure or any other emit failure: swallow.
238
+ // The dispatch resolution must still return cleanly per non-blocking
239
+ // contract (selector-pick-capture test 3 verifies this).
240
+ }
241
+ }
242
+
178
243
  /**
179
244
  * Phase 88.2-04: emit selector presentation telemetry. Wrapped in try/catch --
180
245
  * telemetry NEVER fails Larry's turn. Canon Part 8: LOCAL ledger only, room
@@ -354,6 +419,25 @@ function dispatchShapeFSubShape(requestedShape, args) {
354
419
  personaContext: (typeof payloadObj.personaContext === 'string' && payloadObj.personaContext.length > 0)
355
420
  ? payloadObj.personaContext : undefined,
356
421
  };
422
+ } else if (requestedShape === 'F.7') {
423
+ // Phase 120-01: F.7 Breakthrough Surface (closed-vocab; 5 verbs verbatim).
424
+ // Per CONTEXT.md D-07: built NEW (not a reuse of F.4).
425
+ // Per CONTEXT.md D-08: 5 verbs locked verbatim [Explore deeper]/[Confirm]/
426
+ // [File as decision]/[Dismiss]/[Back].
427
+ // Per CONTEXT.md D-10: [Dismiss] is MANDATORY (renderer asserts presence on
428
+ // every render).
429
+ // Per CONTEXT.md D-20: payload.breakthrough carries provenance.artifact_ids[] --
430
+ // F.7 renderer refuses to render if artifact_ids.length === 0 (HARD FLOOR
431
+ // structural enforcement at the surface layer; defense in depth on top of
432
+ // writeBreakthrough's validateProvenance in lib/core/breakthrough/schema.cjs).
433
+ mod = safeRequire('./shape-f7-breakthrough-renderer.cjs');
434
+ fnName = 'renderShapeF7Breakthrough';
435
+ inputArgs = {
436
+ tier: tier,
437
+ header: callerHeader,
438
+ breakthrough: (payloadObj.breakthrough && typeof payloadObj.breakthrough === 'object') ? payloadObj.breakthrough : null,
439
+ more_count: (Number.isInteger(payloadObj.more_count) && payloadObj.more_count >= 0) ? payloadObj.more_count : 0,
440
+ };
357
441
  }
358
442
 
359
443
  if (!mod || typeof mod[fnName] !== 'function') {
@@ -496,6 +580,11 @@ function pickShape(args) {
496
580
  const payloadObj = (opts.payload && typeof opts.payload === 'object') ? opts.payload : {};
497
581
  if (payloadObj.emitTelemetry === true) {
498
582
  emitPresentationTelemetry(opts.roomDir, result.shape, result.rendered, operator);
583
+ // Phase 121-02 D-04: capture selector pick (88.2/125 ranker) into
584
+ // the unified ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl
585
+ // stream (Plan 121-00 writer chokepoint). Non-blocking; never
586
+ // throws back to caller.
587
+ emitSelectorPickUnified(opts.roomDir, result.shape, result.rendered, payloadObj);
499
588
  }
500
589
  }
501
590
  }
@@ -0,0 +1,222 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 120-01 -- Shape F.7 Breakthrough Surface renderer.
5
+ *
6
+ * Per CONTEXT.md D-07..D-10 (the 5 verbs LOCKED VERBATIM + closed-vocab + mandatory dismiss):
7
+ * - Built NEW (D-07): F.4 maps content-to-user-actionable-observation;
8
+ * F.7 maps user-actions-to-recognition-of-user-own-work. Different speech acts.
9
+ * - 5 verbs LOCKED VERBATIM (D-08): [Explore deeper] / [Confirm] / [File as decision] /
10
+ * [Dismiss] / [Back]. Order is structural, not stylistic.
11
+ * - [File as decision] (D-09) emits breakthrough_filed_as_decision via the Plan 120-02
12
+ * scanner consumer; F.7 only renders the verb -- it does NOT itself emit events.
13
+ * - [Dismiss] (D-10) is MANDATORY -- the renderer asserts its presence; refuses to
14
+ * render a contract without it (defense in depth against accidental drift).
15
+ *
16
+ * Per CONTEXT.md D-11..D-12 (top-1 with "More breakthroughs (N)" affordance):
17
+ * - Top-1 surface; "More breakthroughs (N)" affordance for queued candidates.
18
+ * - more_count is rendered in zones.footer IFF more_count > 0.
19
+ *
20
+ * Per CONTEXT.md D-20 HARD FLOOR (Cypher-provable):
21
+ * - breakthrough.artifact_ids[] must be non-empty AT RENDER TIME. The renderer is
22
+ * the second structural enforcement point (the first is writeBreakthrough in
23
+ * Plan 120-00 schema.cjs). Defense in depth: if a caller somehow bypasses
24
+ * writeBreakthrough's validateProvenance, F.7 refuses to render the surfacing.
25
+ *
26
+ * Per CONTEXT.md D-17 voice scaffold rule 3 (time anchor):
27
+ * - The title line includes a human-readable time anchor (e.g., "in the last 8 hours")
28
+ * derived from breakthrough.detected_at via formatTimeAnchor.
29
+ *
30
+ * Canon Part 3: F.7 IS a Decision Gate primitive instance. LOCAL context only
31
+ * (the breakthrough artifact_ids + theme); BRAIN context is none (D-20 makes
32
+ * breakthroughs locally Cypher-provable); SIGNAL context is none (cross-room
33
+ * forbidden per Part 8).
34
+ *
35
+ * Canon Part 4: every choice is graph data. Each of the 5 verbs maps to a typed
36
+ * event the Plan 120-02 scanner consumer emits at verb-dispatch time.
37
+ *
38
+ * Canon Part 8: zero Brain coupling. The renderer is pure: no FS reads, no DB
39
+ * writes, no fetch calls. Mirrors the F.4 / F.6 pattern byte-for-byte.
40
+ *
41
+ * Pure CJS, node built-ins only, zero deps (Phase 87 invariant).
42
+ */
43
+ 'use strict';
44
+
45
+ // D-08: the 5 verbs LOCKED VERBATIM. Object.freeze prevents accidental mutation
46
+ // of the canonical array; downstream code that needs a mutable copy must call
47
+ // F7_VERBS.slice() (the dispatcher does this in inputArgs).
48
+ const F7_VERBS = Object.freeze([
49
+ 'Explore deeper',
50
+ 'Confirm',
51
+ 'File as decision',
52
+ 'Dismiss',
53
+ 'Back',
54
+ ]);
55
+
56
+ // D-10: structural index of 'Dismiss' in F7_VERBS. assertContractInvariants uses
57
+ // this to surface a precise 'dismiss_required' reason when the contract drifts.
58
+ const F7_DISMISS_INDEX = 3;
59
+
60
+ // Map the four detector kinds Plan 120-00 emits to their canonical display
61
+ // strings. Unknown kinds fall back to the raw kind string (and finally to
62
+ // 'Pattern' if kind is falsy).
63
+ const KIND_DISPLAY_NAMES = Object.freeze({
64
+ convergence: 'Convergence',
65
+ contradiction_resolved: 'Contradiction resolved',
66
+ cross_domain_analogy: 'Cross-domain analogy',
67
+ reverse_salient_closed: 'Reverse salient closed',
68
+ });
69
+
70
+ // D-17 rule 3: human-readable time anchor with bucket boundaries chosen to
71
+ // match the spec examples (in the last hour / in the last N hours / today /
72
+ // in the last day / this week / in the last two weeks / on YYYY-MM-DD).
73
+ // D-06 ethical fence: ages > 14 days surface the calendar date verbatim so
74
+ // the user can see exactly how stale the recognition is.
75
+ function formatTimeAnchor(detected_at) {
76
+ const now = Date.now();
77
+ const ts = (typeof detected_at === 'number' && Number.isFinite(detected_at)) ? detected_at : now;
78
+ const ageMs = Math.max(0, now - ts);
79
+ const ageHours = ageMs / (3600 * 1000);
80
+ const ageDays = ageHours / 24;
81
+ if (ageHours < 1) return 'in the last hour';
82
+ if (ageHours < 8) return 'in the last ' + Math.floor(ageHours) + ' hours';
83
+ if (ageHours < 24) return 'today';
84
+ if (ageDays < 2) return 'in the last day';
85
+ if (ageDays < 7) return 'this week';
86
+ if (ageDays < 14) return 'in the last two weeks';
87
+ // Older than the 14-day ethical fence (D-06) -- include calendar date for transparency.
88
+ const d = new Date(ts);
89
+ const yyyy = d.getUTCFullYear();
90
+ const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
91
+ const dd = String(d.getUTCDate()).padStart(2, '0');
92
+ return 'on ' + yyyy + '-' + mm + '-' + dd;
93
+ }
94
+
95
+ // D-10 MANDATORY-Dismiss structural guard. Returns {ok:true} when the verbs
96
+ // array deep-equals F7_VERBS; otherwise returns {ok:false, reason} with a
97
+ // precise reason code so consumers can surface a meaningful error envelope.
98
+ //
99
+ // Reason codes:
100
+ // verb_count_mismatch -- verbs.length !== 5
101
+ // dismiss_required -- the slot at F7_DISMISS_INDEX is not literally 'Dismiss'
102
+ // verb_order_mismatch -- some other slot diverges from the canonical array
103
+ function assertContractInvariants(verbs) {
104
+ if (!Array.isArray(verbs) || verbs.length !== F7_VERBS.length) {
105
+ return { ok: false, reason: 'verb_count_mismatch' };
106
+ }
107
+ for (let i = 0; i < F7_VERBS.length; i++) {
108
+ if (verbs[i] !== F7_VERBS[i]) {
109
+ return { ok: false, reason: i === F7_DISMISS_INDEX ? 'dismiss_required' : 'verb_order_mismatch' };
110
+ }
111
+ }
112
+ return { ok: true };
113
+ }
114
+
115
+ /**
116
+ * Render the F.7 Breakthrough Surface.
117
+ *
118
+ * @param {object} args
119
+ * @param {number} [args.tier] tier integer; informational only -- F.7 is closed-vocab
120
+ * in every tier (no recommended marker; no mode branch)
121
+ * @param {string} [args.header] caller-supplied header line; default 'Breakthrough'
122
+ * @param {object} args.breakthrough the breakthrough candidate (must carry artifact_ids[])
123
+ * @param {number} [args.more_count] non-negative integer; number of queued candidates
124
+ * surfaced via the "More breakthroughs (N)" footer
125
+ * @param {string} [args.voice_line] Phase 120-03 D-17 additive slot. When present + non-empty,
126
+ * prepended as the first line of zones.body above the title.
127
+ * Caller (Plan 120-03 scanner.surfaceBreakthrough) audits
128
+ * the line via voice-scaffold.auditVoiceLine before passing;
129
+ * this renderer does NOT re-audit (single source of truth).
130
+ *
131
+ * @returns {object} { zones: {header, body, footer}, contract: {...} } OR
132
+ * { error: 'provenance_required' | 'dismiss_required' | 'verb_count_mismatch' | 'verb_order_mismatch' }
133
+ */
134
+ function renderShapeF7Breakthrough(args) {
135
+ const a = (args && typeof args === 'object') ? args : {};
136
+ const more_count = (Number.isInteger(a.more_count) && a.more_count >= 0) ? a.more_count : 0;
137
+ const bk = a.breakthrough;
138
+
139
+ // D-20 HARD FLOOR defense in depth -- refuse to render a provenance-less
140
+ // breakthrough. The Plan 120-00 schema layer rejects writes with empty
141
+ // artifact_ids; this is the SECOND structural enforcement point at the
142
+ // surface layer (in case writeBreakthrough is somehow bypassed).
143
+ if (!bk || typeof bk !== 'object' || !Array.isArray(bk.artifact_ids) || bk.artifact_ids.length === 0) {
144
+ return { error: 'provenance_required' };
145
+ }
146
+
147
+ // Build the locked-verbatim verbs array and assert the D-10 invariants.
148
+ // Defense in depth: the source-of-truth F7_VERBS is Object.freeze-d above;
149
+ // assertContractInvariants on the mutable copy gives a final structural
150
+ // assertion before composition.
151
+ const verbs = F7_VERBS.slice();
152
+ const check = assertContractInvariants(verbs);
153
+ if (!check.ok) {
154
+ return { error: check.reason };
155
+ }
156
+
157
+ // Row 1 (title): kind capitalized + theme + time anchor.
158
+ // Theme is sliced to 120 chars at the surface layer (defense in depth on top
159
+ // of the schema-layer 200-char sanitizer in Plan 120-00).
160
+ const kindKey = (typeof bk.kind === 'string') ? bk.kind : '';
161
+ const kindDisplay = KIND_DISPLAY_NAMES[kindKey] || kindKey || 'Pattern';
162
+ const themeRaw = (typeof bk.theme === 'string') ? bk.theme : '';
163
+ const theme = themeRaw.slice(0, 120);
164
+ const timeAnchor = formatTimeAnchor(bk.detected_at || Date.now());
165
+ const titleLine = kindDisplay + (theme ? ': ' + theme : '') + ' (' + timeAnchor + ')';
166
+
167
+ // Row 2 (verbs): the 5 verbs in [X] brackets, separated by double-space.
168
+ // Bracket style matches the F.4 / F.6 closed-vocab convention.
169
+ const verbsLine = verbs.map(function (v) { return '[' + v + ']'; }).join(' ');
170
+
171
+ // Phase 120-03 D-17 additive: optional voice_line prepend.
172
+ //
173
+ // When args.voice_line is a non-empty string, it is prepended as the FIRST line
174
+ // of zones.body, above the title line. This is the conversational opener
175
+ // composed by Plan 120-03 voice-scaffold.composeBreakthroughVoiceLine. The
176
+ // caller (scanner.surfaceBreakthrough) audits the line via auditVoiceLine
177
+ // before passing; this renderer does NOT re-audit (single source of truth, no
178
+ // duplicated regex logic at the surface layer).
179
+ //
180
+ // The contract.verbs / freeTextOffered / recommended / breakthrough_id /
181
+ // more_count fields are PRESERVED byte-stably across both branches (with-line
182
+ // and without-line). Plan 120-01 11-test byte-stability invariant is preserved
183
+ // when voice_line is absent (the without-line branch is the legacy code path).
184
+ const voiceLineRaw = (typeof a.voice_line === 'string') ? a.voice_line : '';
185
+ const voiceLine = voiceLineRaw.length > 0 ? voiceLineRaw : '';
186
+ const body = voiceLine
187
+ ? (voiceLine + '\n' + titleLine + '\n' + verbsLine)
188
+ : (titleLine + '\n' + verbsLine);
189
+
190
+ const header = (typeof a.header === 'string' && a.header.length > 0) ? a.header : 'Breakthrough';
191
+ // D-11 affordance: only surface "More breakthroughs (N)" when N > 0; empty
192
+ // footer otherwise so the renderer never lies about queued candidates.
193
+ const footer = (more_count > 0) ? ('More breakthroughs (' + more_count + ')') : '';
194
+
195
+ return {
196
+ zones: {
197
+ header: header,
198
+ body: body,
199
+ signals: '',
200
+ footer: footer,
201
+ },
202
+ contract: {
203
+ shape: 'F.7',
204
+ keyboard: 'askuserquestion',
205
+ verbs: verbs,
206
+ freeTextOffered: false, // closed-vocab; mirrors F.3 / F.4 / F.6 plan-review
207
+ mode: 'closed',
208
+ recommended: null, // closed-vocab does not mark a recommended verb
209
+ breakthrough_id: bk.id,
210
+ more_count: more_count,
211
+ },
212
+ };
213
+ }
214
+
215
+ module.exports = {
216
+ renderShapeF7Breakthrough: renderShapeF7Breakthrough,
217
+ F7_VERBS: F7_VERBS,
218
+ F7_DISMISS_INDEX: F7_DISMISS_INDEX,
219
+ formatTimeAnchor: formatTimeAnchor,
220
+ assertContractInvariants: assertContractInvariants,
221
+ KIND_DISPLAY_NAMES: KIND_DISPLAY_NAMES,
222
+ };