@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.21

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 (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.mcp.json +6 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +51 -56
  5. package/bin/mindrian-brain-mcp-client.cjs +152 -0
  6. package/commands/act.md +1 -0
  7. package/commands/admin.md +1 -0
  8. package/commands/analyze-needs.md +2 -0
  9. package/commands/analyze-systems.md +2 -0
  10. package/commands/analyze-timing.md +2 -0
  11. package/commands/auto-explore.md +2 -0
  12. package/commands/beautiful-question.md +2 -0
  13. package/commands/brain-derive.md +2 -0
  14. package/commands/build-knowledge.md +2 -0
  15. package/commands/build-thesis.md +2 -0
  16. package/commands/causal.md +2 -0
  17. package/commands/challenge-assumptions.md +2 -0
  18. package/commands/compare-ventures.md +2 -0
  19. package/commands/dashboard.md +2 -1
  20. package/commands/deep-grade.md +2 -0
  21. package/commands/diagnose.md +21 -1
  22. package/commands/diagnostics.md +14 -3
  23. package/commands/doctor.md +6 -2
  24. package/commands/dogfood-flush.md +92 -0
  25. package/commands/dominant-designs.md +2 -0
  26. package/commands/explain-decision.md +2 -0
  27. package/commands/explore-domains.md +2 -0
  28. package/commands/explore-futures.md +2 -0
  29. package/commands/explore-trends.md +2 -0
  30. package/commands/export.md +1 -0
  31. package/commands/feynman-timeline-refresh.md +2 -0
  32. package/commands/file-meeting.md +2 -0
  33. package/commands/find-analogies.md +1 -0
  34. package/commands/find-bottlenecks.md +2 -0
  35. package/commands/find-connections.md +2 -0
  36. package/commands/funding.md +1 -0
  37. package/commands/grade.md +2 -0
  38. package/commands/graph.md +1 -0
  39. package/commands/hat-briefing.md +1 -0
  40. package/commands/heal.md +22 -170
  41. package/commands/help.md +54 -334
  42. package/commands/hmi-status.md +23 -144
  43. package/commands/jtbd.md +1 -0
  44. package/commands/leadership.md +2 -0
  45. package/commands/lean-canvas.md +2 -0
  46. package/commands/macro-trends.md +2 -0
  47. package/commands/map-unknowns.md +2 -0
  48. package/commands/memory.md +1 -0
  49. package/commands/models.md +1 -0
  50. package/commands/mos-reason.md +2 -0
  51. package/commands/mos.md +139 -0
  52. package/commands/mullins.md +2 -0
  53. package/commands/mva-brief.md +2 -0
  54. package/commands/mva-option.md +2 -0
  55. package/commands/new-project.md +2 -0
  56. package/commands/onboard.md +20 -7
  57. package/commands/operator.md +1 -0
  58. package/commands/opportunities.md +1 -0
  59. package/commands/organize.md +22 -469
  60. package/commands/persona.md +1 -0
  61. package/commands/pipeline.md +2 -0
  62. package/commands/present.md +1 -0
  63. package/commands/publish.md +2 -0
  64. package/commands/query.md +24 -102
  65. package/commands/radar.md +2 -0
  66. package/commands/reanalyze.md +1 -0
  67. package/commands/research.md +2 -0
  68. package/commands/room.md +2 -0
  69. package/commands/rooms.md +1 -0
  70. package/commands/root-cause.md +2 -0
  71. package/commands/rs-experts.md +1 -0
  72. package/commands/rs-explain.md +1 -0
  73. package/commands/rs-fetch.md +1 -0
  74. package/commands/rs-thesis.md +1 -0
  75. package/commands/scenario-plan.md +2 -0
  76. package/commands/scheduled-tasks.md +1 -0
  77. package/commands/score-innovation.md +2 -0
  78. package/commands/scout.md +1 -0
  79. package/commands/setup.md +2 -0
  80. package/commands/snapshot.md +2 -0
  81. package/commands/speakers.md +1 -0
  82. package/commands/splash.md +5 -2
  83. package/commands/status.md +1 -0
  84. package/commands/structure-argument.md +2 -0
  85. package/commands/suggest-next.md +2 -0
  86. package/commands/systems-thinking.md +2 -0
  87. package/commands/think-hats.md +2 -0
  88. package/commands/update.md +2 -0
  89. package/commands/user-needs.md +2 -0
  90. package/commands/validate.md +2 -0
  91. package/commands/value-proposition.md +2 -0
  92. package/commands/vault.md +2 -0
  93. package/commands/visualize.md +24 -29
  94. package/commands/whitespace.md +2 -1
  95. package/commands/wiki.md +1 -0
  96. package/hooks/hooks.json +22 -88
  97. package/lib/agents/auto-explore-agent.cjs +82 -0
  98. package/lib/core/breakthrough/canary.cjs +134 -0
  99. package/lib/core/breakthrough/canary.test.cjs +136 -0
  100. package/lib/core/breakthrough/detectors.cjs +359 -0
  101. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  102. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  103. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  104. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  105. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  106. package/lib/core/breakthrough/review-queue.cjs +154 -0
  107. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  108. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  109. package/lib/core/breakthrough/scanner.cjs +426 -0
  110. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  111. package/lib/core/breakthrough/schema.cjs +164 -0
  112. package/lib/core/breakthrough/schema.test.cjs +256 -0
  113. package/lib/core/breakthrough/scoring.cjs +293 -0
  114. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  115. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  116. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  117. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  118. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  119. package/lib/core/directive-envelope.cjs +175 -0
  120. package/lib/core/directive-envelope.test.cjs +225 -0
  121. package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
  122. package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
  123. package/lib/core/first-touch-version-stamper.cjs +113 -0
  124. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  125. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  126. package/lib/core/llm-name-suggester.cjs +194 -0
  127. package/lib/core/llm-name-suggester.test.cjs +132 -0
  128. package/lib/core/mcp-profiles.cjs +1 -1
  129. package/lib/core/migration-snapshot.cjs +172 -0
  130. package/lib/core/migration-snapshot.test.cjs +174 -0
  131. package/lib/core/mindrian-brain-shim.test.cjs +214 -0
  132. package/lib/core/mva-orchestrator.cjs +41 -0
  133. package/lib/core/mva-telemetry.cjs +31 -143
  134. package/lib/core/navigation/edges.cjs +35 -0
  135. package/lib/core/navigation/memory-events.cjs +126 -0
  136. package/lib/core/room-auto-create.cjs +318 -0
  137. package/lib/core/room-auto-create.test.cjs +198 -0
  138. package/lib/core/room-discard-cascade.cjs +225 -0
  139. package/lib/core/room-discard-cascade.test.cjs +135 -0
  140. package/lib/core/room-name-validator.cjs +132 -0
  141. package/lib/core/room-name-validator.test.cjs +156 -0
  142. package/lib/core/room-naming-selector.cjs +357 -0
  143. package/lib/core/room-naming-selector.test.cjs +277 -0
  144. package/lib/core/room-receipt-emit.cjs +63 -0
  145. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  146. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  147. package/lib/core/rs-nl-to-query.cjs +1 -1
  148. package/lib/core/stale-copy-scanner.cjs +190 -0
  149. package/lib/core/state-aware-router.cjs +78 -0
  150. package/lib/core/telemetry/schema.cjs +168 -0
  151. package/lib/core/telemetry/schema.test.cjs +124 -0
  152. package/lib/core/telemetry/validator.cjs +200 -0
  153. package/lib/core/telemetry/validator.test.cjs +188 -0
  154. package/lib/core/telemetry/writer.cjs +141 -0
  155. package/lib/core/telemetry/writer.test.cjs +331 -0
  156. package/lib/core/terminal-capability.cjs +88 -0
  157. package/lib/core/tier0-messaging.cjs +109 -0
  158. package/lib/core/tier0-messaging.test.cjs +218 -0
  159. package/lib/core/venture-shape-nudge.cjs +163 -0
  160. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  161. package/lib/core/visual-ops.cjs +70 -2
  162. package/lib/hmi/selector-dispatcher.cjs +90 -1
  163. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  164. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  165. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  166. package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
  167. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  168. package/lib/memory/first-touch-version.test.cjs +198 -0
  169. package/lib/memory/help-coverage.test.cjs +108 -0
  170. package/lib/memory/help-renderer.test.cjs +145 -0
  171. package/lib/memory/mos-status-renderer.test.cjs +2 -2
  172. package/lib/memory/navigation-engine-core.test.cjs +1 -1
  173. package/lib/memory/palette-consistency.test.cjs +127 -0
  174. package/lib/memory/pending-tension-store.cjs +80 -0
  175. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  176. package/lib/memory/run-feynman-tests.cjs +223 -0
  177. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  178. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  179. package/lib/memory/soft-alias.test.cjs +144 -0
  180. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  181. package/lib/memory/state-aware-router.test.cjs +90 -0
  182. package/lib/memory/statusline-two-row.test.cjs +338 -0
  183. package/lib/memory/terminal-capability.test.cjs +155 -0
  184. package/lib/render/ROOM.md +74 -22
  185. package/lib/sessionstart/budget-compressor.cjs +130 -0
  186. package/lib/sessionstart/contributor-interface.cjs +134 -0
  187. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  188. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  189. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  190. package/lib/statusline/two-row-renderer.cjs +186 -0
  191. package/lib/statusline/version-resolver.cjs +81 -0
  192. package/package.json +1 -1
  193. package/references/visual/ROOM.md +55 -0
  194. package/references/visual/palette.json +54 -0
  195. package/skills/larry-personality/SKILL.md +34 -0
  196. package/skills/ui-system/SKILL.md +109 -1
  197. package/skills/ui-system/rules/dual-palette.md +156 -0
  198. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  199. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,108 @@
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 2 -- help-coverage CI guard tests.
8
+ *
9
+ * Test map (4 cases, PLAN Task 2 <behavior> Tests 10-13):
10
+ *
11
+ * 10. scripts/check-help-coverage.cjs exits 0 against live repo.
12
+ * 11. Synthetic fixture: temp command without help_jtbd -> exit 1.
13
+ * 12. Synthetic fixture: command file exists but NOT in help-groups.json
14
+ * (and not admin) -> exit 1.
15
+ * 13. Infrastructure group contains "doctor" (closes Cluster 5 audit
16
+ * finding -- doctor was previously silently absent from help.md).
17
+ *
18
+ * Canon Part 3 (UI Ruling System), Part 7 (Reuse Before Build), Part 8
19
+ * (Graph Boundary).
20
+ */
21
+
22
+ const assert = require('node:assert/strict');
23
+ const fs = require('node:fs');
24
+ const os = require('node:os');
25
+ const path = require('node:path');
26
+ const { spawnSync } = require('node:child_process');
27
+
28
+ const REPO = path.resolve(__dirname, '..', '..');
29
+ const CHECKER = path.join(REPO, 'scripts', 'check-help-coverage.cjs');
30
+ const GROUPS_PATH = path.join(REPO, 'data', 'help-groups.json');
31
+ const COMMANDS_DIR = path.join(REPO, 'commands');
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+ const failures = [];
36
+
37
+ function run(name, fn) {
38
+ try {
39
+ fn();
40
+ console.log(' PASS ' + name);
41
+ passed++;
42
+ } catch (err) {
43
+ console.error(' FAIL ' + name);
44
+ console.error(' ' + (err.message || err));
45
+ failed++;
46
+ failures.push(name);
47
+ }
48
+ }
49
+
50
+ // --- Test 10: live repo passes ---
51
+ run('Test 10: check-help-coverage.cjs exits 0 against live repo', () => {
52
+ assert.ok(fs.existsSync(CHECKER), 'scripts/check-help-coverage.cjs must exist');
53
+ const r = spawnSync('node', [CHECKER], { encoding: 'utf8' });
54
+ assert.equal(r.status, 0, 'expected exit 0, got ' + r.status + '\nstdout:' + r.stdout + '\nstderr:' + r.stderr);
55
+ });
56
+
57
+ // --- Test 11: synthetic fixture without help_jtbd -> exit 1 ---
58
+ run('Test 11: command without help_jtbd causes exit 1', () => {
59
+ // Create a temp commands/foo.md without help_jtbd; the checker exits 1.
60
+ const tmpName = '___coverage_tmp_no_jtbd.md';
61
+ const tmpPath = path.join(COMMANDS_DIR, tmpName);
62
+ const content = '---\nname: tmp-no-jtbd\ndescription: temp fixture\n---\n# tmp\n';
63
+ fs.writeFileSync(tmpPath, content);
64
+ try {
65
+ const r = spawnSync('node', [CHECKER], { encoding: 'utf8' });
66
+ assert.notEqual(r.status, 0, 'expected non-zero exit (missing help_jtbd)');
67
+ assert.ok(/help_jtbd/i.test(r.stdout + r.stderr), 'expected diagnostic mentioning help_jtbd');
68
+ } finally {
69
+ fs.unlinkSync(tmpPath);
70
+ }
71
+ });
72
+
73
+ // --- Test 12: synthetic fixture file present but not in groups -> exit 1 ---
74
+ run('Test 12: command file not in help-groups.json causes exit 1', () => {
75
+ const tmpName = '___coverage_tmp_no_group.md';
76
+ const tmpPath = path.join(COMMANDS_DIR, tmpName);
77
+ // Has help_jtbd but is NOT in data/help-groups.json AND not admin.
78
+ const content =
79
+ '---\nname: tmp-no-group\ndescription: temp fixture\nhelp_jtbd: "Test fixture command without a group."\n---\n# tmp\n';
80
+ fs.writeFileSync(tmpPath, content);
81
+ try {
82
+ const r = spawnSync('node', [CHECKER], { encoding: 'utf8' });
83
+ assert.notEqual(r.status, 0, 'expected non-zero exit (missing from groups)');
84
+ assert.ok(
85
+ /help-groups\.json|MISSING from help-groups/i.test(r.stdout + r.stderr),
86
+ 'expected diagnostic mentioning help-groups.json'
87
+ );
88
+ } finally {
89
+ fs.unlinkSync(tmpPath);
90
+ }
91
+ });
92
+
93
+ // --- Test 13: Infrastructure group contains doctor ---
94
+ run('Test 13: Infrastructure group contains doctor (Cluster 5 audit closed)', () => {
95
+ const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
96
+ const infra = groups.groups.find((g) => g.id === 'infrastructure');
97
+ assert.ok(infra, 'infrastructure group must exist');
98
+ assert.ok(infra.commands.includes('doctor'), 'Infrastructure must contain doctor');
99
+ });
100
+
101
+ console.log('');
102
+ console.log('passed=' + passed + ' failed=' + failed);
103
+ if (failed > 0) {
104
+ console.error('Failed tests:');
105
+ for (const t of failures) console.error(' - ' + t);
106
+ process.exit(1);
107
+ }
108
+ process.exit(0);
@@ -0,0 +1,145 @@
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 2 -- help-renderer + help-groups.json tests.
8
+ *
9
+ * Test map (6 cases, one-to-one with PLAN Task 2 <behavior> Tests 1-6):
10
+ *
11
+ * 1. data/help-groups.json parses; groups[] has 11 entries.
12
+ * 2. Each group entry has {id, label, glyph, commands: []}.
13
+ * 3. Every commands/*.md (non-admin) appears in EXACTLY one group's commands[].
14
+ * 4. renderHelp({capability: 'truecolor'}) emits 24-bit ANSI escapes.
15
+ * 5. renderHelp({capability: 'ascii'}) emits zero \x1b escape sequences.
16
+ * 6. renderHelp({capability: 'ascii'}) contains sentinel glyphs for each
17
+ * of the 11 groups (proves all groups render in ASCII mode).
18
+ *
19
+ * Canon Part 3 (UI Ruling System), Part 7 (Reuse Before Build), Part 8
20
+ * (Graph Boundary -- filesystem only, no network, no Brain, no telemetry).
21
+ */
22
+
23
+ const assert = require('node:assert/strict');
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const REPO = path.resolve(__dirname, '..', '..');
28
+ const GROUPS_PATH = path.join(REPO, 'data', 'help-groups.json');
29
+ const RENDERER_PATH = path.join(REPO, 'scripts', 'help-renderer.cjs');
30
+ const COMMANDS_DIR = path.join(REPO, 'commands');
31
+
32
+ let passed = 0;
33
+ let failed = 0;
34
+ const failures = [];
35
+
36
+ function run(name, fn) {
37
+ try {
38
+ fn();
39
+ console.log(' PASS ' + name);
40
+ passed++;
41
+ } catch (err) {
42
+ console.error(' FAIL ' + name);
43
+ console.error(' ' + (err.message || err));
44
+ failed++;
45
+ failures.push(name);
46
+ }
47
+ }
48
+
49
+ // --- Test 1: data/help-groups.json parses + 11 groups ---
50
+ run('Test 1: data/help-groups.json parses; groups[] has 11 entries', () => {
51
+ assert.ok(fs.existsSync(GROUPS_PATH), 'data/help-groups.json must exist');
52
+ const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
53
+ assert.ok(Array.isArray(groups.groups), 'groups must be array');
54
+ assert.equal(groups.groups.length, 11, 'expected 11 groups, got ' + groups.groups.length);
55
+ });
56
+
57
+ // --- Test 2: each group has required keys ---
58
+ run('Test 2: each group has {id, label, glyph, commands: []}', () => {
59
+ const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
60
+ for (const g of groups.groups) {
61
+ assert.ok(typeof g.id === 'string' && g.id.length > 0, 'group.id missing: ' + JSON.stringify(g));
62
+ assert.ok(typeof g.label === 'string' && g.label.length > 0, 'group.label missing: ' + g.id);
63
+ assert.ok(typeof g.glyph === 'string' && g.glyph.length > 0, 'group.glyph missing: ' + g.id);
64
+ assert.ok(Array.isArray(g.commands), 'group.commands not array: ' + g.id);
65
+ }
66
+ });
67
+
68
+ // --- Test 3: every non-admin command appears in exactly one group ---
69
+ run('Test 3: every non-admin command appears in exactly one group', () => {
70
+ const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
71
+ const deprecated = new Set(
72
+ Object.keys(groups.deprecated_aliases || {}).filter((k) => !k.startsWith('_'))
73
+ );
74
+
75
+ // Build seen-count map.
76
+ const seen = new Map();
77
+ for (const g of groups.groups) {
78
+ for (const c of g.commands) {
79
+ seen.set(c, (seen.get(c) || 0) + 1);
80
+ }
81
+ }
82
+
83
+ // Every command file (non-admin) must be in exactly one group OR deprecated.
84
+ const files = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
85
+ const errors = [];
86
+ for (const f of files) {
87
+ const name = f.replace(/\.md$/, '');
88
+ const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
89
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
90
+ let visibility = 'user';
91
+ if (fm) {
92
+ const vm = fm[1].match(/^visibility:\s*(\S+)/m);
93
+ if (vm) visibility = vm[1];
94
+ }
95
+ if (visibility === 'admin') {
96
+ // admin commands may be in Infrastructure group or absent; allowed either way.
97
+ continue;
98
+ }
99
+ if (deprecated.has(name)) continue;
100
+ const count = seen.get(name) || 0;
101
+ if (count === 0) errors.push(name + ' (missing from all groups)');
102
+ if (count > 1) errors.push(name + ' (appears in ' + count + ' groups)');
103
+ }
104
+ assert.deepEqual(errors, [], 'command-group violations: ' + errors.join(', '));
105
+ });
106
+
107
+ // --- Test 4: truecolor render emits ANSI ---
108
+ run('Test 4: renderHelp({capability: truecolor}) emits 24-bit ANSI', () => {
109
+ assert.ok(fs.existsSync(RENDERER_PATH), 'scripts/help-renderer.cjs must exist');
110
+ const { renderHelp } = require(RENDERER_PATH);
111
+ const out = renderHelp({ capability: 'truecolor' });
112
+ assert.ok(out.length > 100, 'output too short');
113
+ // Look for 24-bit truecolor escape pattern: ESC[38;2;R;G;Bm
114
+ assert.ok(/\x1b\[38;2;\d+;\d+;\d+m/.test(out), 'no 24-bit ANSI escape found');
115
+ });
116
+
117
+ // --- Test 5: ascii render has zero ANSI ---
118
+ run('Test 5: renderHelp({capability: ascii}) emits zero \\x1b escapes', () => {
119
+ const { renderHelp } = require(RENDERER_PATH);
120
+ const out = renderHelp({ capability: 'ascii' });
121
+ assert.ok(out.length > 100, 'output too short');
122
+ assert.ok(!/\x1b/.test(out), 'ASCII output contains \\x1b escape');
123
+ });
124
+
125
+ // --- Test 6: ASCII output renders all 11 group labels ---
126
+ run('Test 6: ASCII render contains all 11 group labels', () => {
127
+ const { renderHelp, loadGroups } = require(RENDERER_PATH);
128
+ const groups = loadGroups();
129
+ const out = renderHelp({ capability: 'ascii', groups });
130
+ for (const g of groups.groups) {
131
+ assert.ok(
132
+ out.includes(g.label),
133
+ 'ASCII output missing group label "' + g.label + '"'
134
+ );
135
+ }
136
+ });
137
+
138
+ console.log('');
139
+ console.log('passed=' + passed + ' failed=' + failed);
140
+ if (failed > 0) {
141
+ console.error('Failed tests:');
142
+ for (const t of failures) console.error(' - ' + t);
143
+ process.exit(1);
144
+ }
145
+ process.exit(0);
@@ -37,7 +37,7 @@
37
37
  * 6. --stale-only flag: renders only stale sections + summary.
38
38
  * 7. <section> argument: renders that section's full triple (not truncated).
39
39
  * 8. Missing room: prints "no active room; /mos:rooms to list" + exit 0.
40
- * 9. Zero external network: no fetch/http/brain.mindrian.ai strings in the
40
+ * 9. Zero external network: no fetch/http/mindrian-brain.onrender.com strings in the
41
41
  * renderer source.
42
42
  * 10. Shape E zones present: header + rows + summary + actions.
43
43
  * 11. Warm-cache path: if statusline-cache has fresh data for the section,
@@ -468,7 +468,7 @@ assert.equal(
468
468
  const src = fs.readFileSync(RENDERER, 'utf8');
469
469
  // Canon Part 8: the renderer must be strictly LOCAL.
470
470
  const forbidden = [
471
- 'brain.mindrian.ai',
471
+ 'mindrian-brain.onrender.com',
472
472
  'http://',
473
473
  'https://',
474
474
  'require(\'node:http\')',
@@ -639,7 +639,7 @@ if (engineLoadable()) {
639
639
  run('Test 29: FORBIDDEN Brain network calls (grep)', () => {
640
640
  const src = fs.readFileSync(ENGINE_PATH, 'utf8');
641
641
  // Forbidden: brain-client.query / search / smartSearch and any direct
642
- // network egress to brain.mindrian.ai or fetch/curl for Brain.
642
+ // network egress to mindrian-brain.onrender.com or fetch/curl for Brain.
643
643
  const reBad = /brain[-_]?client\s*\.\s*(query|search|smartSearch)\b|brain\.mindrian\.ai|\bfetch\s*\(|\bcurl\b/;
644
644
  // Allow brain-client.isAvailable and brain-client.schema (Section 9.3).
645
645
  // Strip those allowed calls before scanning.
@@ -0,0 +1,127 @@
1
+ // palette-consistency.test.cjs -- Phase 121.5-03 Task 1
2
+ //
3
+ // Tests for references/visual/palette.json + scripts/check-palette-consistency.cjs.
4
+ //
5
+ // 5 tests: schema, hex format, live-repo check, fixture violation, derived_files paths.
6
+ //
7
+ // 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
+
15
+ const REPO_ROOT = path.join(__dirname, '..', '..');
16
+ const PALETTE_PATH = path.join(REPO_ROOT, 'references', 'visual', 'palette.json');
17
+ const CHECKER_PATH = path.join(REPO_ROOT, 'scripts', 'check-palette-consistency.cjs');
18
+
19
+ const checker = require(CHECKER_PATH);
20
+
21
+ let passed = 0;
22
+ let failed = 0;
23
+
24
+ function ok(name) {
25
+ console.log(' ok ' + name);
26
+ passed++;
27
+ }
28
+ function fail(name, msg) {
29
+ console.error(' FAIL ' + name + (msg ? ' -- ' + msg : ''));
30
+ failed++;
31
+ }
32
+
33
+ function assert(cond, name, msg) {
34
+ if (cond) ok(name);
35
+ else fail(name, msg);
36
+ }
37
+
38
+ console.log('test palette-consistency');
39
+
40
+ // Test 1: palette.json parses; required tiers present.
41
+ let palette;
42
+ try {
43
+ palette = JSON.parse(fs.readFileSync(PALETTE_PATH, 'utf8'));
44
+ const hasAll = palette.base && palette.palette_a_discovery && palette.palette_b_build &&
45
+ palette.ansi_5_color && Array.isArray(palette.derived_files);
46
+ assert(hasAll, 'test1 schema -- base, palette_a, palette_b, ansi_5, derived_files all present');
47
+ } catch (e) {
48
+ fail('test1 schema -- parse error: ' + e.message);
49
+ }
50
+
51
+ // Test 2: Every hex value matches /^#[0-9a-f]{6}$/i.
52
+ try {
53
+ const tiers = [palette.base, palette.palette_a_discovery, palette.palette_b_build, palette.extended || {}];
54
+ let bad = null;
55
+ for (const t of tiers) {
56
+ if (!t) continue;
57
+ for (const k of Object.keys(t)) {
58
+ const v = t[k];
59
+ if (typeof v === 'string' && !/^#[0-9a-f]{6}$/i.test(v)) {
60
+ bad = k + ' = ' + v;
61
+ break;
62
+ }
63
+ }
64
+ if (bad) break;
65
+ }
66
+ assert(bad === null, 'test2 hex format -- every hex matches /^#[0-9a-f]{6}$/i', bad);
67
+ } catch (e) {
68
+ fail('test2 hex format', e.message);
69
+ }
70
+
71
+ // Test 3: check() exits valid against the live repo (or at least returns
72
+ // a structured result with no stray hex from derived files that exist).
73
+ try {
74
+ const result = checker.check();
75
+ // The check() may have violations if Task 2 hasn't wired consumers yet -- that
76
+ // is acceptable for Task 1 alone; we assert that the result is well-structured.
77
+ const ok1 = typeof result === 'object' && typeof result.valid === 'boolean' &&
78
+ Array.isArray(result.violations) && typeof result.canonical_count === 'number';
79
+ assert(ok1, 'test3 live-repo check -- check() returns structured result');
80
+ // Smoke test: canonical_count is at least 9 (the base tier alone has 9 keys).
81
+ assert(result.canonical_count >= 9, 'test3b live-repo check -- canonical_count >= 9');
82
+ } catch (e) {
83
+ fail('test3 live-repo check', e.message);
84
+ }
85
+
86
+ // Test 4: Synthetic fixture with stray hex returns violations.length >= 1.
87
+ try {
88
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'palette-test-'));
89
+ const fakeFile = path.join(tmpDir, 'fake-consumer.css');
90
+ fs.writeFileSync(fakeFile, ':root { --not-canon: #abcdef; }');
91
+ // Build synthetic palette referencing this fake file via absolute path
92
+ // (relative-to-repo trick: place fixture under a known subpath).
93
+ // Instead use the in-process checker on a synthesized text directly.
94
+ const hexes = checker.hexesFromText(':root { --not-canon: #abcdef; --canon: #A63D2F; }');
95
+ const found1 = hexes.has('#abcdef');
96
+ const found2 = hexes.has('#a63d2f');
97
+ assert(found1 && found2, 'test4 fixture -- hexesFromText finds both canonical + non-canonical');
98
+ // Build a canonical set + check membership directly.
99
+ const canon = checker.collectCanonical(palette);
100
+ assert(canon.has('#a63d2f') && !canon.has('#abcdef'),
101
+ 'test4b fixture -- canonical set contains shipped red, not the synthetic non-canon hex');
102
+ // Clean up tmp dir
103
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
104
+ } catch (e) {
105
+ fail('test4 fixture', e.message);
106
+ }
107
+
108
+ // Test 5: derived_files entries point to files (or skip gracefully).
109
+ try {
110
+ const derived = palette.derived_files;
111
+ assert(Array.isArray(derived) && derived.length > 0,
112
+ 'test5 derived_files -- non-empty array');
113
+ // At least one of the derived files must exist on disk.
114
+ const existCount = derived.filter(function(e) {
115
+ return fs.existsSync(path.join(REPO_ROOT, e.path));
116
+ }).length;
117
+ assert(existCount > 0,
118
+ 'test5b derived_files -- at least one file exists on disk');
119
+ } catch (e) {
120
+ fail('test5 derived_files', e.message);
121
+ }
122
+
123
+ // Summary
124
+ console.log('');
125
+ console.log('palette-consistency.test: ' + passed + ' passed, ' + failed + ' failed');
126
+ if (failed > 0) process.exit(1);
127
+ process.exit(0);
@@ -37,6 +37,29 @@ const crypto = require('node:crypto');
37
37
  // ---------- Constants ----------
38
38
 
39
39
  const PENDING_TENSIONS_DIR = path.join(os.homedir(), '.mindrian', 'pending-tensions');
40
+
41
+ // Phase 121-02 D-05: tension_engagement emit support. Maps the local Phase 116
42
+ // last_response vocabulary {RESOLVE, LATER, SKIP, DROPPED} -> the unified
43
+ // telemetry user_response enum {resolve, defer, ignore}. DROPPED is system-
44
+ // driven (3-strikes decay or explicit drop) and is intentionally excluded so
45
+ // only user-initiated transitions ever land in the unified stream.
46
+ const USER_RESPONSE_MAP = Object.freeze({
47
+ RESOLVE: 'resolve',
48
+ LATER: 'defer',
49
+ SKIP: 'ignore',
50
+ });
51
+
52
+ // Map Phase 116 tension_type vocabulary -> unified telemetry enum.
53
+ // contradiction -> contradicts
54
+ // convergence -> converges
55
+ // stale_decision -> invalidates (a stale decision invalidates its premise)
56
+ // open_question -> invalidates (an open question invalidates closure)
57
+ const TENSION_TYPE_MAP = Object.freeze({
58
+ contradiction: 'contradicts',
59
+ convergence: 'converges',
60
+ stale_decision: 'invalidates',
61
+ open_question: 'invalidates',
62
+ });
40
63
  const VALID_STATES = Object.freeze(new Set(['queued', 'surfaced', 'resolved', 'dropped']));
41
64
  const VALID_TYPES = Object.freeze(new Set([
42
65
  'contradiction',
@@ -220,6 +243,58 @@ function markSurfaced(roomSlug, tension_id) {
220
243
  return { ok: true, surfacing_count: next.surfacing_count };
221
244
  }
222
245
 
246
+ /**
247
+ * Phase 121-02 D-05: capture user engagement with surfaced tensions into the
248
+ * unified ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl stream (Plan 121-00
249
+ * writer chokepoint). Emit ONLY on user-initiated state transitions
250
+ * (RESOLVE -> resolve, LATER -> defer, SKIP -> ignore). Decayed tensions
251
+ * (system-driven 3-strikes via evaluateAndDecay or explicit markDropped) are
252
+ * NOT engagement events and intentionally do NOT emit.
253
+ *
254
+ * Non-blocking: try/catch wraps every step. A Canon Part 8 forbidden-pattern
255
+ * rejection in the writer never crashes the markResolved transition; the
256
+ * JSONL state append has already succeeded by the time we emit.
257
+ *
258
+ * TTR: time from last_surfaced_at -> now. Floor at 0; integer seconds.
259
+ * Context hash: 16-char sha256 of context_signature.focus_node_id (or empty).
260
+ */
261
+ function emitTensionEngagementUnified(roomSlug, prev, response) {
262
+ try {
263
+ if (!USER_RESPONSE_MAP[response]) return; // system-driven; do not emit
264
+ let writer;
265
+ try {
266
+ writer = require('../core/telemetry/writer.cjs');
267
+ } catch (_e) {
268
+ return; // missing writer module; soft skip
269
+ }
270
+ if (!writer || typeof writer.emit !== 'function') return;
271
+ const tensionType = TENSION_TYPE_MAP[prev && prev.type] || 'invalidates';
272
+ const surfacedAt = (prev && Number.isFinite(prev.last_surfaced_at))
273
+ ? Number(prev.last_surfaced_at)
274
+ : 0;
275
+ const ttrSeconds = surfacedAt > 0
276
+ ? Math.max(0, Math.round((Date.now() - surfacedAt) / 1000))
277
+ : 0;
278
+ // Hash the focus node id (or any context_signature field) to a 16-char
279
+ // sha256 prefix; never the raw scalar.
280
+ let contextSrc = '';
281
+ if (prev && isPlainObject(prev.context_signature)) {
282
+ contextSrc = String(prev.context_signature.focus_node_id || prev.context_signature.context_id || '');
283
+ }
284
+ const contextHash = crypto.createHash('sha256').update(contextSrc).digest('hex').slice(0, 16);
285
+ const slugHash = crypto.createHash('sha256').update(String(roomSlug || '')).digest('hex');
286
+ writer.emit('tension_engagement', {
287
+ tension_type: String(tensionType).slice(0, 64),
288
+ user_response: USER_RESPONSE_MAP[response],
289
+ ttr_seconds: ttrSeconds,
290
+ room_slug_sha256: slugHash,
291
+ context_hash: contextHash,
292
+ });
293
+ } catch (_e) {
294
+ // Swallow: telemetry MUST never crash the tension transition.
295
+ }
296
+ }
297
+
223
298
  /**
224
299
  * Mark a tension as resolved with a user response. State -> 'resolved'.
225
300
  *
@@ -239,6 +314,11 @@ function markResolved(roomSlug, tension_id, response) {
239
314
  });
240
315
  const r = appendTension(roomSlug, next);
241
316
  if (!r.ok) return r;
317
+ // Phase 121-02 D-05: emit only AFTER the JSONL state append succeeds. The
318
+ // emit is gated on USER_RESPONSE_MAP (RESOLVE/LATER/SKIP) so DROPPED falls
319
+ // through silently. This is the load-bearing exclusion: decayed tensions
320
+ // are system-driven and must not count as engagement.
321
+ emitTensionEngagementUnified(roomSlug, prev, response);
242
322
  return { ok: true };
243
323
  }
244
324