@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,63 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 121-03 Task 2 -- emit helper for room_receipt_written events (D-09 surface 2).
5
+ *
6
+ * Called from the Phase 119 receipt-write surface (lib/core/room-auto-create.cjs
7
+ * after autoCreatePlaceholderRoom finishes scaffolding the room directory + registry
8
+ * entry + room_auto_created memory_event). One line per receipt-write, routed
9
+ * through writer.emit() (Canon Part 9 chokepoint), validated by Canon Part 8.
10
+ *
11
+ * Per Canon Part 10 sub-claim 3 ("rooms are receipts, not entry points"): every
12
+ * room creation IS a receipt of conversation work. This module emits the
13
+ * trajectory-telemetry counterpart of the room_auto_created memory_event, so
14
+ * SEED-002 consumers can correlate receipt cadence with engagement quality.
15
+ *
16
+ * Non-throwing by contract: validation breaches and disk errors are swallowed
17
+ * so the Phase 119 receipt-write surface is never blocked by telemetry. The
18
+ * pipeline must not crash because telemetry storage is unavailable (mirrors
19
+ * the writer.cjs best-effort silent-swallow policy).
20
+ *
21
+ * Hash discipline:
22
+ * - room_slug_sha256: sha256(slug) -- 64-char hex; if slug falsy, sha256('').
23
+ * - conversation_id_hash: sha256(conversationId) first 16 chars -- shorter hash
24
+ * (matches schema.cjs MAX_CONTEXT_HASH_LEN convention).
25
+ * - generated_at_ts: ISO-8601 timestamp string (Date.toISOString()).
26
+ *
27
+ * Pure CJS, node built-ins only. Zero new runtime deps.
28
+ */
29
+ 'use strict';
30
+
31
+ const crypto = require('node:crypto');
32
+ const telemetry = require('./telemetry/writer.cjs');
33
+
34
+ function _sha256(s) {
35
+ return crypto.createHash('sha256').update(String(s || '')).digest('hex');
36
+ }
37
+
38
+ /**
39
+ * emitReceiptWritten(roomSlug, conversationId) -> void
40
+ *
41
+ * Best-effort emit of one room_receipt_written event. Falsy arguments are
42
+ * defensively normalized to empty-string hashes so the Phase 119 wire-in
43
+ * never blocks on missing scaffolding metadata.
44
+ *
45
+ * @param {string|null|undefined} roomSlug placeholder slug (e.g. 'untitled-2026-05-19-1234')
46
+ * @param {string|null|undefined} conversationId Phase 119 source_material_id or session id
47
+ */
48
+ function emitReceiptWritten(roomSlug, conversationId) {
49
+ try {
50
+ telemetry.emit('room_receipt_written', {
51
+ room_slug_sha256: _sha256(roomSlug),
52
+ conversation_id_hash: _sha256(conversationId).slice(0, 16),
53
+ generated_at_ts: new Date().toISOString(),
54
+ });
55
+ } catch (_e) {
56
+ // Best-effort. Per Canon Part 9 (memory locality) and the writer's
57
+ // silent-swallow policy: telemetry MUST NEVER block the surface it
58
+ // observes. A missing telemetry row is recoverable; a crashed room
59
+ // creation is not.
60
+ }
61
+ }
62
+
63
+ module.exports = { emitReceiptWritten: emitReceiptWritten };
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-02 -- Room skeleton scaffold generator.
4
+ *
5
+ * Fills a placeholder room (created by Plan 119-00's autoCreatePlaceholderRoom)
6
+ * with the canonical 8-section ICM structure + STATE.md + MINTO.md + USER.md +
7
+ * per-directory ROOM.md identity files (Canon decision 15: ICM Layer 0 everywhere).
8
+ *
9
+ * Idempotent + reentrant: never overwrites human-authored content. When STATE.md
10
+ * already has auto_created:false in its frontmatter (OR the frontmatter lacks the
11
+ * auto_created key entirely -- treated as human-authored), the regenerable
12
+ * STATE.md is skipped while the missing identity files are still created
13
+ * (Canon Part 9 "files preserve meaning" invariant).
14
+ *
15
+ * Atomic writes per Phase 124-02 precedent (tmp + rename).
16
+ *
17
+ * Canon Part 8 invariant: pure-local; no Brain MCP, no fetch, no telemetry.
18
+ * Canon Part 9 invariant: no direct room-db.cjs require (this module touches
19
+ * only files; memory_event emission stays in Plan 119-00 and Plan 119-01).
20
+ * Canon Part 10 sub-claim 3: rooms are receipts -- the receipt's substrate
21
+ * materializes the instant the auto-explore finding lands, never gated on
22
+ * material quality (D-05).
23
+ * Canon decision 15: every directory in the Data Room MUST have ROOM.md;
24
+ * the IDENTITY_DIRECTORIES table covers the 5 non-ICM cases.
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const TEMPLATES_DIR = path.resolve(__dirname, '..', '..', 'templates', 'room-skeleton');
31
+
32
+ /**
33
+ * The 8 ICM sections. Lookup table extracted from commands/new-project.md
34
+ * Section Definitions. Single source of truth here; renames cascade.
35
+ */
36
+ const SECTION_NAMES = Object.freeze([
37
+ 'problem-definition',
38
+ 'market-analysis',
39
+ 'solution-design',
40
+ 'business-model',
41
+ 'competitive-analysis',
42
+ 'team-execution',
43
+ 'legal-ip',
44
+ 'financial-model',
45
+ ]);
46
+
47
+ const SECTION_METADATA = Object.freeze({
48
+ 'problem-definition': { purpose: 'Define the core problem this venture addresses.', stage_relevance: ['Pre-Opportunity', 'Discovery'], default_methodologies: ['domain-explorer', 'beautiful-question', 'trending-to-absurd'] },
49
+ 'market-analysis': { purpose: 'Map market size, trends, and customer segments.', stage_relevance: ['Discovery', 'Validation'], default_methodologies: ['domain-explorer', 'scenario-analysis'] },
50
+ 'solution-design': { purpose: 'Design the solution, technology, and architecture.', stage_relevance: ['Validation', 'Design'], default_methodologies: ['structure-argument', 'think-hats'] },
51
+ 'business-model': { purpose: 'Define revenue model, unit economics, go-to-market.', stage_relevance: ['Design', 'Investment'], default_methodologies: ['structure-argument', 'scenario-analysis'] },
52
+ 'competitive-analysis': { purpose: 'Analyze competition, positioning, differentiation.', stage_relevance: ['Discovery', 'Design'], default_methodologies: ['challenge-assumptions', 'find-bottlenecks'] },
53
+ 'team-execution': { purpose: 'Document team, advisors, and execution plan.', stage_relevance: ['Validation', 'Design'], default_methodologies: ['think-hats', 'analyze-needs'] },
54
+ 'legal-ip': { purpose: 'Track legal structure, agreements, IP protection.', stage_relevance: ['Design', 'Investment'], default_methodologies: ['structure-argument'] },
55
+ 'financial-model': { purpose: 'Build financial projections and metrics.', stage_relevance: ['Design', 'Investment'], default_methodologies: ['scenario-analysis', 'build-thesis'] },
56
+ });
57
+
58
+ /**
59
+ * Non-ICM directories that still need ROOM.md per Canon decision 15.
60
+ */
61
+ const IDENTITY_DIRECTORIES = Object.freeze({
62
+ 'team': { directory_type: 'team', purpose: 'The people layer of the Data Room. Members, mentors, advisors -- created on demand from meetings or user input.' },
63
+ 'assets': { directory_type: 'assets', purpose: 'Binary file storage (PDFs, images, videos) organized by section. Subdirectories appear when scripts/file-asset is invoked.' },
64
+ '.intelligence':{ directory_type: '.intelligence', purpose: 'Sentinel-generated alerts and digests (health checks, deadline reports, competitor watch).' },
65
+ '.snapshots': { directory_type: '.snapshots', purpose: 'Weekly STATE.md copies for drift detection by sentinel-health-check.' },
66
+ '.context': { directory_type: '.context', purpose: 'Per-session conversational state (last-session, rejection-log, methodology-history, weekly-digest).' },
67
+ });
68
+
69
+ /**
70
+ * Render a template with {{KEY}} string substitutions. Pure function.
71
+ *
72
+ * @param {string} templateContent the raw template file contents
73
+ * @param {object} substitutions map of KEY -> value (any type coerced via String())
74
+ * @returns {string} the substituted output
75
+ */
76
+ function renderTemplate(templateContent, substitutions) {
77
+ let rendered = templateContent;
78
+ for (const key of Object.keys(substitutions || {})) {
79
+ const re = new RegExp('\\{\\{' + key + '\\}\\}', 'g');
80
+ rendered = rendered.replace(re, String(substitutions[key]));
81
+ }
82
+ return rendered;
83
+ }
84
+
85
+ /**
86
+ * Atomic write: tmp + rename. Returns true on success, false on failure.
87
+ * Mirrors Phase 124-02 timeline-runner.cjs invariant. The tmp file path
88
+ * includes pid + random suffix so concurrent invocations do not collide.
89
+ */
90
+ function atomicWrite(filePath, content) {
91
+ try {
92
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o755 });
93
+ const tmpPath = filePath + '.tmp.' + process.pid + '.' + Math.random().toString(36).slice(2, 10);
94
+ fs.writeFileSync(tmpPath, content, 'utf8');
95
+ fs.renameSync(tmpPath, filePath);
96
+ return true;
97
+ } catch (_e) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ function readTemplate(name) {
103
+ try {
104
+ return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8');
105
+ } catch (_e) {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Detect whether STATE.md has been authored by a human (or by a non-Phase-119
112
+ * source). Returns true if the file:
113
+ * - Exists AND
114
+ * - Has frontmatter AND
115
+ * - Frontmatter contains `auto_created: false` OR lacks the `auto_created` key.
116
+ *
117
+ * Returns false if STATE.md doesn't exist OR has `auto_created: true` (the
118
+ * Phase 119 auto-scaffold signal).
119
+ */
120
+ function isStateAuthored(roomDir) {
121
+ try {
122
+ const statePath = path.join(roomDir, 'STATE.md');
123
+ if (!fs.existsSync(statePath)) return false;
124
+ const raw = fs.readFileSync(statePath, 'utf8');
125
+ const m = raw.match(/^---\n([\s\S]*?)\n---/);
126
+ if (!m) return false;
127
+ // If frontmatter has auto_created: false, treat as authored.
128
+ if (/^\s*auto_created:\s*false\s*$/m.test(m[1])) return true;
129
+ // If frontmatter LACKS auto_created entirely, treat as authored (human
130
+ // crafted from scratch -- the absence of the key is the signal).
131
+ if (!/^\s*auto_created:\s*/m.test(m[1])) return true;
132
+ return false;
133
+ } catch (_e) {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * scaffoldRoomSkeleton(roomDir, opts) -- the entry point.
140
+ *
141
+ * Fills a placeholder room with the canonical skeleton: STATE.md + MINTO.md +
142
+ * USER.md + 8 ICM section folders (each with ROOM.md) + 5 identity directories
143
+ * (each with ROOM.md per Canon decision 15).
144
+ *
145
+ * Idempotent. Reentrancy invariant: existing files are NEVER overwritten;
146
+ * human-authored STATE.md is byte-preserved.
147
+ *
148
+ * @param {string} roomDir absolute path to the placeholder room
149
+ * @param {object} opts
150
+ * @param {string} [opts.placeholder_slug] e.g. 'untitled-2026-05-16-1845'
151
+ * @param {string} [opts.source_material_id] 32-char hex from Phase 117
152
+ * @param {object} [opts.auto_explore_finding] the Phase 117 JSON output (used
153
+ * for the thinness pass-through)
154
+ * @returns {{ok: boolean,
155
+ * sections_created: string[],
156
+ * identity_files_created: string[],
157
+ * state_written: boolean,
158
+ * minto_written: boolean,
159
+ * user_written: boolean,
160
+ * thinness_acknowledged: boolean,
161
+ * errors: string[]}}
162
+ */
163
+ function scaffoldRoomSkeleton(roomDir, opts) {
164
+ const options = opts || {};
165
+ const result = {
166
+ ok: true,
167
+ sections_created: [],
168
+ identity_files_created: [],
169
+ state_written: false,
170
+ minto_written: false,
171
+ user_written: false,
172
+ thinness_acknowledged: false,
173
+ errors: [],
174
+ };
175
+
176
+ if (!roomDir || typeof roomDir !== 'string') {
177
+ result.ok = false;
178
+ result.errors.push('invalid_room_dir');
179
+ return result;
180
+ }
181
+ if (!fs.existsSync(roomDir)) {
182
+ result.ok = false;
183
+ result.errors.push('room_dir_does_not_exist');
184
+ return result;
185
+ }
186
+
187
+ // Substitutions used across templates.
188
+ const subs = {
189
+ AUTO_CREATED_AT_ISO: new Date().toISOString(),
190
+ PLACEHOLDER_SLUG: options.placeholder_slug || path.basename(roomDir),
191
+ SOURCE_MATERIAL_ID: options.source_material_id || 'unknown',
192
+ };
193
+
194
+ const authored = isStateAuthored(roomDir);
195
+
196
+ // Render STATE.md (skip if authored by human).
197
+ if (!authored) {
198
+ const stateTpl = readTemplate('STATE.md.tmpl');
199
+ if (stateTpl) {
200
+ const stateContent = renderTemplate(stateTpl, subs);
201
+ if (atomicWrite(path.join(roomDir, 'STATE.md'), stateContent)) {
202
+ result.state_written = true;
203
+ } else {
204
+ result.errors.push('state_write_failed');
205
+ }
206
+ } else {
207
+ result.errors.push('state_template_missing');
208
+ }
209
+ } else {
210
+ process.stderr.write('[room-skeleton] STATE.md already authored; skipping\n');
211
+ }
212
+
213
+ // Render MINTO.md (skip if file exists -- the sentinel-bounded content is the contract).
214
+ const mintoPath = path.join(roomDir, 'MINTO.md');
215
+ if (!fs.existsSync(mintoPath)) {
216
+ const mintoTpl = readTemplate('MINTO.md.tmpl');
217
+ if (mintoTpl) {
218
+ if (atomicWrite(mintoPath, mintoTpl)) {
219
+ result.minto_written = true;
220
+ } else {
221
+ result.errors.push('minto_write_failed');
222
+ }
223
+ } else {
224
+ result.errors.push('minto_template_missing');
225
+ }
226
+ }
227
+
228
+ // Render USER.md (skip if exists).
229
+ const userPath = path.join(roomDir, 'USER.md');
230
+ if (!fs.existsSync(userPath)) {
231
+ const userTpl = readTemplate('USER.md.tmpl');
232
+ if (userTpl) {
233
+ if (atomicWrite(userPath, userTpl)) {
234
+ result.user_written = true;
235
+ } else {
236
+ result.errors.push('user_write_failed');
237
+ }
238
+ } else {
239
+ result.errors.push('user_template_missing');
240
+ }
241
+ }
242
+
243
+ // 8 ICM sections with per-section ROOM.md.
244
+ const sectionTpl = readTemplate('ROOM.md.section.tmpl');
245
+ if (sectionTpl) {
246
+ for (const section of SECTION_NAMES) {
247
+ const sectionDir = path.join(roomDir, section);
248
+ const sectionRoomMd = path.join(sectionDir, 'ROOM.md');
249
+ if (!fs.existsSync(sectionRoomMd)) {
250
+ const meta = SECTION_METADATA[section];
251
+ const titleCase = section.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
252
+ const sectionSubs = {
253
+ SECTION_NAME: section,
254
+ SECTION_NAME_TITLE_CASE: titleCase,
255
+ SECTION_PURPOSE: meta.purpose,
256
+ STAGE_RELEVANCE_LIST: meta.stage_relevance.map(s => ' - ' + s).join('\n'),
257
+ DEFAULT_METHODOLOGIES_LIST: meta.default_methodologies.map(m => ' - ' + m).join('\n'),
258
+ };
259
+ const sectionContent = renderTemplate(sectionTpl, sectionSubs);
260
+ if (atomicWrite(sectionRoomMd, sectionContent)) {
261
+ result.sections_created.push(section);
262
+ } else {
263
+ result.errors.push('section_write_failed:' + section);
264
+ }
265
+ }
266
+ }
267
+ } else {
268
+ result.errors.push('section_template_missing');
269
+ }
270
+
271
+ // Non-ICM identity directories with ROOM.md (Canon decision 15).
272
+ const identityTpl = readTemplate('ROOM.md.identity.tmpl');
273
+ if (identityTpl) {
274
+ for (const dirName of Object.keys(IDENTITY_DIRECTORIES)) {
275
+ const dirPath = path.join(roomDir, dirName);
276
+ const identityRoomMd = path.join(dirPath, 'ROOM.md');
277
+ if (!fs.existsSync(identityRoomMd)) {
278
+ const meta = IDENTITY_DIRECTORIES[dirName];
279
+ const identitySubs = {
280
+ DIRECTORY_TYPE: meta.directory_type,
281
+ DIRECTORY_PURPOSE: meta.purpose,
282
+ };
283
+ const identityContent = renderTemplate(identityTpl, identitySubs);
284
+ if (atomicWrite(identityRoomMd, identityContent)) {
285
+ result.identity_files_created.push(dirName);
286
+ }
287
+ }
288
+ }
289
+ } else {
290
+ result.errors.push('identity_template_missing');
291
+ }
292
+
293
+ // Thinness pass-through (Plan 119-01 reads result.thinness_acknowledged
294
+ // to decide whether to render the Larry voice line at F.1 selector time).
295
+ try {
296
+ const tha = require('./larry-thinness-acknowledgment.cjs');
297
+ result.thinness_acknowledged = tha.shouldAcknowledgeThinness(options.auto_explore_finding || null);
298
+ } catch (_e) {
299
+ result.thinness_acknowledged = true; // fail-safe: default to acknowledging thinness
300
+ }
301
+
302
+ // ok is true if no errors OR if at least the regenerable surface (state or
303
+ // sections) made it through. Errors are still recorded for observability.
304
+ result.ok = result.errors.length === 0;
305
+ return result;
306
+ }
307
+
308
+ module.exports = {
309
+ SECTION_NAMES: SECTION_NAMES,
310
+ SECTION_METADATA: SECTION_METADATA,
311
+ IDENTITY_DIRECTORIES: IDENTITY_DIRECTORIES,
312
+ scaffoldRoomSkeleton: scaffoldRoomSkeleton,
313
+ renderTemplate: renderTemplate,
314
+ isStateAuthored: isStateAuthored,
315
+ };
@@ -0,0 +1,291 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-02 Task 2 -- test suite for room-skeleton-scaffold.cjs.
4
+ *
5
+ * Verifies the 14 behaviors from
6
+ * .planning/phases/119-room-as-receipt-invariant/119-02-PLAN.md Task 2.
7
+ *
8
+ * Each test uses an isolated tmp roomDir via os.tmpdir() + crypto-random
9
+ * suffix; tear-down via fs.rmSync(.., {recursive:true, force:true}).
10
+ *
11
+ * Canon Part 8 invariant: zero Brain MCP coupling (Test 12).
12
+ * Canon Part 9 invariant: zero room-db.cjs require (Test 13 -- this module
13
+ * writes files only; memory_event emission stays in Plan 119-00 + 119-01).
14
+ * Em-dash HARD RULE: zero `--` chars in source (Test 14).
15
+ */
16
+
17
+ const assert = require('node:assert');
18
+ const test = require('node:test');
19
+ const fs = require('node:fs');
20
+ const os = require('node:os');
21
+ const path = require('node:path');
22
+ const crypto = require('node:crypto');
23
+
24
+ const {
25
+ scaffoldRoomSkeleton,
26
+ SECTION_NAMES,
27
+ SECTION_METADATA,
28
+ IDENTITY_DIRECTORIES,
29
+ renderTemplate,
30
+ isStateAuthored,
31
+ } = require('./room-skeleton-scaffold.cjs');
32
+
33
+ const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
34
+
35
+ function makeTmpRoom() {
36
+ const dir = path.join(os.tmpdir(), 'phase119-02-' + crypto.randomBytes(4).toString('hex'));
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
39
+ // Mark as placeholder room (matches Plan 119-00 contract).
40
+ fs.writeFileSync(path.join(dir, '.room-root'), '');
41
+ return dir;
42
+ }
43
+
44
+ function rmTmp(dir) {
45
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
46
+ }
47
+
48
+ test('Test 1: 8 ICM sections present after scaffolding', () => {
49
+ const roomDir = makeTmpRoom();
50
+ try {
51
+ const r = scaffoldRoomSkeleton(roomDir, {});
52
+ assert.strictEqual(r.ok, true, 'scaffold did not return ok');
53
+ const expectedSections = [
54
+ 'problem-definition', 'market-analysis', 'solution-design', 'business-model',
55
+ 'competitive-analysis', 'team-execution', 'legal-ip', 'financial-model',
56
+ ];
57
+ for (const section of expectedSections) {
58
+ const sectionDir = path.join(roomDir, section);
59
+ const roomMd = path.join(sectionDir, 'ROOM.md');
60
+ assert.ok(fs.existsSync(sectionDir), `missing section dir: ${section}`);
61
+ assert.ok(fs.existsSync(roomMd), `missing ROOM.md in ${section}/`);
62
+ }
63
+ } finally { rmTmp(roomDir); }
64
+ });
65
+
66
+ test('Test 2: non-ICM identity directories present with ROOM.md (Canon decision 15)', () => {
67
+ const roomDir = makeTmpRoom();
68
+ try {
69
+ scaffoldRoomSkeleton(roomDir, {});
70
+ const identityDirs = ['team', 'assets', '.intelligence', '.snapshots', '.context'];
71
+ for (const d of identityDirs) {
72
+ const dirPath = path.join(roomDir, d);
73
+ const roomMd = path.join(dirPath, 'ROOM.md');
74
+ assert.ok(fs.existsSync(dirPath), `missing identity dir: ${d}`);
75
+ assert.ok(fs.existsSync(roomMd), `missing ROOM.md in ${d}/`);
76
+ }
77
+ } finally { rmTmp(roomDir); }
78
+ });
79
+
80
+ test('Test 3: STATE.md rendered with substitutions', () => {
81
+ const roomDir = makeTmpRoom();
82
+ try {
83
+ scaffoldRoomSkeleton(roomDir, {
84
+ placeholder_slug: 'untitled-test-slug',
85
+ source_material_id: 'deadbeefcafef00d',
86
+ });
87
+ const state = fs.readFileSync(path.join(roomDir, 'STATE.md'), 'utf8');
88
+ assert.match(state, /phase: scoping/, 'phase scoping not rendered');
89
+ assert.match(state, /journey_stage: ordinary-world/, 'journey_stage not rendered');
90
+ assert.match(state, /placeholder_slug: 'untitled-test-slug'/, 'placeholder_slug substitution failed');
91
+ assert.match(state, /source_material_id: 'deadbeefcafef00d'/, 'source_material_id substitution failed');
92
+ } finally { rmTmp(roomDir); }
93
+ });
94
+
95
+ test('Test 4: MINTO.md is sentinel-bounded', () => {
96
+ const roomDir = makeTmpRoom();
97
+ try {
98
+ scaffoldRoomSkeleton(roomDir, {});
99
+ const minto = fs.readFileSync(path.join(roomDir, 'MINTO.md'), 'utf8');
100
+ assert.ok(minto.includes('<!-- mos:minto-begin -->'), 'missing minto-begin sentinel');
101
+ assert.ok(minto.includes('<!-- mos:minto-end -->'), 'missing minto-end sentinel');
102
+ } finally { rmTmp(roomDir); }
103
+ });
104
+
105
+ test('Test 5: USER.md present with non-zero size', () => {
106
+ const roomDir = makeTmpRoom();
107
+ try {
108
+ scaffoldRoomSkeleton(roomDir, {});
109
+ const userPath = path.join(roomDir, 'USER.md');
110
+ assert.ok(fs.existsSync(userPath), 'USER.md not created');
111
+ const stat = fs.statSync(userPath);
112
+ assert.ok(stat.size > 0, 'USER.md is empty');
113
+ } finally { rmTmp(roomDir); }
114
+ });
115
+
116
+ test('Test 6: per-section ROOM.md has YAML frontmatter with required fields', () => {
117
+ const roomDir = makeTmpRoom();
118
+ try {
119
+ scaffoldRoomSkeleton(roomDir, {});
120
+ const content = fs.readFileSync(path.join(roomDir, 'problem-definition', 'ROOM.md'), 'utf8');
121
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
122
+ assert.ok(m, 'no frontmatter delimiter');
123
+ const fm = m[1];
124
+ assert.match(fm, /section: problem-definition/, 'section field missing');
125
+ assert.match(fm, /purpose:/, 'purpose field missing');
126
+ assert.match(fm, /stage_relevance:/, 'stage_relevance missing');
127
+ assert.match(fm, /default_methodologies:/, 'default_methodologies missing');
128
+ assert.match(fm, /icm_layer: 0/, 'icm_layer 0 missing');
129
+ assert.match(fm, /auto_scaffolded: true/, 'auto_scaffolded missing');
130
+ } finally { rmTmp(roomDir); }
131
+ });
132
+
133
+ test('Test 7: each of 8 sections has section-matching ROOM.md identity', () => {
134
+ const roomDir = makeTmpRoom();
135
+ try {
136
+ scaffoldRoomSkeleton(roomDir, {});
137
+ for (const section of SECTION_NAMES) {
138
+ const content = fs.readFileSync(path.join(roomDir, section, 'ROOM.md'), 'utf8');
139
+ assert.match(content, new RegExp('section: ' + section), `wrong section identity in ${section}/ROOM.md`);
140
+ }
141
+ } finally { rmTmp(roomDir); }
142
+ });
143
+
144
+ test('Test 8: REENTRANCY -- human-authored STATE.md preserved across re-invocation', () => {
145
+ const roomDir = makeTmpRoom();
146
+ try {
147
+ // Pre-author STATE.md with auto_created: false (human signal).
148
+ const humanState = '---\nphase: validation\nauto_created: false\nventure_name: my-venture\n---\n\n# Human STATE\n';
149
+ fs.writeFileSync(path.join(roomDir, 'STATE.md'), humanState);
150
+ const before = fs.readFileSync(path.join(roomDir, 'STATE.md'), 'utf8');
151
+
152
+ // Capture stderr to verify the skip message.
153
+ const origWrite = process.stderr.write.bind(process.stderr);
154
+ let stderrCapture = '';
155
+ process.stderr.write = (chunk) => { stderrCapture += String(chunk); return true; };
156
+ try {
157
+ const r = scaffoldRoomSkeleton(roomDir, {});
158
+ const after = fs.readFileSync(path.join(roomDir, 'STATE.md'), 'utf8');
159
+ assert.strictEqual(after, before, 'human-authored STATE.md was overwritten');
160
+ assert.strictEqual(r.state_written, false, 'state_written should be false on skip path');
161
+ assert.ok(stderrCapture.includes('STATE.md already authored'), 'expected skip message on stderr');
162
+ // Sections + identity files STILL created (idempotent fill).
163
+ assert.ok(fs.existsSync(path.join(roomDir, 'problem-definition', 'ROOM.md')), 'sections not filled on re-entry');
164
+ } finally {
165
+ process.stderr.write = origWrite;
166
+ }
167
+ } finally { rmTmp(roomDir); }
168
+ });
169
+
170
+ test('Test 9: atomic-write idiom -- tmp file removed on success (no leftover .tmp files)', () => {
171
+ const roomDir = makeTmpRoom();
172
+ try {
173
+ scaffoldRoomSkeleton(roomDir, {});
174
+ // Walk roomDir; no .tmp.* files should remain.
175
+ const walk = (dir) => {
176
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
177
+ const results = [];
178
+ for (const e of entries) {
179
+ const p = path.join(dir, e.name);
180
+ if (e.isDirectory()) results.push(...walk(p));
181
+ else results.push(p);
182
+ }
183
+ return results;
184
+ };
185
+ const files = walk(roomDir);
186
+ const leftovers = files.filter(f => /\.tmp\./.test(f));
187
+ assert.strictEqual(leftovers.length, 0, `leftover tmp files: ${leftovers.join(',')}`);
188
+ } finally { rmTmp(roomDir); }
189
+ });
190
+
191
+ test('Test 10: return shape has all expected fields', () => {
192
+ const roomDir = makeTmpRoom();
193
+ try {
194
+ const r = scaffoldRoomSkeleton(roomDir, {});
195
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'ok'));
196
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'sections_created'));
197
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'identity_files_created'));
198
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'state_written'));
199
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'minto_written'));
200
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'user_written'));
201
+ assert.ok(Object.prototype.hasOwnProperty.call(r, 'thinness_acknowledged'));
202
+ assert.ok(Array.isArray(r.sections_created));
203
+ assert.ok(Array.isArray(r.identity_files_created));
204
+ assert.strictEqual(typeof r.state_written, 'boolean');
205
+ assert.strictEqual(typeof r.minto_written, 'boolean');
206
+ assert.strictEqual(typeof r.user_written, 'boolean');
207
+ assert.strictEqual(typeof r.thinness_acknowledged, 'boolean');
208
+ } finally { rmTmp(roomDir); }
209
+ });
210
+
211
+ test('Test 11: thinness pass-through -- null finding => thinness_acknowledged true', () => {
212
+ const roomDir = makeTmpRoom();
213
+ try {
214
+ const r = scaffoldRoomSkeleton(roomDir, { auto_explore_finding: null });
215
+ assert.strictEqual(r.thinness_acknowledged, true);
216
+ } finally { rmTmp(roomDir); }
217
+
218
+ const roomDir2 = makeTmpRoom();
219
+ try {
220
+ const substantive = { findings: [{ source_pipeline: 'whitespace', hsi_score: 0.7 }, { source_pipeline: 'cross-domain', hsi_score: 0.6 }] };
221
+ const r = scaffoldRoomSkeleton(roomDir2, { auto_explore_finding: substantive });
222
+ assert.strictEqual(r.thinness_acknowledged, false);
223
+ } finally { rmTmp(roomDir2); }
224
+ });
225
+
226
+ test('Test 12: no Brain MCP coupling in scaffold module', () => {
227
+ const src = fs.readFileSync(path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.cjs'), 'utf8');
228
+ assert.ok(!/require\([^)]*brain-client/.test(src), 'brain-client require found');
229
+ assert.ok(!/fetch\([^)]*brain\.mindrian/.test(src), 'brain.mindrian fetch found');
230
+ });
231
+
232
+ test('Test 13: no direct room-db.cjs require (Canon Part 9 chokepoint preserved)', () => {
233
+ const src = fs.readFileSync(path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.cjs'), 'utf8');
234
+ assert.ok(!/require\([^)]*room-db\.cjs/.test(src), 'room-db.cjs require found');
235
+ });
236
+
237
+ test('Test 14: em-dash invariant -- zero U+2014 in source files', () => {
238
+ // Construct the em-dash via charCode so this test file itself does not
239
+ // contain the literal char (which would self-trip the invariant).
240
+ const EMDASH = String.fromCharCode(0x2014);
241
+ const sources = [
242
+ path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.cjs'),
243
+ path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.test.cjs'),
244
+ ];
245
+ for (const file of sources) {
246
+ const src = fs.readFileSync(file, 'utf8');
247
+ assert.ok(!src.includes(EMDASH), 'em-dash U+2014 found in ' + file);
248
+ }
249
+ });
250
+
251
+ test('Bonus Test 15: SECTION_NAMES has exactly 8 entries; IDENTITY_DIRECTORIES has exactly 5', () => {
252
+ assert.strictEqual(SECTION_NAMES.length, 8, 'SECTION_NAMES not 8');
253
+ assert.strictEqual(Object.keys(IDENTITY_DIRECTORIES).length, 5, 'IDENTITY_DIRECTORIES not 5');
254
+ });
255
+
256
+ test('Bonus Test 16: renderTemplate substitutes {{KEY}} tokens correctly', () => {
257
+ const out = renderTemplate('hello {{NAME}}, you are {{ROLE}}.', { NAME: 'Larry', ROLE: 'pedagogical guide' });
258
+ assert.strictEqual(out, 'hello Larry, you are pedagogical guide.');
259
+ });
260
+
261
+ test('Bonus Test 17: isStateAuthored detects auto_created:false in frontmatter', () => {
262
+ const roomDir = makeTmpRoom();
263
+ try {
264
+ // auto_created:true => not authored (auto-scaffold output)
265
+ fs.writeFileSync(path.join(roomDir, 'STATE.md'), '---\nphase: scoping\nauto_created: true\n---\n');
266
+ assert.strictEqual(isStateAuthored(roomDir), false);
267
+
268
+ // auto_created:false => authored (human override)
269
+ fs.writeFileSync(path.join(roomDir, 'STATE.md'), '---\nphase: validation\nauto_created: false\n---\n');
270
+ assert.strictEqual(isStateAuthored(roomDir), true);
271
+
272
+ // No auto_created key => treat as authored (human authored from scratch)
273
+ fs.writeFileSync(path.join(roomDir, 'STATE.md'), '---\nphase: validation\n---\n');
274
+ assert.strictEqual(isStateAuthored(roomDir), true);
275
+ } finally { rmTmp(roomDir); }
276
+ });
277
+
278
+ test('Bonus Test 18: invalid roomDir returns ok:false with error', () => {
279
+ const r = scaffoldRoomSkeleton('/this/path/definitely/does/not/exist/xyzpdq', {});
280
+ assert.strictEqual(r.ok, false);
281
+ assert.ok(r.errors.includes('room_dir_does_not_exist'));
282
+ });
283
+
284
+ test('Bonus Test 19: SECTION_METADATA covers all 8 sections', () => {
285
+ for (const section of SECTION_NAMES) {
286
+ assert.ok(SECTION_METADATA[section], `missing metadata for ${section}`);
287
+ assert.ok(SECTION_METADATA[section].purpose, `missing purpose for ${section}`);
288
+ assert.ok(Array.isArray(SECTION_METADATA[section].stage_relevance), `stage_relevance not array for ${section}`);
289
+ assert.ok(Array.isArray(SECTION_METADATA[section].default_methodologies), `default_methodologies not array for ${section}`);
290
+ }
291
+ });
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Why this module is the hardest Canon Part 8 surface in v1.11.0:
12
12
  *
13
- * The Brain (brain.mindrian.ai) is a generic methodology repository
13
+ * The Brain (mindrian-brain.onrender.com) is a generic methodology repository
14
14
  * that MUST never receive user content. Every other 89.x callsite
15
15
  * builds Brain queries from STRUCTURED inputs (problem_type, framework
16
16
  * id, phase id) where the audit surface is a finite enum. This module