@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +6 -1
- package/CHANGELOG.md +31 -0
- package/README.md +51 -56
- package/bin/mindrian-brain-mcp-client.cjs +152 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +6 -2
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +2 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +2 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +2 -0
- package/commands/mva-option.md +2 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +20 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +22 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/directive-envelope.cjs +175 -0
- package/lib/core/directive-envelope.test.cjs +225 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/mcp-profiles.cjs +1 -1
- package/lib/core/migration-snapshot.cjs +172 -0
- package/lib/core/migration-snapshot.test.cjs +174 -0
- package/lib/core/mindrian-brain-shim.test.cjs +214 -0
- package/lib/core/mva-orchestrator.cjs +41 -0
- package/lib/core/mva-telemetry.cjs +31 -143
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/rs-nl-to-query.cjs +1 -1
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +200 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/tier0-messaging.cjs +109 -0
- package/lib/core/tier0-messaging.test.cjs +218 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/mos-status-renderer.test.cjs +2 -2
- package/lib/memory/navigation-engine-core.test.cjs +1 -1
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +223 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 121-00 -- unified writer acceptance tests.
|
|
8
|
+
*
|
|
9
|
+
* Verifies lib/core/telemetry/writer.cjs is THE single chokepoint (Canon
|
|
10
|
+
* Part 9 navigation.cjs precedent) for trajectory-telemetry emits. Every
|
|
11
|
+
* downstream capture point in 121-02 and 121-03 routes through emit() here.
|
|
12
|
+
*
|
|
13
|
+
* Test map (10 cases, one-to-one with the PLAN <behavior> block for Task 2):
|
|
14
|
+
* 1. Writer exports surface: emit, telemetryDir, telemetryFile,
|
|
15
|
+
* isoWeekFilename, EVENT_TYPES, ALLOWED_FIELDS.
|
|
16
|
+
* 2. telemetryDir() resolves under $HOME/.mindrian/telemetry/v1.13.
|
|
17
|
+
* 3. isoWeekFilename() zero-pads week and gates 2026-01-01 -> W01,
|
|
18
|
+
* 2026-01-05 -> W02, 2026-05-19 -> W21.
|
|
19
|
+
* 4. telemetryFile(date) composes telemetryDir() + isoWeekFilename(date).
|
|
20
|
+
* 5. emit() writes JSONL with schema_version: 1 (Number), valid
|
|
21
|
+
* ISO-8601 timestamp, session_id from env or 'default', all payload
|
|
22
|
+
* keys present with correct values.
|
|
23
|
+
* 6. emit() throws Error.code='TELEMETRY_VALIDATION' on unknown event.
|
|
24
|
+
* 7. emit() throws on Canon Part 8 forbidden pattern (Cypher in
|
|
25
|
+
* allowed field).
|
|
26
|
+
* 8. emit() creates the v1.13 dir if missing (recursive mkdir); does
|
|
27
|
+
* NOT throw if fs.appendFileSync fails (best-effort -- pipeline
|
|
28
|
+
* must not crash on telemetry disk failure).
|
|
29
|
+
* 9. Two emit() calls 5ms apart land in the SAME ISO-week file.
|
|
30
|
+
* 10. Every line is JSON.parseable (no BOM, single newline terminator).
|
|
31
|
+
*
|
|
32
|
+
* Hermetic: each test creates a tmpdir, points HOME at it, writes, asserts,
|
|
33
|
+
* and tears down via fs.rmSync in finally. Mirrors
|
|
34
|
+
* lib/memory/query-efficiency-telemetry.test.cjs.
|
|
35
|
+
*
|
|
36
|
+
* Registered in lib/memory/run-feynman-tests.cjs.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const assert = require('node:assert/strict');
|
|
40
|
+
const fs = require('node:fs');
|
|
41
|
+
const path = require('node:path');
|
|
42
|
+
const os = require('node:os');
|
|
43
|
+
|
|
44
|
+
const REPO = path.resolve(__dirname, '..', '..', '..');
|
|
45
|
+
const WRITER_PATH = path.join(REPO, 'lib/core/telemetry/writer.cjs');
|
|
46
|
+
|
|
47
|
+
try { delete require.cache[require.resolve(WRITER_PATH)]; } catch (_) {}
|
|
48
|
+
const writer = require(WRITER_PATH);
|
|
49
|
+
|
|
50
|
+
// ---------- Fixture scaffolding ----------
|
|
51
|
+
|
|
52
|
+
function mkTmpDir(prefix) {
|
|
53
|
+
const base = path.join(os.tmpdir(), 'mos-121-00-' + prefix + '-' + process.pid + '-' + Date.now().toString(36));
|
|
54
|
+
fs.mkdirSync(base, { recursive: true });
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rmRf(p) {
|
|
59
|
+
try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function withHome(tmp, fn) {
|
|
63
|
+
const originalHome = process.env.HOME;
|
|
64
|
+
process.env.HOME = tmp;
|
|
65
|
+
try { return fn(); } finally { process.env.HOME = originalHome; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readJsonl(filePath) {
|
|
69
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
70
|
+
// No BOM allowed, single newline per line.
|
|
71
|
+
assert.ok(text.charCodeAt(0) !== 0xFEFF, 'JSONL must not start with BOM');
|
|
72
|
+
const lines = text.split('\n').filter(l => l.length > 0);
|
|
73
|
+
return lines.map(l => JSON.parse(l));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------- Test 1: writer export surface ----------
|
|
77
|
+
|
|
78
|
+
(function test1Exports() {
|
|
79
|
+
assert.equal(typeof writer.emit, 'function', 'writer.emit must be a function');
|
|
80
|
+
assert.equal(typeof writer.telemetryDir, 'function', 'writer.telemetryDir must be a function');
|
|
81
|
+
assert.equal(typeof writer.telemetryFile, 'function', 'writer.telemetryFile must be a function');
|
|
82
|
+
assert.equal(typeof writer.isoWeekFilename, 'function', 'writer.isoWeekFilename must be a function');
|
|
83
|
+
assert.ok(Array.isArray(writer.EVENT_TYPES), 'writer must re-export EVENT_TYPES');
|
|
84
|
+
assert.equal(typeof writer.ALLOWED_FIELDS, 'object', 'writer must re-export ALLOWED_FIELDS');
|
|
85
|
+
console.log('PASS test 1: writer exports emit/telemetryDir/telemetryFile/isoWeekFilename/EVENT_TYPES/ALLOWED_FIELDS');
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
// ---------- Test 2: telemetryDir resolves under HOME ----------
|
|
89
|
+
|
|
90
|
+
(function test2TelemetryDir() {
|
|
91
|
+
const tmp = mkTmpDir('dir');
|
|
92
|
+
try {
|
|
93
|
+
withHome(tmp, () => {
|
|
94
|
+
const dir = writer.telemetryDir();
|
|
95
|
+
const expected = path.join(tmp, '.mindrian', 'telemetry', 'v1.13');
|
|
96
|
+
assert.equal(dir, expected,
|
|
97
|
+
'telemetryDir() must resolve to <HOME>/.mindrian/telemetry/v1.13; got ' + dir);
|
|
98
|
+
});
|
|
99
|
+
console.log('PASS test 2: telemetryDir() resolves under HOME');
|
|
100
|
+
} finally {
|
|
101
|
+
rmRf(tmp);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
// ---------- Test 3: isoWeekFilename zero-pads + handles year boundaries ----------
|
|
106
|
+
|
|
107
|
+
(function test3IsoWeek() {
|
|
108
|
+
// 2026-05-19 is a Tuesday in ISO week 21 of 2026.
|
|
109
|
+
const f1 = writer.isoWeekFilename(new Date('2026-05-19T12:00:00Z'));
|
|
110
|
+
assert.equal(f1, 'events-2026-W21.jsonl',
|
|
111
|
+
'isoWeekFilename(2026-05-19) must be events-2026-W21.jsonl; got ' + f1);
|
|
112
|
+
|
|
113
|
+
// 2026-01-05 is a Monday in ISO week 02 of 2026 (W01 is the week of
|
|
114
|
+
// 2025-12-29..2026-01-04 because the first Thursday of 2026 is 2026-01-01).
|
|
115
|
+
const f2 = writer.isoWeekFilename(new Date('2026-01-05T12:00:00Z'));
|
|
116
|
+
assert.equal(f2, 'events-2026-W02.jsonl',
|
|
117
|
+
'isoWeekFilename(2026-01-05) must be events-2026-W02.jsonl (zero-padded); got ' + f2);
|
|
118
|
+
|
|
119
|
+
// 2026-01-01 is the Thursday that anchors ISO week 01 of 2026.
|
|
120
|
+
const f3 = writer.isoWeekFilename(new Date('2026-01-01T12:00:00Z'));
|
|
121
|
+
assert.equal(f3, 'events-2026-W01.jsonl',
|
|
122
|
+
'isoWeekFilename(2026-01-01) must be events-2026-W01.jsonl; got ' + f3);
|
|
123
|
+
|
|
124
|
+
console.log('PASS test 3: isoWeekFilename zero-pads week (W21, W02, W01)');
|
|
125
|
+
})();
|
|
126
|
+
|
|
127
|
+
// ---------- Test 4: telemetryFile composes ----------
|
|
128
|
+
|
|
129
|
+
(function test4TelemetryFile() {
|
|
130
|
+
const tmp = mkTmpDir('file');
|
|
131
|
+
try {
|
|
132
|
+
withHome(tmp, () => {
|
|
133
|
+
const f = writer.telemetryFile(new Date('2026-05-19T00:00:00Z'));
|
|
134
|
+
const expected = path.join(tmp, '.mindrian', 'telemetry', 'v1.13', 'events-2026-W21.jsonl');
|
|
135
|
+
assert.equal(f, expected,
|
|
136
|
+
'telemetryFile() must compose dir + isoWeekFilename; got ' + f);
|
|
137
|
+
});
|
|
138
|
+
console.log('PASS test 4: telemetryFile() composes dir + filename');
|
|
139
|
+
} finally {
|
|
140
|
+
rmRf(tmp);
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
// ---------- Test 5: emit() writes JSONL with schema_version + iso timestamp + session_id + payload ----------
|
|
145
|
+
|
|
146
|
+
(function test5EmitWritesJsonl() {
|
|
147
|
+
const tmp = mkTmpDir('emit');
|
|
148
|
+
const originalSession = process.env.CLAUDE_SESSION_ID;
|
|
149
|
+
process.env.CLAUDE_SESSION_ID = 'test-session-abc';
|
|
150
|
+
try {
|
|
151
|
+
withHome(tmp, () => {
|
|
152
|
+
writer.emit('selector_pick', {
|
|
153
|
+
sub_shape: 'F.1',
|
|
154
|
+
mode: 'A',
|
|
155
|
+
ranker_confidence: 0.73,
|
|
156
|
+
recommended_rendered: true,
|
|
157
|
+
options_count: 4,
|
|
158
|
+
room_slug_sha256: 'a'.repeat(64),
|
|
159
|
+
verb_chosen: 'Run Methodology',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const filePath = writer.telemetryFile();
|
|
163
|
+
assert.ok(fs.existsSync(filePath), 'emit() must create the JSONL file at ' + filePath);
|
|
164
|
+
|
|
165
|
+
const rows = readJsonl(filePath);
|
|
166
|
+
assert.equal(rows.length, 1, 'emit() must write exactly 1 line; got ' + rows.length);
|
|
167
|
+
|
|
168
|
+
const r = rows[0];
|
|
169
|
+
assert.equal(r.event, 'selector_pick', 'event field must be selector_pick');
|
|
170
|
+
assert.equal(r.schema_version, 1, 'schema_version must be Number 1; got ' + r.schema_version + ' (' + typeof r.schema_version + ')');
|
|
171
|
+
assert.equal(typeof r.schema_version, 'number', 'schema_version must be typeof number');
|
|
172
|
+
assert.match(r.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, 'timestamp must be ISO-8601');
|
|
173
|
+
assert.equal(typeof r.session_id, 'string', 'session_id must be a string');
|
|
174
|
+
assert.ok(r.session_id.length > 0, 'session_id must be non-empty');
|
|
175
|
+
assert.equal(r.session_id, 'test-session-abc', 'session_id must reflect CLAUDE_SESSION_ID env');
|
|
176
|
+
|
|
177
|
+
// All 7 payload keys present with correct values.
|
|
178
|
+
assert.equal(r.sub_shape, 'F.1');
|
|
179
|
+
assert.equal(r.mode, 'A');
|
|
180
|
+
assert.equal(r.ranker_confidence, 0.73);
|
|
181
|
+
assert.equal(r.recommended_rendered, true);
|
|
182
|
+
assert.equal(r.options_count, 4);
|
|
183
|
+
assert.equal(r.room_slug_sha256, 'a'.repeat(64));
|
|
184
|
+
assert.equal(r.verb_chosen, 'Run Methodology');
|
|
185
|
+
});
|
|
186
|
+
console.log('PASS test 5: emit() writes JSONL with schema_version=1, ISO timestamp, session_id, all 7 payload keys');
|
|
187
|
+
} finally {
|
|
188
|
+
if (originalSession === undefined) { delete process.env.CLAUDE_SESSION_ID; }
|
|
189
|
+
else { process.env.CLAUDE_SESSION_ID = originalSession; }
|
|
190
|
+
rmRf(tmp);
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
// ---------- Test 6: emit() throws on unknown event ----------
|
|
195
|
+
|
|
196
|
+
(function test6UnknownEventThrows() {
|
|
197
|
+
const tmp = mkTmpDir('unknown');
|
|
198
|
+
try {
|
|
199
|
+
withHome(tmp, () => {
|
|
200
|
+
assert.throws(() => writer.emit('definitely_not_a_real_event', {}),
|
|
201
|
+
(err) => err.code === 'TELEMETRY_VALIDATION',
|
|
202
|
+
'emit() on unknown event must throw with code TELEMETRY_VALIDATION');
|
|
203
|
+
});
|
|
204
|
+
console.log('PASS test 6: emit() throws TELEMETRY_VALIDATION on unknown event');
|
|
205
|
+
} finally {
|
|
206
|
+
rmRf(tmp);
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
|
|
210
|
+
// ---------- Test 7: emit() throws on Canon Part 8 forbidden pattern ----------
|
|
211
|
+
|
|
212
|
+
(function test7ForbiddenPatternThrows() {
|
|
213
|
+
const tmp = mkTmpDir('forbidden');
|
|
214
|
+
try {
|
|
215
|
+
withHome(tmp, () => {
|
|
216
|
+
assert.throws(() => writer.emit('selector_pick', {
|
|
217
|
+
sub_shape: 'F.1',
|
|
218
|
+
verb_chosen: 'MATCH (n:Framework) RETURN n', // Cypher injection
|
|
219
|
+
}), (err) => err.code === 'TELEMETRY_VALIDATION',
|
|
220
|
+
'emit() must throw on Cypher fragment in allowed field');
|
|
221
|
+
});
|
|
222
|
+
console.log('PASS test 7: emit() throws on Canon Part 8 forbidden pattern (Cypher)');
|
|
223
|
+
} finally {
|
|
224
|
+
rmRf(tmp);
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
|
|
228
|
+
// ---------- Test 8: emit() creates dir + swallows fs errors silently ----------
|
|
229
|
+
|
|
230
|
+
(function test8DirCreationAndFsErrorSilent() {
|
|
231
|
+
// Step A: emit into a HOME that does NOT yet contain .mindrian/telemetry/v1.13.
|
|
232
|
+
const tmp = mkTmpDir('create');
|
|
233
|
+
try {
|
|
234
|
+
withHome(tmp, () => {
|
|
235
|
+
// Verify the dir does not exist yet.
|
|
236
|
+
const dir = writer.telemetryDir();
|
|
237
|
+
assert.equal(fs.existsSync(dir), false, 'dir must NOT exist pre-emit');
|
|
238
|
+
writer.emit('selector_pick', {
|
|
239
|
+
sub_shape: 'F.1',
|
|
240
|
+
mode: 'A',
|
|
241
|
+
options_count: 1,
|
|
242
|
+
});
|
|
243
|
+
assert.equal(fs.existsSync(dir), true, 'emit() must create the v1.13 dir recursively');
|
|
244
|
+
const filePath = writer.telemetryFile();
|
|
245
|
+
assert.equal(fs.existsSync(filePath), true, 'emit() must create the JSONL file');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Step B: emit when fs.appendFileSync is monkeypatched to throw. Pipeline
|
|
249
|
+
// must not crash. Done in a fresh subprocess so we can monkeypatch without
|
|
250
|
+
// breaking the rest of the suite.
|
|
251
|
+
const tmp2 = mkTmpDir('fserr');
|
|
252
|
+
withHome(tmp2, () => {
|
|
253
|
+
const realAppend = fs.appendFileSync;
|
|
254
|
+
fs.appendFileSync = function () { throw new Error('disk full simulated'); };
|
|
255
|
+
try {
|
|
256
|
+
writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
|
|
257
|
+
// If we reach here, the swallow worked.
|
|
258
|
+
} finally {
|
|
259
|
+
fs.appendFileSync = realAppend;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
rmRf(tmp2);
|
|
263
|
+
|
|
264
|
+
console.log('PASS test 8: emit() creates dir recursively + swallows fs errors silently');
|
|
265
|
+
} finally {
|
|
266
|
+
rmRf(tmp);
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
|
|
270
|
+
// ---------- Test 9: two emits 5ms apart land in the same ISO-week file ----------
|
|
271
|
+
|
|
272
|
+
(function test9SameFileAcrossShortInterval() {
|
|
273
|
+
const tmp = mkTmpDir('sameweek');
|
|
274
|
+
try {
|
|
275
|
+
withHome(tmp, () => {
|
|
276
|
+
writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
|
|
277
|
+
const sleep5 = Date.now() + 5;
|
|
278
|
+
while (Date.now() < sleep5) {} // busy-wait
|
|
279
|
+
writer.emit('selector_pick', { sub_shape: 'F.2', mode: 'B' });
|
|
280
|
+
|
|
281
|
+
const filePath = writer.telemetryFile();
|
|
282
|
+
const rows = readJsonl(filePath);
|
|
283
|
+
assert.equal(rows.length, 2,
|
|
284
|
+
'two emits 5ms apart must land in same file -> 2 lines; got ' + rows.length);
|
|
285
|
+
assert.equal(rows[0].sub_shape, 'F.1');
|
|
286
|
+
assert.equal(rows[1].sub_shape, 'F.2');
|
|
287
|
+
});
|
|
288
|
+
console.log('PASS test 9: two emits 5ms apart in same ISO-week file');
|
|
289
|
+
} finally {
|
|
290
|
+
rmRf(tmp);
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
// ---------- Test 10: lines are JSON.parseable, no BOM, no trailing whitespace ----------
|
|
295
|
+
|
|
296
|
+
(function test10ParseableLines() {
|
|
297
|
+
const tmp = mkTmpDir('parse');
|
|
298
|
+
try {
|
|
299
|
+
withHome(tmp, () => {
|
|
300
|
+
writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
|
|
301
|
+
writer.emit('command_invocation', { command: '/mos:status', outcome: 'success', duration_ms: 42 });
|
|
302
|
+
writer.emit('nav_bypass', { op: 'read', reason: 'legacy_path' });
|
|
303
|
+
|
|
304
|
+
const filePath = writer.telemetryFile();
|
|
305
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
306
|
+
|
|
307
|
+
// BOM check.
|
|
308
|
+
assert.ok(text.charCodeAt(0) !== 0xFEFF, 'no BOM at start');
|
|
309
|
+
|
|
310
|
+
// Each line must JSON.parse.
|
|
311
|
+
const lines = text.split('\n');
|
|
312
|
+
// Last element after split('\n') is '' if file ends with \n (which it should).
|
|
313
|
+
assert.equal(lines[lines.length - 1], '', 'file must end with newline (last split chunk is empty string)');
|
|
314
|
+
|
|
315
|
+
const dataLines = lines.slice(0, -1);
|
|
316
|
+
assert.equal(dataLines.length, 3, 'must have 3 data lines; got ' + dataLines.length);
|
|
317
|
+
for (const line of dataLines) {
|
|
318
|
+
assert.equal(line[line.length - 1] !== ' ' && line[line.length - 1] !== '\r', true,
|
|
319
|
+
'no trailing whitespace on line: ' + JSON.stringify(line));
|
|
320
|
+
const obj = JSON.parse(line); // throws if malformed
|
|
321
|
+
assert.equal(typeof obj.event, 'string');
|
|
322
|
+
assert.equal(obj.schema_version, 1);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
console.log('PASS test 10: every line is JSON.parseable, no BOM, single newline terminator');
|
|
326
|
+
} finally {
|
|
327
|
+
rmRf(tmp);
|
|
328
|
+
}
|
|
329
|
+
})();
|
|
330
|
+
|
|
331
|
+
console.log('\nwriter.test.cjs: 10/10 tests passed');
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
5
|
+
*
|
|
6
|
+
* Phase 121.5-07 Task 1 -- Terminal capability probe.
|
|
7
|
+
*
|
|
8
|
+
* Decision D-05 (LOCKED):
|
|
9
|
+
* probeCapability(opts) returns one of:
|
|
10
|
+
* 'truecolor' -- process.stdout.isTTY === true AND ($COLORTERM matches
|
|
11
|
+
* truecolor/24bit OR equals literal "truecolor")
|
|
12
|
+
* '256color' -- isTTY === true AND ($COLORTERM contains "256color" OR
|
|
13
|
+
* $TERM contains "256color")
|
|
14
|
+
* 'ascii' -- otherwise (non-TTY, no COLORTERM, Desktop chat surface)
|
|
15
|
+
*
|
|
16
|
+
* Desktop hard-override: env.CLAUDE_DESKTOP=1 or env.MINDRIAN_DESKTOP=1 forces
|
|
17
|
+
* 'ascii' regardless of TTY state. This is the Desktop chat surface, which
|
|
18
|
+
* strips ANSI escapes.
|
|
19
|
+
*
|
|
20
|
+
* Canon references:
|
|
21
|
+
* Part 3 UI Ruling System -- bulletproof terminal coherence depends on a
|
|
22
|
+
* single capability probe shared across surfaces.
|
|
23
|
+
* Part 7 Reuse Before Build -- this is the REUSABLE substrate for
|
|
24
|
+
* /mos:status, /mos:doctor, /mos:splash, /mos:help renderer.
|
|
25
|
+
* Part 8 Graph Boundary -- reads env-vars + isTTY only; zero network,
|
|
26
|
+
* zero Brain, zero telemetry egress.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Probe the current terminal capability.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {object} [opts.env] -- env override (defaults to process.env)
|
|
34
|
+
* @param {boolean} [opts.isTTY] -- TTY override (defaults to process.stdout.isTTY)
|
|
35
|
+
* @returns {'truecolor'|'256color'|'ascii'}
|
|
36
|
+
*/
|
|
37
|
+
function probeCapability(opts) {
|
|
38
|
+
const o = opts || {};
|
|
39
|
+
const env = o.env || process.env;
|
|
40
|
+
const isTTY =
|
|
41
|
+
o.isTTY !== undefined
|
|
42
|
+
? o.isTTY
|
|
43
|
+
: Boolean(process.stdout && process.stdout.isTTY);
|
|
44
|
+
|
|
45
|
+
// Desktop simulator hard-override.
|
|
46
|
+
if (env.CLAUDE_DESKTOP === '1' || env.MINDRIAN_DESKTOP === '1') {
|
|
47
|
+
return 'ascii';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Test/CI hard-override for harnesses that pipe stdout (isTTY false).
|
|
51
|
+
// Used by the Phase 121.5-09 coherence smoke test harness to verify the
|
|
52
|
+
// truecolor branch fires on the CLI surface. Production code paths never
|
|
53
|
+
// set this; isTTY-based detection remains the default contract.
|
|
54
|
+
if (env.MINDRIAN_FORCE_TRUECOLOR === '1') return 'truecolor';
|
|
55
|
+
if (env.MINDRIAN_FORCE_256COLOR === '1') return '256color';
|
|
56
|
+
|
|
57
|
+
if (!isTTY) return 'ascii';
|
|
58
|
+
|
|
59
|
+
const colorterm = String(env.COLORTERM || '').toLowerCase();
|
|
60
|
+
const term = String(env.TERM || '').toLowerCase();
|
|
61
|
+
|
|
62
|
+
if (colorterm === 'truecolor' || /truecolor/.test(colorterm) || /24bit/.test(colorterm)) {
|
|
63
|
+
return 'truecolor';
|
|
64
|
+
}
|
|
65
|
+
if (colorterm.includes('256color') || term.includes('256color')) {
|
|
66
|
+
return '256color';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'ascii';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {'truecolor'|'256color'|'ascii'} capability
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function supportsColor(capability) {
|
|
77
|
+
return capability === 'truecolor' || capability === '256color';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {'truecolor'|'256color'|'ascii'} capability
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function supportsTruecolor(capability) {
|
|
85
|
+
return capability === 'truecolor';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { probeCapability, supportsColor, supportsTruecolor };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 127-02 BRAIN-MCP-127-09 -- Tier-0 graceful messaging chokepoint.
|
|
5
|
+
*
|
|
6
|
+
* Single source-of-truth for the DIRECTOR_NOT_AVAILABLE sentinel shape used
|
|
7
|
+
* across the plugin:
|
|
8
|
+
* - bin/mindrian-brain-mcp-client.cjs (the stdio shim from Phase 127-00)
|
|
9
|
+
* - Larry's prose surface (one-line hint via larryTier0Hint)
|
|
10
|
+
* - Future statusline + /mos:status surfaces (CONTEXT acceptance gate #4)
|
|
11
|
+
*
|
|
12
|
+
* Before this chokepoint, the shim shipped its own inline copy of the
|
|
13
|
+
* sentinel shape. Future surfaces (statusline, /mos:status, persona output)
|
|
14
|
+
* would each duplicate the same shape, drifting on the upgrade_hint URL or
|
|
15
|
+
* the fallback_advice phrasing. This module locks the wire shape and the
|
|
16
|
+
* Larry-prose phrasing so every consumer reads the same canonical bytes.
|
|
17
|
+
*
|
|
18
|
+
* Wire shape locked here (BRAIN-MCP-127-09 invariant):
|
|
19
|
+
* {
|
|
20
|
+
* status: "DIRECTOR_NOT_AVAILABLE",
|
|
21
|
+
* reason: "MINDRIAN_BRAIN_KEY not set",
|
|
22
|
+
* command_context: <toolName string | "unknown" for non-string input>,
|
|
23
|
+
* upgrade_hint: "Request a Brain key at https://mindrianos.vercel.app/brain-access",
|
|
24
|
+
* fallback_advice: "Larry can still talk with you and reflect on your room context. Methodology orchestration requires Brain."
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Canon Part 7 (reuse): isAvailable() is a one-line delegation to
|
|
28
|
+
* brain-client.cjs's existing isAvailable(); no parallel key-resolver code
|
|
29
|
+
* path lives here. The shim's local tier0Response becomes a one-line
|
|
30
|
+
* passthrough after the Phase 127-02 refactor; no duplicate shape exists.
|
|
31
|
+
*
|
|
32
|
+
* Canon Part 8 (graph boundary): zero network surface in this file.
|
|
33
|
+
* isAvailable() delegates to brain-client.cjs (the existing chokepoint that
|
|
34
|
+
* reads ONLY the LOCAL key via resolve-brain-key.cjs). No fetch, no http,
|
|
35
|
+
* no Brain endpoint domain strings.
|
|
36
|
+
*
|
|
37
|
+
* HARD RULE: no em-dashes anywhere in this file (hyphens only).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const brainClient = require('./brain-client.cjs');
|
|
41
|
+
|
|
42
|
+
// Locked wire string. Renaming this constant breaks every downstream consumer
|
|
43
|
+
// (the shim, Larry's prose surface, the doctor's Class-M smoke L5 check).
|
|
44
|
+
// Treat as a phase-amendment boundary.
|
|
45
|
+
const DIRECTOR_NOT_AVAILABLE = 'DIRECTOR_NOT_AVAILABLE';
|
|
46
|
+
|
|
47
|
+
// Locked sentinel strings. Tests assert the keys; the values are
|
|
48
|
+
// human-facing and may evolve, but only via explicit phase amendment.
|
|
49
|
+
const REASON_NO_KEY = 'MINDRIAN_BRAIN_KEY not set';
|
|
50
|
+
const UPGRADE_HINT = 'Request a Brain key at https://mindrianos.vercel.app/brain-access';
|
|
51
|
+
const FALLBACK_ADVICE = 'Larry can still talk with you and reflect on your room context. Methodology orchestration requires Brain.';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Construct the Tier-0 sentinel response. Returned by every Brain-tool entry
|
|
55
|
+
* point when no key is resolvable. The shape is byte-locked.
|
|
56
|
+
*
|
|
57
|
+
* Defensive: non-string / empty / non-truthy commandContext arguments coerce
|
|
58
|
+
* to "unknown" so the wire shape is invariant under bad-caller inputs (the
|
|
59
|
+
* shim's tool-handler closure passes the literal tool name; future callers
|
|
60
|
+
* may pass null in error paths).
|
|
61
|
+
*
|
|
62
|
+
* @param {string} commandContext the tool name (e.g. "brain_ask"); falls back
|
|
63
|
+
* to "unknown" for non-string / empty inputs.
|
|
64
|
+
* @returns {{status: string, reason: string, command_context: string,
|
|
65
|
+
* upgrade_hint: string, fallback_advice: string}}
|
|
66
|
+
*/
|
|
67
|
+
function tier0Response(commandContext) {
|
|
68
|
+
const ctx = (typeof commandContext === 'string' && commandContext.length > 0)
|
|
69
|
+
? commandContext
|
|
70
|
+
: 'unknown';
|
|
71
|
+
return {
|
|
72
|
+
status: DIRECTOR_NOT_AVAILABLE,
|
|
73
|
+
reason: REASON_NO_KEY,
|
|
74
|
+
command_context: ctx,
|
|
75
|
+
upgrade_hint: UPGRADE_HINT,
|
|
76
|
+
fallback_advice: FALLBACK_ADVICE,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Is the Brain reachable from this process right now? Delegates to
|
|
82
|
+
* brain-client.cjs's existing chokepoint (which reads only the LOCAL key via
|
|
83
|
+
* resolve-brain-key.cjs). One-line passthrough -- never duplicate the key
|
|
84
|
+
* resolution logic.
|
|
85
|
+
*
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function isAvailable() {
|
|
89
|
+
return brainClient.isAvailable();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One-line Larry-prose hint for the Tier-0 path. Used by Larry's surface
|
|
94
|
+
* (and future statusline / /mos:status) when isAvailable() returns false to
|
|
95
|
+
* tell the user how to unlock Brain. Locked under 120 chars so it fits in
|
|
96
|
+
* statusline + chat-prefix surfaces without truncation.
|
|
97
|
+
*
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
function larryTier0Hint() {
|
|
101
|
+
return 'Methodology orchestration needs a Brain key. Drop one in ~/.mindrian.env or set MINDRIAN_BRAIN_KEY.';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
DIRECTOR_NOT_AVAILABLE,
|
|
106
|
+
tier0Response,
|
|
107
|
+
isAvailable,
|
|
108
|
+
larryTier0Hint,
|
|
109
|
+
};
|