@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,190 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * lib/core/stale-copy-scanner.cjs
6
+ * -------------------------------
7
+ * Phase 121.5-05 Sub-plan F Task 2 (SEED-007 absorption).
8
+ *
9
+ * Pure CJS module that scans the first-touch surfaces declared in
10
+ * data/first-touch-surfaces.json for two regression patterns:
11
+ *
12
+ * - stale_version_hardcode a literal `vX.Y.Z` (or `X.Y.Z`) in greeting
13
+ * copy that is OLDER than the running plugin
14
+ * version (semver compare). Source finding
15
+ * (SEED-007, 2026-05-09): a v1.12.0 greeting
16
+ * was rendering on a stale Windows install that
17
+ * had been bumped to v1.13.0.
18
+ * - em_dash_violation any U+2014 EM DASH in a surface that has
19
+ * em_dash_check: true. Enforces the
20
+ * feedback_no_emdashes hard rule.
21
+ *
22
+ * Per-surface invariants come from data/first-touch-surfaces.json:
23
+ * - version_stamp_required: false -> skip stale_version_hardcode
24
+ * - em_dash_check: false -> skip em_dash_violation
25
+ * - allowed_hardcoded_versions[] -> whitelist of strings exempt from
26
+ * stale_version_hardcode (template
27
+ * tokens, doc examples)
28
+ *
29
+ * Wired into scripts/doctor.cjs as class K (stale-first-touch-copy).
30
+ *
31
+ * Canon Part 7 (Reuse Before Build): consolidates the SEED-007 detection
32
+ * responsibility previously scattered across scripts/check-first-touch-drift.cjs
33
+ * (which exists but is partial / not exposed in /mos:doctor) and the manual
34
+ * audit pattern. ZERO new commands; class K extends the existing class A-J
35
+ * doctor pattern.
36
+ *
37
+ * Canon Part 8: local-only; no fetch, no Brain, no telemetry egress.
38
+ *
39
+ * Note: this plan was scoped against class H in 121.5-05-PLAN.md, but class
40
+ * H was already taken (install-incomplete: missing statusLine block + halted
41
+ * .install-receipt.json) at the time of execution. Renamed to class K (next
42
+ * available after I + J) per deviation Rule 3 (auto-fix blocking issue:
43
+ * naming collision). The behavior contract from the plan is preserved
44
+ * byte-identical; only the class letter changed.
45
+ *
46
+ * No emoji. No em-dashes. ASCII-clean source.
47
+ */
48
+
49
+ 'use strict';
50
+
51
+ const fs = require('fs');
52
+ const path = require('path');
53
+ const {
54
+ readSurfaces,
55
+ resolveCurrentVersion,
56
+ hasEmDash,
57
+ } = require('./first-touch-version-stamper.cjs');
58
+
59
+ const REPO_ROOT = path.join(__dirname, '..', '..');
60
+
61
+ // vX.Y.Z or X.Y.Z (optional -<prerelease> tail). Captures the X.Y.Z core in
62
+ // group 1 (without the leading v) for semver compare. Matches:
63
+ // v1.13.0 -> 1.13.0
64
+ // 1.13.0 -> 1.13.0
65
+ // v1.13.0-beta.15 -> 1.13.0-beta.15
66
+ const VERSION_LITERAL_RE = /\bv?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)\b/g;
67
+
68
+ // compareSemver(a, b) returns negative if a < b, positive if a > b, 0 if
69
+ // equal. Loose splitter (numeric-aware for X.Y.Z core; lexical for the
70
+ // prerelease tail). Sufficient for the SEED-007 detection pattern (older
71
+ // X.Y.Z than current); does not aim to match the full semver 2.0 spec.
72
+ function compareSemver(a, b) {
73
+ const parseLoose = (s) => String(s)
74
+ .replace(/^v/, '')
75
+ .split(/[.-]/)
76
+ .map((p) => (/^\d+$/.test(p) ? parseInt(p, 10) : p));
77
+ const A = parseLoose(a);
78
+ const B = parseLoose(b);
79
+ const len = Math.max(A.length, B.length);
80
+ for (let i = 0; i < len; i++) {
81
+ const x = A[i];
82
+ const y = B[i];
83
+ if (x === undefined) return -1;
84
+ if (y === undefined) return 1;
85
+ if (typeof x === 'number' && typeof y === 'number') {
86
+ if (x !== y) return x - y;
87
+ continue;
88
+ }
89
+ // Mixed-type or string compare: stringwise.
90
+ const sx = String(x);
91
+ const sy = String(y);
92
+ if (sx !== sy) return sx < sy ? -1 : 1;
93
+ }
94
+ return 0;
95
+ }
96
+
97
+ // scanSurface(surface, currentVersion, opts?) -> violations[]
98
+ // opts.repoRoot can override the file root for tests / fixtures.
99
+ function scanSurface(surface, currentVersion, opts) {
100
+ const violations = [];
101
+ const repoRoot = (opts && opts.repoRoot) || REPO_ROOT;
102
+ const filePath = path.isAbsolute(surface.file)
103
+ ? surface.file
104
+ : path.join(repoRoot, surface.file);
105
+ let content;
106
+ try {
107
+ content = fs.readFileSync(filePath, 'utf8');
108
+ } catch (_e) {
109
+ // Missing surface file is NOT a violation (per scripts/check-first-touch-
110
+ // drift.cjs convention -- missing is warn-only, not hard fail). Tests
111
+ // explicitly cover the present-but-stale + present-but-em-dashed paths.
112
+ return violations;
113
+ }
114
+ if (surface.em_dash_check && hasEmDash(content)) {
115
+ violations.push({
116
+ surface: surface.id,
117
+ file: surface.file,
118
+ kind: 'em_dash_violation',
119
+ });
120
+ }
121
+ if (surface.version_stamp_required !== false) {
122
+ const allowed = new Set(surface.allowed_hardcoded_versions || []);
123
+ VERSION_LITERAL_RE.lastIndex = 0;
124
+ let m;
125
+ while ((m = VERSION_LITERAL_RE.exec(content)) !== null) {
126
+ const found = m[1];
127
+ // Allowlist check: exact match against the captured X.Y.Z(-prerelease)
128
+ // core OR against the original literal token (with optional 'v' prefix).
129
+ if (allowed.has(found) || allowed.has('v' + found) || allowed.has(m[0])) continue;
130
+ // Anything OLDER than the running plugin version is stale -- that is
131
+ // the SEED-007 trigger. Equal or newer is fine.
132
+ if (compareSemver(found, currentVersion) < 0) {
133
+ violations.push({
134
+ surface: surface.id,
135
+ file: surface.file,
136
+ kind: 'stale_version_hardcode',
137
+ found,
138
+ current: currentVersion,
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return violations;
144
+ }
145
+
146
+ // scanForStaleCopy(opts?) -> {valid, violations, scanned_count, current_version}
147
+ // opts.surfaces overrides the default surfaces[] from data/first-touch-surfaces.json.
148
+ // opts.currentVersion overrides the auto-resolved plugin.json version.
149
+ // opts.repoRoot routes relative paths against an override (for tests).
150
+ function scanForStaleCopy(opts) {
151
+ opts = opts || {};
152
+ const surfaces = opts.surfaces ? opts.surfaces : readSurfaces().surfaces;
153
+ const currentVersion = opts.currentVersion || resolveCurrentVersion();
154
+ const violations = [];
155
+ for (const s of surfaces) {
156
+ violations.push(...scanSurface(s, currentVersion, { repoRoot: opts.repoRoot }));
157
+ }
158
+ return {
159
+ valid: violations.length === 0,
160
+ violations,
161
+ scanned_count: surfaces.length,
162
+ current_version: currentVersion,
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ scanForStaleCopy,
168
+ scanSurface,
169
+ compareSemver,
170
+ VERSION_LITERAL_RE,
171
+ };
172
+
173
+ // CLI invocation: `node lib/core/stale-copy-scanner.cjs` runs the scanner
174
+ // against the live repo and prints a summary. Exit 0 clean / Exit 1 violations.
175
+ if (require.main === module) {
176
+ const result = scanForStaleCopy();
177
+ if (result.valid) {
178
+ process.stdout.write('stale-copy-scanner: clean (' + result.scanned_count + ' surfaces scanned, current v' + result.current_version + ')\n');
179
+ process.exit(0);
180
+ }
181
+ process.stdout.write('stale-copy-scanner: ' + result.violations.length + ' violation(s) (current v' + result.current_version + ')\n');
182
+ for (const v of result.violations) {
183
+ if (v.kind === 'stale_version_hardcode') {
184
+ process.stdout.write(' ' + v.surface + ' (' + v.file + ') -- stale version literal "' + v.found + '" (current ' + v.current + ')\n');
185
+ } else if (v.kind === 'em_dash_violation') {
186
+ process.stdout.write(' ' + v.surface + ' (' + v.file + ') -- em-dash (U+2014) in greeting surface\n');
187
+ }
188
+ }
189
+ process.exit(1);
190
+ }
@@ -0,0 +1,78 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ * Phase 121.5-08 (Sub-plan J) -- /mos:mos state-aware router.
4
+ *
5
+ * D-10 LOCKED: /mos:mos picks the right surface based on the navigator's
6
+ * current room state. The user types /mos:mos and the router meets them
7
+ * where they are.
8
+ *
9
+ * No room exists -> /mos:onboard
10
+ * Room exists but mostly empty -> /mos:status (+ suggest next move)
11
+ * ("mostly empty" = sectionsCount == 0 OR stage matches /Pre-Opportunity/i)
12
+ * Room populated (sections >= 1, stage past Pre-Opportunity)
13
+ * -> /mos:suggest-next
14
+ *
15
+ * The router is a PURE function. Reading STATE.md / the registry is done
16
+ * upstream by commands/mos.md; the router operates on the resolved
17
+ * roomState object only. This keeps the routing logic hermetic and
18
+ * unit-testable without any fs / spawn.
19
+ *
20
+ * Canon Part 3: /mos:mos is itself a Decision Gate at the meta level --
21
+ * the router writes a typed (navigator) -[ROUTED_VIA {target, reason}]->
22
+ * (next-surface) edge upstream when the routing decision is committed
23
+ * by commands/mos.md. The router function here only RESOLVES the target;
24
+ * edge emission is the caller's responsibility.
25
+ *
26
+ * Canon Part 7: ONE function replaces the prior "pick the right /mos:*
27
+ * surface for me" mental load on the user. NO new command behaviors
28
+ * are added -- /mos:mos delegates to /mos:onboard / /mos:status /
29
+ * /mos:suggest-next which already exist.
30
+ *
31
+ * Canon Part 8: zero Brain calls, zero network, zero side-channel writes.
32
+ */
33
+ 'use strict';
34
+
35
+ /**
36
+ * Resolve which canonical surface to route to given the current room state.
37
+ *
38
+ * @param {{
39
+ * roomState?: {
40
+ * exists?: boolean, // does the room directory exist?
41
+ * sectionsCount?: number, // how many section folders are populated?
42
+ * stage?: string, // venture stage from STATE.md (or empty)
43
+ * }
44
+ * }} opts
45
+ * @returns {{
46
+ * route: string, // '/mos:onboard' | '/mos:status' | '/mos:suggest-next'
47
+ * reason: string, // 'no_room' | 'mostly_empty' | 'populated'
48
+ * addendum: string | null, // optional follow-on hint (e.g. 'suggest next move')
49
+ * }}
50
+ */
51
+ function resolveNextSurface(opts) {
52
+ const rs = (opts && opts.roomState) || {};
53
+
54
+ // Branch 1: no room exists at all -- the new navigator path.
55
+ if (!rs.exists) {
56
+ return { route: '/mos:onboard', reason: 'no_room', addendum: null };
57
+ }
58
+
59
+ // Branch 2: room exists but is mostly empty. Two signals:
60
+ // (a) sectionsCount == 0 -- the navigator has not populated any section.
61
+ // (b) stage matches Pre-Opportunity (case-insensitive) -- the venture
62
+ // has not even crossed the Opportunity threshold yet.
63
+ // Either signal routes to /mos:status with a next-move addendum.
64
+ const sections = typeof rs.sectionsCount === 'number' ? rs.sectionsCount : 0;
65
+ const stage = typeof rs.stage === 'string' ? rs.stage : '';
66
+ if (sections === 0 || /Pre-Opportunity/i.test(stage)) {
67
+ return {
68
+ route: '/mos:status',
69
+ reason: 'mostly_empty',
70
+ addendum: 'suggest next move',
71
+ };
72
+ }
73
+
74
+ // Branch 3: room is populated -- the navigator wants the next move.
75
+ return { route: '/mos:suggest-next', reason: 'populated', addendum: null };
76
+ }
77
+
78
+ module.exports = { resolveNextSurface };
@@ -0,0 +1,168 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 121-00 -- frozen v1 telemetry schema.
5
+ *
6
+ * Source of truth for the unified trajectory-telemetry stream at
7
+ * ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl
8
+ *
9
+ * Per Canon D-10 (frozen v1 schema with per-row schema_version):
10
+ * - EVENT_TYPES is Object.frozen and locked at 15 entries for v1.
11
+ * - ALLOWED_FIELDS is a frozen, per-event whitelist of scalar field names.
12
+ * - SCHEMA_VERSION is a literal Number (not String) so consumers can
13
+ * dispatch on version with a strict-equal check.
14
+ * - Future structural changes ship as SCHEMA_VERSION 2 events that
15
+ * coexist in the same JSONL stream; v1 stays additive-only.
16
+ *
17
+ * Per Canon Part 8 (Graph Boundary):
18
+ * - String-length caps prevent raw artifact bytes from sneaking through
19
+ * allowed fields. emit() applies them; this module declares them.
20
+ * - The 9 net-new event types (D-04..D-09) only allow scalar fields:
21
+ * hashes (sha256), enum strings, counts, scores, timestamps.
22
+ * There is no field on any event that intentionally carries free-text.
23
+ *
24
+ * Inheritance: the 6 mva.* event types mirror lib/core/mva-telemetry.cjs
25
+ * ALLOWED_FIELDS verbatim. Per D-08, mva-telemetry.cjs becomes a thin shim
26
+ * delegating to the unified writer in a downstream plan; v1 keeps both
27
+ * call paths byte-identical on the wire so the shim is a no-op cut-over.
28
+ *
29
+ * Pure CJS, node built-ins only. Zero runtime deps. Zero network surface.
30
+ */
31
+ 'use strict';
32
+
33
+ // ---------- Frozen v1 schema version ----------
34
+
35
+ const SCHEMA_VERSION = 1;
36
+
37
+ // ---------- Frozen invariants (15 event types) ----------
38
+
39
+ const EVENT_TYPES = Object.freeze([
40
+ // 6 inherited from lib/core/mva-telemetry.cjs (Phase 118):
41
+ 'mva_pipeline_started',
42
+ 'mva_agent_returned',
43
+ 'mva_brief_rendered',
44
+ 'mva_option_selected',
45
+ 'mva_brief_deployed',
46
+ 'mva_pipeline_failed',
47
+ // 9 net-new for the unified Phase 121 surface (D-04..D-09):
48
+ 'selector_pick', // D-04: F-shape selector picks (88.2 + 125 ranker)
49
+ 'tension_engagement', // D-05: tension hook user response (116)
50
+ 'auto_explore_decision', // D-06: auto-explore acceptance (117)
51
+ 'breakthrough_dismissed',// D-07: F.7 + ethics-tier dismissal (120)
52
+ 'hooked_axis_score', // D-08: Hooked axis re-score (117 scripts)
53
+ 'empathy_observation', // D-09: empathy audit harness observation
54
+ 'room_receipt_written', // D-09: room-as-receipt write (119)
55
+ 'command_invocation', // D-09: PostToolUse /mos:* sweep
56
+ 'nav_bypass', // D-09: navigation.cjs bypass migration source
57
+ ]);
58
+
59
+ // ---------- Per-event ALLOWED_FIELDS (frozen arrays) ----------
60
+ //
61
+ // Every entry is a closed whitelist. emit() rejects any payload key that is
62
+ // not in this list. Adding a field to v1 requires:
63
+ // 1. amending this list (additive only)
64
+ // 2. a Canon Part 8 review of the new field
65
+ // 3. matching frontmatter update in the consuming plan's CONTEXT.md
66
+ // Structural changes (renaming, removing, re-typing a field) require a
67
+ // SCHEMA_VERSION bump to 2 + a separate plan that introduces the v2 event.
68
+ //
69
+ // Field-naming convention:
70
+ // *_sha256 -- 64-char hex hash, exempt from raw-hex detector
71
+ // *_hash -- shorter hash (8-32 chars), also hash-class
72
+ // *_score -- numeric, no length cap concern
73
+ // ttr_seconds -- numeric (time-to-response in seconds)
74
+ // ranker_* -- 125-ranker outputs (numeric)
75
+ // *_response -- enum string (kept / redid / ignored / resolve / defer / ignore)
76
+ // *_count -- non-negative integer
77
+
78
+ const ALLOWED_FIELDS = Object.freeze({
79
+ // ----- 6 inherited mva.* (must mirror lib/core/mva-telemetry.cjs verbatim) -----
80
+ mva_pipeline_started: Object.freeze(['sentence_sha256']),
81
+ mva_agent_returned: Object.freeze(['sentence_sha256', 'agent_id', 'duration_ms', 'status', 'error_short']),
82
+ mva_brief_rendered: Object.freeze(['sentence_sha256', 'total_duration_ms', 'agent_count_ok', 'agent_count_failed']),
83
+ mva_option_selected: Object.freeze(['sentence_sha256', 'option_id', 'time_to_click_ms']),
84
+ mva_brief_deployed: Object.freeze(['sentence_sha256', 'vercel_subdomain_hash', 'deploy_duration_ms', 'status', 'error_short']),
85
+ mva_pipeline_failed: Object.freeze(['sentence_sha256', 'total_duration_ms', 'error_short']),
86
+
87
+ // ----- 9 net-new (D-04..D-09) -----
88
+ selector_pick: Object.freeze([
89
+ 'sub_shape', // enum: 'F.0' | 'F.1' | 'F.2' | 'F.3' | 'F.4' | 'F.5' | 'F.6' | 'F.7'
90
+ 'mode', // enum: 'A' (Brain reachable) | 'B' (Local Only) | 'Tier 0'
91
+ 'ranker_confidence', // number 0..1 (125-ranker confidence for picked option)
92
+ 'recommended_rendered', // boolean (RECOMMENDED marker was shown)
93
+ 'options_count', // integer
94
+ 'room_slug_sha256', // 64-char sha256
95
+ 'verb_chosen', // enum (closed verb vocab, one of the 10 canonical verbs)
96
+ ]),
97
+ tension_engagement: Object.freeze([
98
+ 'tension_type', // enum: 'contradicts' | 'converges' | 'invalidates'
99
+ 'user_response', // enum: 'resolve' | 'defer' | 'ignore'
100
+ 'ttr_seconds', // number (time-to-response)
101
+ 'room_slug_sha256', // 64-char sha256
102
+ 'context_hash', // shorter hash (16 chars) tying back to surfaced context
103
+ ]),
104
+ auto_explore_decision: Object.freeze([
105
+ 'finding_type', // enum: 'whitespace' | 'reverse_salient' | 'cross_domain_match'
106
+ 'user_response', // enum: 'kept' | 'redid' | 'ignored'
107
+ 'domain_match_score', // number 0..1
108
+ 'room_slug_sha256',
109
+ ]),
110
+ breakthrough_dismissed: Object.freeze([
111
+ 'detector_type', // enum (per Phase 120 D-19 detector taxonomy)
112
+ 'verb_chosen', // enum (F.7 verb pick or Free-Text)
113
+ 'ethics_tier', // enum: 'HARD_FLOOR' | 'SOFT_BAND' | 'NEUTRAL' | 'GREEN'
114
+ 'voice_audit_pass', // boolean
115
+ 'room_slug_sha256',
116
+ ]),
117
+ hooked_axis_score: Object.freeze([
118
+ 'axis_name', // enum: 'investment' | 'reward' | 'trigger' | 'action'
119
+ 'score_value', // number
120
+ 'room_slug_sha256',
121
+ 'window_iso_week', // string 'YYYY-WNN' (ISO week the score window covers)
122
+ ]),
123
+ empathy_observation: Object.freeze([
124
+ 'engaged_past_15m', // boolean
125
+ 'handed_back_material', // boolean
126
+ 'returned_within_48h', // boolean
127
+ 'ttr_seconds', // number
128
+ 'tester_id_hash', // hash (NOT the tester's real ID; per Canon Part 8)
129
+ ]),
130
+ room_receipt_written: Object.freeze([
131
+ 'room_slug_sha256',
132
+ 'conversation_id_hash', // hash
133
+ 'generated_at_ts', // number (epoch ms) or ISO string
134
+ ]),
135
+ command_invocation: Object.freeze([
136
+ 'command', // enum string (e.g. '/mos:whitespace', '/mos:think-hats')
137
+ 'outcome', // enum: 'success' | 'error' | 'aborted'
138
+ 'duration_ms', // number
139
+ 'context_hash', // hash
140
+ ]),
141
+ nav_bypass: Object.freeze([
142
+ 'op', // enum: 'read' | 'write' | 'query'
143
+ 'reason', // enum: 'legacy_path' | 'startup_bootstrap' | 'tooling_only'
144
+ 'caller_hash', // hash (stack-frame-derived identifier; NOT a file path)
145
+ 'room_slug_sha256',
146
+ ]),
147
+ });
148
+
149
+ // ---------- String-length caps ----------
150
+ //
151
+ // MAX_STRING_LEN is the default cap on any string payload value. The two
152
+ // other constants are special-cased: sha256 fields must be exactly 64 hex
153
+ // characters; error_short is a short error description with its own cap.
154
+
155
+ const MAX_STRING_LEN = 64;
156
+ const MAX_ERROR_SHORT_LEN = 60;
157
+ const SHA256_LEN = 64;
158
+ const MAX_CONTEXT_HASH_LEN = 16;
159
+
160
+ module.exports = {
161
+ EVENT_TYPES,
162
+ ALLOWED_FIELDS,
163
+ SCHEMA_VERSION,
164
+ MAX_STRING_LEN,
165
+ MAX_ERROR_SHORT_LEN,
166
+ SHA256_LEN,
167
+ MAX_CONTEXT_HASH_LEN,
168
+ };
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 121-00 -- frozen v1 telemetry schema acceptance tests.
8
+ *
9
+ * Verifies EVENT_TYPES + ALLOWED_FIELDS contracts from
10
+ * lib/core/telemetry/schema.cjs. The schema is the source of truth that
11
+ * lib/core/telemetry/validator.cjs and lib/core/telemetry/writer.cjs
12
+ * consume. Per Canon Part 8 + D-10 (frozen v1 schema), this contract is
13
+ * locked for the v1 wire forever; future evolution lives in v2 events
14
+ * that coexist in the same JSONL stream (per-row schema_version).
15
+ *
16
+ * Test map (2 cases, one-to-one with the PLAN <behavior> block for Task 1):
17
+ * 1. EVENT_TYPES is Object.freeze'd array of exactly 15 strings
18
+ * (6 inherited mva.* + 9 net-new; superset of mva-telemetry.cjs).
19
+ * 2. ALLOWED_FIELDS keys === EVENT_TYPES set; every value is frozen array.
20
+ *
21
+ * Registered in lib/memory/run-feynman-tests.cjs.
22
+ */
23
+
24
+ const assert = require('node:assert/strict');
25
+ const path = require('node:path');
26
+
27
+ const REPO = path.resolve(__dirname, '..', '..', '..');
28
+ const SCHEMA_PATH = path.join(REPO, 'lib/core/telemetry/schema.cjs');
29
+
30
+ try { delete require.cache[require.resolve(SCHEMA_PATH)]; } catch (_) {}
31
+ const schema = require(SCHEMA_PATH);
32
+
33
+ assert.equal(typeof schema, 'object', 'schema module must export an object');
34
+ assert.ok(Array.isArray(schema.EVENT_TYPES), 'schema must export EVENT_TYPES array');
35
+ assert.equal(typeof schema.ALLOWED_FIELDS, 'object', 'schema must export ALLOWED_FIELDS object');
36
+ assert.equal(typeof schema.SCHEMA_VERSION, 'number', 'schema must export numeric SCHEMA_VERSION');
37
+
38
+ // ---------- Test 1: EVENT_TYPES frozen + exactly 15 entries ----------
39
+
40
+ (function test1EventTypesFrozen() {
41
+ const expected = [
42
+ 'mva_pipeline_started',
43
+ 'mva_agent_returned',
44
+ 'mva_brief_rendered',
45
+ 'mva_option_selected',
46
+ 'mva_brief_deployed',
47
+ 'mva_pipeline_failed',
48
+ 'selector_pick',
49
+ 'tension_engagement',
50
+ 'auto_explore_decision',
51
+ 'breakthrough_dismissed',
52
+ 'hooked_axis_score',
53
+ 'empathy_observation',
54
+ 'room_receipt_written',
55
+ 'command_invocation',
56
+ 'nav_bypass',
57
+ ];
58
+
59
+ assert.equal(schema.EVENT_TYPES.length, 15,
60
+ 'EVENT_TYPES must contain exactly 15 entries (6 mva.* + 9 net-new), got ' + schema.EVENT_TYPES.length);
61
+ assert.equal(Object.isFrozen(schema.EVENT_TYPES), true,
62
+ 'EVENT_TYPES must be Object.freeze\'d');
63
+
64
+ for (const name of expected) {
65
+ assert.ok(schema.EVENT_TYPES.includes(name),
66
+ 'EVENT_TYPES missing required event: ' + name);
67
+ }
68
+
69
+ // SCHEMA_VERSION must be the literal Number 1 (Canon D-10).
70
+ assert.equal(schema.SCHEMA_VERSION, 1,
71
+ 'SCHEMA_VERSION must be Number 1, got ' + schema.SCHEMA_VERSION + ' (typeof=' + typeof schema.SCHEMA_VERSION + ')');
72
+
73
+ console.log('PASS test 1: EVENT_TYPES frozen array of 15 strings + SCHEMA_VERSION === 1');
74
+ })();
75
+
76
+ // ---------- Test 2: ALLOWED_FIELDS shape ----------
77
+
78
+ (function test2AllowedFieldsShape() {
79
+ const keys = Object.keys(schema.ALLOWED_FIELDS);
80
+
81
+ // Key parity with EVENT_TYPES.
82
+ assert.equal(keys.length, schema.EVENT_TYPES.length,
83
+ 'ALLOWED_FIELDS key count must equal EVENT_TYPES length');
84
+ for (const evt of schema.EVENT_TYPES) {
85
+ assert.ok(keys.includes(evt),
86
+ 'ALLOWED_FIELDS missing key for event type: ' + evt);
87
+ }
88
+
89
+ // Every value must be a frozen array.
90
+ for (const evt of schema.EVENT_TYPES) {
91
+ const allowed = schema.ALLOWED_FIELDS[evt];
92
+ assert.ok(Array.isArray(allowed),
93
+ 'ALLOWED_FIELDS[' + evt + '] must be an array');
94
+ assert.equal(Object.isFrozen(allowed), true,
95
+ 'ALLOWED_FIELDS[' + evt + '] must be frozen');
96
+ assert.ok(allowed.length >= 1,
97
+ 'ALLOWED_FIELDS[' + evt + '] must have at least 1 allowed field');
98
+ }
99
+
100
+ // Spot-check the 9 net-new event types per the plan (D-04 through D-09).
101
+ assert.ok(schema.ALLOWED_FIELDS.selector_pick.includes('sub_shape'));
102
+ assert.ok(schema.ALLOWED_FIELDS.selector_pick.includes('ranker_confidence'));
103
+ assert.ok(schema.ALLOWED_FIELDS.selector_pick.includes('room_slug_sha256'));
104
+ assert.ok(schema.ALLOWED_FIELDS.tension_engagement.includes('ttr_seconds'));
105
+ assert.ok(schema.ALLOWED_FIELDS.auto_explore_decision.includes('domain_match_score'));
106
+ assert.ok(schema.ALLOWED_FIELDS.breakthrough_dismissed.includes('ethics_tier'));
107
+ assert.ok(schema.ALLOWED_FIELDS.hooked_axis_score.includes('axis_name'));
108
+ assert.ok(schema.ALLOWED_FIELDS.empathy_observation.includes('engaged_past_15m'));
109
+ assert.ok(schema.ALLOWED_FIELDS.room_receipt_written.includes('conversation_id_hash'));
110
+ assert.ok(schema.ALLOWED_FIELDS.command_invocation.includes('command'));
111
+ assert.ok(schema.ALLOWED_FIELDS.nav_bypass.includes('caller_hash'));
112
+
113
+ // 6 inherited mva.* event types (must mirror lib/core/mva-telemetry.cjs ALLOWED_FIELDS).
114
+ assert.ok(schema.ALLOWED_FIELDS.mva_pipeline_started.includes('sentence_sha256'));
115
+ assert.ok(schema.ALLOWED_FIELDS.mva_agent_returned.includes('duration_ms'));
116
+ assert.ok(schema.ALLOWED_FIELDS.mva_brief_rendered.includes('total_duration_ms'));
117
+ assert.ok(schema.ALLOWED_FIELDS.mva_option_selected.includes('option_id'));
118
+ assert.ok(schema.ALLOWED_FIELDS.mva_brief_deployed.includes('vercel_subdomain_hash'));
119
+ assert.ok(schema.ALLOWED_FIELDS.mva_pipeline_failed.includes('error_short'));
120
+
121
+ console.log('PASS test 2: ALLOWED_FIELDS shape (15 frozen arrays; mva.* + 9 net-new spot-checks pass)');
122
+ })();
123
+
124
+ console.log('\nschema.test.cjs: 2/2 tests passed');