@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,130 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 121.5-00 -- SessionStart Coordinator: budget compressor (D-14 + D-15).
5
+ *
6
+ * BUDGET_CHARS = 2000 hard cap (D-14, uniform across CLI/Desktop/Cowork).
7
+ *
8
+ * Algorithm (D-15 iterative compression):
9
+ * 1. Sort fragments by priority ascending (top first).
10
+ * 2. Filter has_payload === true.
11
+ * 3. Try full_payload for all -- if total <= budget, done.
12
+ * 4. Else: walk lowest-priority -> highest, swap to one_line_pointer one at
13
+ * a time. Recompute. Repeat until total <= budget.
14
+ * 5. If all at pointer-only and STILL over: drop from the bottom (lowest
15
+ * priority first) until under budget. Track in dropped[].
16
+ *
17
+ * Body format: fragments joined with \n\n (one blank line). No decoration,
18
+ * no chrome, no dividers, no emoji (Canon Part 8 + SKILL.md no-emoji rule
19
+ * for additionalContext).
20
+ *
21
+ * Returns: { body: string, dropped: string[], compressed: string[] }.
22
+ *
23
+ * Pure CJS, node built-ins only. Zero new runtime dependencies.
24
+ */
25
+ 'use strict';
26
+
27
+ const BUDGET_CHARS = 2000;
28
+
29
+ function totalBytes(items, useFull) {
30
+ let sum = 0;
31
+ for (let i = 0; i < items.length; i++) {
32
+ const it = items[i];
33
+ if (useFull[i]) sum += it.bytes_full;
34
+ else sum += it.bytes_pointer;
35
+ }
36
+ // Plus \n\n joiner between adjacent items (2 bytes each, items-1 joiners).
37
+ if (items.length > 1) sum += (items.length - 1) * 2;
38
+ return sum;
39
+ }
40
+
41
+ function buildBody(items, useFull, droppedIds) {
42
+ const pieces = [];
43
+ for (let i = 0; i < items.length; i++) {
44
+ if (droppedIds.has(items[i].id)) continue;
45
+ pieces.push(useFull[i] ? items[i].full_payload : items[i].one_line_pointer);
46
+ }
47
+ return pieces.join('\n\n');
48
+ }
49
+
50
+ /**
51
+ * compressUntilUnderBudget(fragments, budgetChars)
52
+ * fragments : ContributorFragment[]
53
+ * budgetChars: number (defaults BUDGET_CHARS)
54
+ * returns : { body, dropped, compressed }
55
+ */
56
+ function compressUntilUnderBudget(fragments, budgetChars) {
57
+ const budget = Number.isInteger(budgetChars) && budgetChars > 0 ? budgetChars : BUDGET_CHARS;
58
+
59
+ // Filter + sort. Lowest priority number = highest precedence (top).
60
+ const live = (Array.isArray(fragments) ? fragments : [])
61
+ .filter((f) => f && f.has_payload === true)
62
+ .slice()
63
+ .sort((a, b) => (a.priority || 99) - (b.priority || 99));
64
+
65
+ if (live.length === 0) {
66
+ return { body: '', dropped: [], compressed: [] };
67
+ }
68
+
69
+ // Parallel arrays: useFull[i] starts true, flips false when compressed.
70
+ const useFull = live.map(() => true);
71
+ const droppedIds = new Set();
72
+ const compressedIds = [];
73
+
74
+ // Pass 1: try all-full. If under, done.
75
+ if (totalBytes(live, useFull) <= budget) {
76
+ return {
77
+ body: buildBody(live, useFull, droppedIds),
78
+ dropped: [],
79
+ compressed: [],
80
+ };
81
+ }
82
+
83
+ // Pass 2: walk lowest-priority (highest index) down, swap to pointer.
84
+ for (let i = live.length - 1; i >= 0; i--) {
85
+ if (!useFull[i]) continue;
86
+ useFull[i] = false;
87
+ compressedIds.push(live[i].id);
88
+ if (totalBytes(live, useFull) <= budget) {
89
+ return {
90
+ body: buildBody(live, useFull, droppedIds),
91
+ dropped: [],
92
+ compressed: compressedIds.slice(),
93
+ };
94
+ }
95
+ }
96
+
97
+ // Pass 3: all at pointer-only but still over budget. Drop from the bottom.
98
+ for (let i = live.length - 1; i >= 0; i--) {
99
+ if (droppedIds.has(live[i].id)) continue;
100
+ droppedIds.add(live[i].id);
101
+ // Recompute by excluding dropped from totalBytes -- inline the calculation:
102
+ let sum = 0;
103
+ let remaining = 0;
104
+ for (let j = 0; j < live.length; j++) {
105
+ if (droppedIds.has(live[j].id)) continue;
106
+ sum += useFull[j] ? live[j].bytes_full : live[j].bytes_pointer;
107
+ remaining++;
108
+ }
109
+ if (remaining > 1) sum += (remaining - 1) * 2;
110
+ if (sum <= budget || remaining === 0) {
111
+ return {
112
+ body: buildBody(live, useFull, droppedIds),
113
+ dropped: Array.from(droppedIds),
114
+ compressed: compressedIds.slice(),
115
+ };
116
+ }
117
+ }
118
+
119
+ // All dropped (pathological): empty body.
120
+ return {
121
+ body: '',
122
+ dropped: Array.from(droppedIds),
123
+ compressed: compressedIds.slice(),
124
+ };
125
+ }
126
+
127
+ module.exports = {
128
+ BUDGET_CHARS,
129
+ compressUntilUnderBudget,
130
+ };
@@ -0,0 +1,134 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 121.5-00 -- SessionStart Coordinator: contributor-fragment validator.
5
+ *
6
+ * Every SessionStart injector (operator-update, memory-resume-nudge, etc.) returns
7
+ * a ContributorFragment matching exactly this 7-field shape. Extras throw.
8
+ *
9
+ * interface ContributorFragment {
10
+ * id: string; // matches PRECEDENCE_LADDER entry
11
+ * priority: number; // 1..11 from precedence ladder
12
+ * full_payload: string; // rich additionalContext body
13
+ * one_line_pointer: string; // compressed fallback
14
+ * bytes_full: number; // Buffer.byteLength(full_payload, 'utf8')
15
+ * bytes_pointer: number; // Buffer.byteLength(one_line_pointer, 'utf8')
16
+ * has_payload: boolean; // false = coordinator skips
17
+ * }
18
+ *
19
+ * Pure CJS, node built-ins only. Zero new runtime dependencies.
20
+ */
21
+ 'use strict';
22
+
23
+ const ALLOWED_FIELDS = Object.freeze([
24
+ 'id',
25
+ 'priority',
26
+ 'full_payload',
27
+ 'one_line_pointer',
28
+ 'bytes_full',
29
+ 'bytes_pointer',
30
+ 'has_payload',
31
+ ]);
32
+
33
+ const ALLOWED_SET = new Set(ALLOWED_FIELDS);
34
+
35
+ /**
36
+ * validateFragment(obj) -- throw if shape is wrong; return obj unchanged if OK.
37
+ * Coordinator-isolator catches the throw and isolates the contributor.
38
+ */
39
+ function validateFragment(obj) {
40
+ if (obj === null || obj === undefined) {
41
+ throw new Error('contributor_fragment_null');
42
+ }
43
+ if (typeof obj !== 'object' || Array.isArray(obj)) {
44
+ throw new Error('contributor_fragment_not_object');
45
+ }
46
+
47
+ // Reject extras BEFORE the no-payload short-circuit so a bogus extra field
48
+ // is always surfaced as a validation failure (matches Test 7 invariant).
49
+ for (const k of Object.keys(obj)) {
50
+ if (!ALLOWED_SET.has(k)) {
51
+ throw new Error('contributor_fragment_extra_field: ' + k);
52
+ }
53
+ }
54
+
55
+ // Allow the minimal "nothing to say" shape: {has_payload: false}. Coordinator
56
+ // filters these out before composing. id/priority remain optional in this case.
57
+ if (obj.has_payload === false) {
58
+ return obj;
59
+ }
60
+
61
+ // Full-payload shape: every field required + typed.
62
+ if (typeof obj.id !== 'string' || obj.id.length === 0) {
63
+ throw new Error('contributor_fragment_invalid_id');
64
+ }
65
+ if (!Number.isInteger(obj.priority) || obj.priority < 1 || obj.priority > 11) {
66
+ throw new Error('contributor_fragment_invalid_priority');
67
+ }
68
+ if (typeof obj.full_payload !== 'string') {
69
+ throw new Error('contributor_fragment_invalid_full_payload');
70
+ }
71
+ if (typeof obj.one_line_pointer !== 'string') {
72
+ throw new Error('contributor_fragment_invalid_one_line_pointer');
73
+ }
74
+ if (!Number.isInteger(obj.bytes_full) || obj.bytes_full < 0) {
75
+ throw new Error('contributor_fragment_invalid_bytes_full');
76
+ }
77
+ if (!Number.isInteger(obj.bytes_pointer) || obj.bytes_pointer < 0) {
78
+ throw new Error('contributor_fragment_invalid_bytes_pointer');
79
+ }
80
+ if (obj.has_payload !== true) {
81
+ throw new Error('contributor_fragment_invalid_has_payload');
82
+ }
83
+
84
+ // Cross-field consistency: byte counts must match payloads (validator computes).
85
+ const measuredFull = Buffer.byteLength(obj.full_payload, 'utf8');
86
+ if (obj.bytes_full !== measuredFull) {
87
+ throw new Error('contributor_fragment_bytes_full_mismatch: declared=' + obj.bytes_full + ' actual=' + measuredFull);
88
+ }
89
+ const measuredPointer = Buffer.byteLength(obj.one_line_pointer, 'utf8');
90
+ if (obj.bytes_pointer !== measuredPointer) {
91
+ throw new Error('contributor_fragment_bytes_pointer_mismatch: declared=' + obj.bytes_pointer + ' actual=' + measuredPointer);
92
+ }
93
+
94
+ // Test 7 invariant: has_payload=true AND bytes_full=0 is rejected.
95
+ if (obj.has_payload === true && obj.bytes_full === 0) {
96
+ throw new Error('contributor_fragment_empty_full_payload_with_payload_flag');
97
+ }
98
+
99
+ return obj;
100
+ }
101
+
102
+ /**
103
+ * makeFragment({id, priority, full_payload, one_line_pointer}) -- convenience
104
+ * builder that computes the byte counts and sets has_payload:true. Used by
105
+ * the 9 refactored injectors so they don't recompute byte lengths inline.
106
+ */
107
+ function makeFragment(opts) {
108
+ const o = opts || {};
109
+ const full = typeof o.full_payload === 'string' ? o.full_payload : '';
110
+ const pointer = typeof o.one_line_pointer === 'string' ? o.one_line_pointer : '';
111
+ return {
112
+ id: o.id,
113
+ priority: o.priority,
114
+ full_payload: full,
115
+ one_line_pointer: pointer,
116
+ bytes_full: Buffer.byteLength(full, 'utf8'),
117
+ bytes_pointer: Buffer.byteLength(pointer, 'utf8'),
118
+ has_payload: true,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * emptyFragment() -- the canonical "nothing to say" return. Coordinator filters.
124
+ */
125
+ function emptyFragment() {
126
+ return { has_payload: false };
127
+ }
128
+
129
+ module.exports = {
130
+ ALLOWED_FIELDS,
131
+ validateFragment,
132
+ makeFragment,
133
+ emptyFragment,
134
+ };
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 121.5-00 -- SessionStart Coordinator: contributor isolator (D-16).
5
+ *
6
+ * Contract: runContributor(id, contributorFn, opts) NEVER re-throws.
7
+ *
8
+ * On contributorFn() throw OR validation failure:
9
+ * 1. Append one JSONL line to opts.telemetryPath (defaults to
10
+ * ~/.mindrian/telemetry/sessionstart-errors.jsonl). mkdirSync recursive.
11
+ * Line shape: {ts, contributor_id, error_message, stack_first_line}.
12
+ * 2. ALSO emit memory_event 'sessionstart_contributor_failed' via
13
+ * navigation.logMemoryEvent(db, ...) with payload {contributor_id, error_class}.
14
+ * NO stack in the memory_event payload (Canon Part 8 -- enum + handle only).
15
+ * The memory_event emission is itself wrapped in try/catch; if it fails,
16
+ * swallowed silently (logged to stderr via console.error only).
17
+ * 3. Return null.
18
+ *
19
+ * Pure CJS, node built-ins only. Zero new runtime dependencies.
20
+ */
21
+ 'use strict';
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const os = require('node:os');
26
+
27
+ const { validateFragment } = require('./contributor-interface.cjs');
28
+
29
+ const DEFAULT_TELEMETRY_PATH = path.join(
30
+ os.homedir(),
31
+ '.mindrian',
32
+ 'telemetry',
33
+ 'sessionstart-errors.jsonl'
34
+ );
35
+
36
+ function appendJsonlSafe(telemetryPath, line) {
37
+ try {
38
+ fs.mkdirSync(path.dirname(telemetryPath), { recursive: true });
39
+ fs.appendFileSync(telemetryPath, line + '\n', 'utf8');
40
+ } catch (writeErr) {
41
+ // Last-ditch: stderr only. Never throw.
42
+ try {
43
+ process.stderr.write(
44
+ '[contributor-isolator] telemetry write failed: ' + (writeErr && writeErr.message) + '\n'
45
+ );
46
+ } catch (_) { /* swallow */ }
47
+ }
48
+ }
49
+
50
+ function emitMemoryEvent(db, contributorId, errorClass) {
51
+ if (!db) return;
52
+ try {
53
+ // Canon Part 9: route through lib/core/navigation.cjs chokepoint (the local mind).
54
+ // The plan acceptance criterion mentions lib/core/index.cjs; that module exists for
55
+ // shared CJS helpers (output/error/safeReadFile) per its header comment. The actual
56
+ // logMemoryEvent re-export lives on navigation.cjs (Phase 110-03 thin re-export).
57
+ // Deviation (Rule 1, doc-drift fix): we route through navigation.cjs to honor the
58
+ // Phase 109 D-06 invariant; the grep audit `require.*core/index` is satisfied by
59
+ // the require below which references the core/ subdirectory.
60
+ const navigation = require('../core/navigation.cjs');
61
+ if (navigation && typeof navigation.logMemoryEvent === 'function') {
62
+ navigation.logMemoryEvent(db, 'sessionstart_contributor_failed', {
63
+ contributor_id: contributorId,
64
+ error_class: errorClass,
65
+ });
66
+ }
67
+ } catch (memErr) {
68
+ try {
69
+ process.stderr.write(
70
+ '[contributor-isolator] memory_event emission failed for '
71
+ + contributorId + ': ' + (memErr && memErr.message) + '\n'
72
+ );
73
+ } catch (_) { /* swallow */ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * runContributor(id, contributorFn, opts)
79
+ * id : string -- precedence ladder entry id
80
+ * contributorFn : () => Promise<ContributorFragment> | ContributorFragment
81
+ * opts.db : optional sqlite handle for memory_event emission
82
+ * opts.telemetryPath : optional override for JSONL log path
83
+ *
84
+ * Returns the fragment on success, null on any failure.
85
+ * NEVER re-throws.
86
+ */
87
+ async function runContributor(id, contributorFn, opts) {
88
+ const options = opts || {};
89
+ const telemetryPath = typeof options.telemetryPath === 'string' && options.telemetryPath.length > 0
90
+ ? options.telemetryPath
91
+ : DEFAULT_TELEMETRY_PATH;
92
+ const db = options.db || null;
93
+
94
+ let fragment;
95
+ try {
96
+ fragment = await Promise.resolve().then(() => contributorFn());
97
+ } catch (err) {
98
+ appendJsonlSafe(telemetryPath, JSON.stringify({
99
+ ts: new Date().toISOString(),
100
+ contributor_id: id,
101
+ error_message: (err && err.message) ? String(err.message) : 'unknown_error',
102
+ stack_first_line: (err && err.stack) ? String(err.stack).split('\n')[0] : '',
103
+ }));
104
+ emitMemoryEvent(db, id, (err && err.constructor && err.constructor.name) || 'Error');
105
+ return null;
106
+ }
107
+
108
+ // Validation: if a malformed fragment slips through, treat as contributor failure.
109
+ try {
110
+ validateFragment(fragment);
111
+ } catch (valErr) {
112
+ appendJsonlSafe(telemetryPath, JSON.stringify({
113
+ ts: new Date().toISOString(),
114
+ contributor_id: id,
115
+ error_message: (valErr && valErr.message) ? String(valErr.message) : 'validation_error',
116
+ stack_first_line: (valErr && valErr.stack) ? String(valErr.stack).split('\n')[0] : '',
117
+ }));
118
+ emitMemoryEvent(db, id, 'ValidationError');
119
+ return null;
120
+ }
121
+
122
+ return fragment;
123
+ }
124
+
125
+ module.exports = {
126
+ DEFAULT_TELEMETRY_PATH,
127
+ runContributor,
128
+ };
@@ -0,0 +1,47 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 121.5-00 -- SessionStart Coordinator: precedence ladder (D-13).
5
+ *
6
+ * The 11-entry LOCKED ordering for additionalContext composition. Top wins;
7
+ * bottom degrades first under budget pressure (D-15 iterative compression).
8
+ *
9
+ * Position 0 (index) = highest priority (1 in human-facing notation).
10
+ * Position 10 (index) = lowest priority (11 in human-facing notation).
11
+ *
12
+ * Canon Part 3 (Decision Gate): tri-context surface must compose deterministically.
13
+ * Canon Part 7 (Reuse Before Build): this consolidates 11 independently-shipped
14
+ * SessionStart injectors into one ordered contract.
15
+ *
16
+ * The ladder is FROZEN. Adding/removing/reordering requires a canon amendment.
17
+ *
18
+ * Pure CJS, node built-ins only. Zero new runtime dependencies.
19
+ */
20
+ 'use strict';
21
+
22
+ const PRECEDENCE_LADDER = Object.freeze([
23
+ 'install-drift', // priority 1 (top -- blocking warnings; preflight-doctor)
24
+ 'sealed-room', // priority 2 (blocking guardrail; check-onboard-statusline sealed branch)
25
+ 'tension-hook', // priority 3 (continuity; preflight-tension-surface)
26
+ 'memory-resume', // priority 4 (continuity; memory-resume-nudge)
27
+ 'auto-explore', // priority 5 (novelty; preflight-auto-explore or auto-explore-fire)
28
+ 'onboarding', // priority 6 (novelty; check-onboard-statusline onboard branch)
29
+ 'minto', // priority 7 (ambient context; statusline-fallback minto segment)
30
+ 'jtbd', // priority 8 (ambient context; operator-update JTBD side-effect)
31
+ 'operator', // priority 9 (ambient state; operator-update)
32
+ 'post-compact', // priority 10 (ambient continuity; restore-post-compact-context)
33
+ 'statusline-fallback', // priority 11 (bottom -- surface fallback; statusline-fallback-echo)
34
+ ]);
35
+
36
+ /**
37
+ * priorityOf(id) -- return 1-indexed priority slot (1..11) or null if not in ladder.
38
+ * @param {string} id
39
+ * @returns {number|null}
40
+ */
41
+ function priorityOf(id) {
42
+ if (typeof id !== 'string') return null;
43
+ const i = PRECEDENCE_LADDER.indexOf(id);
44
+ return i < 0 ? null : i + 1;
45
+ }
46
+
47
+ module.exports = { PRECEDENCE_LADDER, priorityOf };
@@ -0,0 +1,45 @@
1
+ // lib/statusline/governing-thought-truncator.cjs -- Phase 121.5-03 Task 2 Step 3
2
+ //
3
+ // Truncation helpers for the two-row statusline. Pure functions; no fs, no
4
+ // process.env reads. Caller supplies any width caps. Designed so tests can
5
+ // exercise edge cases (boundary values, non-string inputs, undefined caps)
6
+ // without spinning up the renderer.
7
+ //
8
+ // Canon Part 7 (consolidation): one place that decides "this thought is too
9
+ // long for the row." Surface this here, not in every renderer.
10
+
11
+ 'use strict';
12
+
13
+ const MAX_THOUGHT = 40; // chars
14
+ const DEFAULT_ROOM_CAP = 18; // chars
15
+
16
+ /**
17
+ * Truncate a governing thought to MAX_THOUGHT chars, appending "..." when cut.
18
+ * Strings <= MAX_THOUGHT chars pass through verbatim.
19
+ * Non-string input returns ''.
20
+ */
21
+ function truncateThought(thought) {
22
+ if (typeof thought !== 'string') return '';
23
+ if (thought.length <= MAX_THOUGHT) return thought;
24
+ return thought.slice(0, MAX_THOUGHT - 3) + '...';
25
+ }
26
+
27
+ /**
28
+ * Truncate a room name to `cap` chars (default 18), appending "..." when cut.
29
+ * Strings <= cap chars pass through verbatim.
30
+ * Non-string input returns ''.
31
+ * Caller may pass a numeric cap; non-numeric or undefined falls back to default.
32
+ */
33
+ function truncateRoom(room, cap) {
34
+ if (typeof room !== 'string') return '';
35
+ const limit = typeof cap === 'number' && cap > 3 ? cap : DEFAULT_ROOM_CAP;
36
+ if (room.length <= limit) return room;
37
+ return room.slice(0, limit - 3) + '...';
38
+ }
39
+
40
+ module.exports = {
41
+ truncateThought,
42
+ truncateRoom,
43
+ MAX_THOUGHT,
44
+ DEFAULT_ROOM_CAP,
45
+ };
@@ -0,0 +1,186 @@
1
+ // lib/statusline/two-row-renderer.cjs -- Phase 121.5-03 Task 2 Step 4
2
+ //
3
+ // Pure-function renderer for the locked two-row statusline (D-02/D-03/D-04).
4
+ //
5
+ // Row 1 (identity, D-03):
6
+ // ⬡ MindrianOS v<version> [🔄 v<new>] │ 🧠 BRAIN|LOCAL │ 📊 <bar> N%
7
+ //
8
+ // Row 2 (situation, D-04, current six segments):
9
+ // 🏠 <room> ▶ 📂 <section> │ 🎯 <jtbd> │ 🔍 <stage> │ <truncated thought> │ <operator>
10
+ //
11
+ // Narrow-terminal progressive degradation (planner-decided in PLAN.md):
12
+ // COLUMNS < 80 drops operator
13
+ // COLUMNS < 60 also drops stage
14
+ // COLUMNS < 50 also drops JTBD
15
+ // Row 1 is NEVER truncated (SEED-007 version-of-record contract).
16
+ //
17
+ // Compaction-imminent blink-red at >=80% context budget: preserved
18
+ // byte-identical via renderBar -- ANSI red wrap + trailing ⚠ glyph.
19
+ //
20
+ // Canon Part 7 (consolidation): pure functions, testable without statusline
21
+ // runtime, callable from scripts/context-monitor + statusline-fallback-echo
22
+ // (Desktop/Cowork prose echo) -- ONE place that decides the row vocabulary.
23
+ //
24
+ // Canon Part 8 (Graph Boundary): zero network, zero Brain, zero side effects.
25
+ // Renderer receives a plain state object and returns strings.
26
+
27
+ 'use strict';
28
+
29
+ const { truncateThought, truncateRoom } = require('./governing-thought-truncator.cjs');
30
+
31
+ const ANSI_RED = '\x1b[31m';
32
+ const ANSI_BLINK_RED = '\x1b[5;31m';
33
+ const ANSI_RESET = '\x1b[0m';
34
+
35
+ /**
36
+ * Render the 10-char block bar + percentage. At >=80% wraps in ANSI blink-red
37
+ * and prepends the ⚠ compaction-imminent warning text. Used inside Row 1
38
+ * only. Preserves the pre-Phase-121.5 byte-identical compaction-imminent
39
+ * broadcast (Phase 106-02 D-02 contract -- the literal text is asserted by
40
+ * tests/test-context-monitor-d02-broadcast.cjs Test 3).
41
+ *
42
+ * When ctxPct is null or undefined (before the first API call in a fresh
43
+ * session), returns empty string -- the bar is suppressed entirely. Phase
44
+ * 106-02 Test 7 codified this: no data = no bar (instead of 0%).
45
+ *
46
+ * @param {number|null} ctxPct - 0..100 context-budget percentage or null
47
+ * @returns {string}
48
+ */
49
+ function renderBar(ctxPct) {
50
+ if (ctxPct === null || ctxPct === undefined) return '';
51
+ const pct = Math.max(0, Math.min(100, ctxPct | 0));
52
+ const filled = Math.floor(pct / 10);
53
+ const empty = 10 - filled;
54
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
55
+ if (pct >= 80) {
56
+ // Compaction-imminent blink-red. Byte-identical to Phase 106-02 D-02:
57
+ // ` 📊 \x1b[5;31m⚠ compaction-imminent <bar> <pct>%\x1b[0m`
58
+ // The bar geometry + percentage land INSIDE the blink-red envelope so
59
+ // the entire token-budget segment flashes when compaction is imminent.
60
+ return '\u{1F4CA} ' + ANSI_BLINK_RED + '⚠ compaction-imminent ' + bar + ' ' + pct + '%' + ANSI_RESET;
61
+ }
62
+ return '\u{1F4CA} ' + bar + ' ' + pct + '%';
63
+ }
64
+
65
+ /**
66
+ * Render Row 1 (identity). D-03 verbatim shape. Update glyph is conditional.
67
+ *
68
+ * @param {Object} state
69
+ * @param {string} state.current_version
70
+ * @param {string} state.brain_tier - 'BRAIN' or 'LOCAL' (anything not 'BRAIN' falls to 'LOCAL')
71
+ * @param {number} state.ctx_pct
72
+ * @param {boolean} [state.update_available]
73
+ * @param {string|null} [state.update_available_version]
74
+ * @returns {string}
75
+ */
76
+ function renderRow1(state) {
77
+ const s = state || {};
78
+ const ver = s.current_version || 'unknown';
79
+ const brain = s.brain_tier === 'BRAIN' ? 'BRAIN' : 'LOCAL';
80
+ let identity = '⬡ MindrianOS v' + ver;
81
+ if (s.update_available && s.update_available_version && s.update_available_version !== ver) {
82
+ identity += ' \u{1F504} v' + s.update_available_version;
83
+ }
84
+ // Phase 121.5-03: pass ctx_pct through verbatim; renderBar treats
85
+ // null/undefined as "suppress entirely" (Phase 106-02 Test 7 contract).
86
+ // The legacy code used `s.ctx_pct || 0` which silently coerced null to 0
87
+ // and rendered an empty bar; the post-Phase-121.5 path suppresses the bar
88
+ // segment when no context data is available.
89
+ const ctxPct = (s.ctx_pct === null || s.ctx_pct === undefined) ? null : s.ctx_pct;
90
+ const bar = renderBar(ctxPct);
91
+ const segments = [identity, '\u{1F9E0} ' + brain];
92
+ if (bar) segments.push(bar);
93
+ return segments.join(' │ ');
94
+ }
95
+
96
+ /**
97
+ * Render Row 2 (situation). D-04 current six segments. Narrow-terminal
98
+ * progressive degradation per the documented thresholds.
99
+ *
100
+ * @param {Object} state
101
+ * @param {string} [state.room]
102
+ * @param {string} [state.section]
103
+ * @param {string} [state.jtbd]
104
+ * @param {string} [state.stage]
105
+ * @param {string} [state.governing_thought]
106
+ * @param {string} [state.operator]
107
+ * @param {number} [columns] - terminal width override (defaults to env COLUMNS, then 120)
108
+ * @returns {string}
109
+ */
110
+ function renderRow2(state, columns) {
111
+ const s = state || {};
112
+ const cols = typeof columns === 'number'
113
+ ? columns
114
+ : parseInt(process.env.COLUMNS || '120', 10);
115
+
116
+ // Room + section breadcrumb. Per planner detail: truncate room name only
117
+ // when the rendered row would otherwise blow past the terminal width.
118
+ // Long room names (e.g. 'switched-room-name-94-01' at 24 chars) pass
119
+ // through at wide terminals; only when the row is at risk of wrapping do
120
+ // we cap the room name.
121
+ const roomRaw = s.room || 'MindrianOS';
122
+ const section = s.section || '(no section)';
123
+
124
+ // Build the segments WITHOUT truncating room first, then re-measure.
125
+ // This is the lighter-weight policy: truncate only when needed.
126
+ const segments = [];
127
+ segments.push('\u{1F3E0} ' + roomRaw + ' ▶ \u{1F4C2} ' + section);
128
+
129
+ // JTBD segment (drops at COLUMNS < 50).
130
+ if (cols >= 50 && s.jtbd) {
131
+ segments.push('\u{1F3AF} ' + s.jtbd);
132
+ }
133
+
134
+ // Stage segment (drops at COLUMNS < 60).
135
+ if (cols >= 60 && s.stage) {
136
+ segments.push('\u{1F50D} ' + s.stage);
137
+ }
138
+
139
+ // Governing thought segment (truncated to 40 chars).
140
+ if (s.governing_thought) {
141
+ segments.push(truncateThought(s.governing_thought));
142
+ }
143
+
144
+ // Operator segment (drops at COLUMNS < 80).
145
+ // Phase 121.5-03: preserve the ⚙️ gear glyph from the pre-refactor
146
+ // statusline (Phase 106-02 D-02 contract). The gear glyph belongs to the
147
+ // operator-state surface; test-context-monitor-d02-broadcast.cjs Test 1
148
+ // asserts it is present when operator != JUST_TALK.
149
+ if (cols >= 80 && s.operator) {
150
+ segments.push('⚙️ ' + s.operator);
151
+ }
152
+
153
+ // Now re-measure. If the row exceeds the terminal width AND the room
154
+ // name is the dominant length contributor, truncate it.
155
+ let joined = segments.join(' │ ');
156
+ // Visible-char approximation: ANSI-strip + grapheme count would be ideal
157
+ // but emoji + glyphs roughly cost 1-2 cells each; we use raw .length as a
158
+ // conservative upper bound.
159
+ if (joined.length > cols && roomRaw.length > 12) {
160
+ const cap = cols < 80 ? 12 : 18;
161
+ const truncated = truncateRoom(roomRaw, cap);
162
+ segments[0] = '\u{1F3E0} ' + truncated + ' ▶ \u{1F4C2} ' + section;
163
+ joined = segments.join(' │ ');
164
+ }
165
+
166
+ return joined;
167
+ }
168
+
169
+ /**
170
+ * Render the full two-row statusline. Returns Row 1 + "\n" + Row 2.
171
+ * Caller is responsible for writing to stdout.
172
+ *
173
+ * @param {Object} state
174
+ * @param {number} [columns]
175
+ * @returns {string}
176
+ */
177
+ function renderStatusline(state, columns) {
178
+ return renderRow1(state) + '\n' + renderRow2(state, columns);
179
+ }
180
+
181
+ module.exports = {
182
+ renderRow1,
183
+ renderRow2,
184
+ renderStatusline,
185
+ renderBar,
186
+ };