@mindrian_os/install 1.13.0-beta.16 → 1.13.0-beta.17
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/CHANGELOG.md +10 -0
- package/commands/file-meeting.md +2 -0
- package/commands/grade.md +2 -0
- package/commands/mva-brief.md +56 -0
- package/commands/mva-option.md +89 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +2 -0
- package/hooks/hooks.json +9 -0
- package/lib/agents/mva/brain-classic-traps.cjs +77 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/conversation/operator.cjs +64 -0
- package/lib/conversation/operator.test.cjs +160 -0
- package/lib/core/mva-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +370 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +197 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +292 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +324 -0
- package/lib/core/mva-orchestrator.test.cjs +908 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +213 -0
- package/lib/core/mva-rule-linter.test.cjs +336 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +170 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation.cjs +11 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -0
- package/lib/memory/run-feynman-tests.cjs +27 -0
- package/package.json +1 -1
- package/skills/mva-pipeline/SKILL.md +129 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-01 Plan 01 Task 3 -- mva-dispatcher tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies parallel fan-out with streaming-as-settled order, per-agent
|
|
7
|
+
* timeouts, global timeout, all-fail handling, dispatchToArray collector,
|
|
8
|
+
* and the Canon Part 8 invariant that the AgentContext contains ONLY
|
|
9
|
+
* sentence_sha256 (no raw_sentence, no MVA_SENTENCE env var).
|
|
10
|
+
*
|
|
11
|
+
* Pure CJS, node built-ins only. Run via `node --test`.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const test = require('node:test');
|
|
16
|
+
const assert = require('node:assert');
|
|
17
|
+
|
|
18
|
+
const { dispatch, dispatchToArray } = require('./mva-dispatcher.cjs');
|
|
19
|
+
|
|
20
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
|
|
22
|
+
function mkAgent(id, fn) {
|
|
23
|
+
return { id, fn };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('mva-dispatcher: Test 1 -- 6 fast agents fan out in parallel', async () => {
|
|
27
|
+
const agents = Array.from({ length: 6 }, (_, i) =>
|
|
28
|
+
mkAgent(`agent_${i}`, async () => ({ status: 'ok', payload: { i } }))
|
|
29
|
+
);
|
|
30
|
+
const t0 = Date.now();
|
|
31
|
+
const results = await dispatchToArray(agents, 'sha_abc');
|
|
32
|
+
const elapsed = Date.now() - t0;
|
|
33
|
+
assert.strictEqual(results.length, 6);
|
|
34
|
+
for (const r of results) {
|
|
35
|
+
assert.strictEqual(r.status, 'ok');
|
|
36
|
+
}
|
|
37
|
+
assert.ok(elapsed < 300, `wall-clock ${elapsed}ms should be < 300ms (fast fan-out)`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('mva-dispatcher: Test 2 -- 6 slow agents all timeout independently', async () => {
|
|
41
|
+
const agents = Array.from({ length: 6 }, (_, i) =>
|
|
42
|
+
mkAgent(`agent_${i}`, async (_ctx, signal) => {
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
const t = setTimeout(resolve, 1000);
|
|
45
|
+
signal.addEventListener('abort', () => {
|
|
46
|
+
clearTimeout(t);
|
|
47
|
+
reject(new Error('aborted'));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
return { status: 'ok', payload: {} };
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
const t0 = Date.now();
|
|
54
|
+
const results = await dispatchToArray(agents, 'sha_abc', {
|
|
55
|
+
globalBudgetMs: 5000,
|
|
56
|
+
perAgentCapMs: 50
|
|
57
|
+
});
|
|
58
|
+
const elapsed = Date.now() - t0;
|
|
59
|
+
assert.strictEqual(results.length, 6);
|
|
60
|
+
for (const r of results) {
|
|
61
|
+
assert.strictEqual(r.status, 'timeout', `agent ${r.agent_id} should be timeout`);
|
|
62
|
+
}
|
|
63
|
+
assert.ok(elapsed < 400, `wall-clock ${elapsed}ms should be < 400ms (parallel timeouts)`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('mva-dispatcher: Test 3 -- mixed fast/slow stream in arrival order', async () => {
|
|
67
|
+
const agents = [
|
|
68
|
+
mkAgent('fast_1', async () => { await sleep(10); return { status: 'ok', payload: 'f1' }; }),
|
|
69
|
+
mkAgent('slow_1', async (_c, s) => {
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
const t = setTimeout(resolve, 1000);
|
|
72
|
+
s.addEventListener('abort', () => { clearTimeout(t); reject(new Error('aborted')); });
|
|
73
|
+
});
|
|
74
|
+
return { status: 'ok', payload: 's1' };
|
|
75
|
+
}),
|
|
76
|
+
mkAgent('fast_2', async () => { await sleep(10); return { status: 'ok', payload: 'f2' }; }),
|
|
77
|
+
mkAgent('slow_2', async (_c, s) => {
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
const t = setTimeout(resolve, 1000);
|
|
80
|
+
s.addEventListener('abort', () => { clearTimeout(t); reject(new Error('aborted')); });
|
|
81
|
+
});
|
|
82
|
+
return { status: 'ok', payload: 's2' };
|
|
83
|
+
}),
|
|
84
|
+
mkAgent('fast_3', async () => { await sleep(10); return { status: 'ok', payload: 'f3' }; }),
|
|
85
|
+
mkAgent('slow_3', async (_c, s) => {
|
|
86
|
+
await new Promise((resolve, reject) => {
|
|
87
|
+
const t = setTimeout(resolve, 1000);
|
|
88
|
+
s.addEventListener('abort', () => { clearTimeout(t); reject(new Error('aborted')); });
|
|
89
|
+
});
|
|
90
|
+
return { status: 'ok', payload: 's3' };
|
|
91
|
+
})
|
|
92
|
+
];
|
|
93
|
+
const t0 = Date.now();
|
|
94
|
+
const order = [];
|
|
95
|
+
const stamps = [];
|
|
96
|
+
for await (const r of dispatch(agents, 'sha_abc', {
|
|
97
|
+
globalBudgetMs: 5000,
|
|
98
|
+
perAgentCapMs: 100
|
|
99
|
+
})) {
|
|
100
|
+
order.push({ id: r.agent_id, status: r.status });
|
|
101
|
+
stamps.push(Date.now() - t0);
|
|
102
|
+
}
|
|
103
|
+
assert.strictEqual(order.length, 6);
|
|
104
|
+
// First 3 yielded are fast (status=ok); next 3 are slow (status=timeout).
|
|
105
|
+
const fastOnes = order.slice(0, 3);
|
|
106
|
+
const slowOnes = order.slice(3);
|
|
107
|
+
for (const r of fastOnes) {
|
|
108
|
+
assert.strictEqual(r.status, 'ok', `expected fast ok, got ${JSON.stringify(r)}`);
|
|
109
|
+
assert.ok(['fast_1', 'fast_2', 'fast_3'].includes(r.id));
|
|
110
|
+
}
|
|
111
|
+
for (const r of slowOnes) {
|
|
112
|
+
assert.strictEqual(r.status, 'timeout');
|
|
113
|
+
assert.ok(['slow_1', 'slow_2', 'slow_3'].includes(r.id));
|
|
114
|
+
}
|
|
115
|
+
// Last fast yields well before first slow yields.
|
|
116
|
+
assert.ok(stamps[2] < 80, `fast results should yield by ~50ms; got ${stamps[2]}`);
|
|
117
|
+
assert.ok(stamps[3] >= 90, `first slow result should yield around ~100ms; got ${stamps[3]}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('mva-dispatcher: Test 4 -- global budget short truncates remaining agents', async () => {
|
|
121
|
+
const agents = Array.from({ length: 6 }, (_, i) =>
|
|
122
|
+
mkAgent(`agent_${i}`, async (_c, s) => {
|
|
123
|
+
await new Promise((resolve, reject) => {
|
|
124
|
+
const t = setTimeout(resolve, 1000);
|
|
125
|
+
s.addEventListener('abort', () => { clearTimeout(t); reject(new Error('aborted')); });
|
|
126
|
+
});
|
|
127
|
+
return { status: 'ok', payload: {} };
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
const t0 = Date.now();
|
|
131
|
+
const results = await dispatchToArray(agents, 'sha_abc', {
|
|
132
|
+
globalBudgetMs: 100,
|
|
133
|
+
perAgentCapMs: 35000
|
|
134
|
+
});
|
|
135
|
+
const elapsed = Date.now() - t0;
|
|
136
|
+
assert.strictEqual(results.length, 6);
|
|
137
|
+
for (const r of results) {
|
|
138
|
+
assert.strictEqual(r.status, 'timeout');
|
|
139
|
+
}
|
|
140
|
+
assert.ok(elapsed < 250, `global budget should clamp wall-clock; got ${elapsed}ms`);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('mva-dispatcher: Test 5 -- dispatchToArray equivalent to for-await', async () => {
|
|
144
|
+
const agents = Array.from({ length: 3 }, (_, i) =>
|
|
145
|
+
mkAgent(`agent_${i}`, async () => ({ status: 'ok', payload: { i } }))
|
|
146
|
+
);
|
|
147
|
+
const a = await dispatchToArray(agents, 'sha_abc');
|
|
148
|
+
const b = [];
|
|
149
|
+
for await (const r of dispatch(agents, 'sha_abc')) b.push(r);
|
|
150
|
+
assert.strictEqual(a.length, b.length);
|
|
151
|
+
// Order may differ since both run in parallel; compare by id set.
|
|
152
|
+
const idsA = new Set(a.map((r) => r.agent_id));
|
|
153
|
+
const idsB = new Set(b.map((r) => r.agent_id));
|
|
154
|
+
assert.deepStrictEqual([...idsA].sort(), [...idsB].sort());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('mva-dispatcher: Test 6 -- AgentContext carries sentence_sha256 ONLY', async () => {
|
|
158
|
+
// Pre-condition: env var must be unset before dispatch.
|
|
159
|
+
delete process.env.MVA_SENTENCE;
|
|
160
|
+
assert.strictEqual(process.env.MVA_SENTENCE, undefined);
|
|
161
|
+
|
|
162
|
+
let observedCtx = null;
|
|
163
|
+
let envAtCall = null;
|
|
164
|
+
const agentDef = mkAgent('inspector', async (ctx, _signal) => {
|
|
165
|
+
observedCtx = ctx;
|
|
166
|
+
envAtCall = process.env.MVA_SENTENCE;
|
|
167
|
+
return { status: 'ok', payload: { sha: ctx.sentence_sha256 } };
|
|
168
|
+
});
|
|
169
|
+
const results = await dispatchToArray([agentDef], 'sha_xyz_789');
|
|
170
|
+
assert.strictEqual(results.length, 1);
|
|
171
|
+
assert.strictEqual(results[0].status, 'ok');
|
|
172
|
+
assert.strictEqual(results[0].payload.sha, 'sha_xyz_789');
|
|
173
|
+
|
|
174
|
+
// Forbidden keys: must NOT be present.
|
|
175
|
+
assert.ok(observedCtx, 'agent must have observed ctx');
|
|
176
|
+
assert.strictEqual(observedCtx.sentence_sha256, 'sha_xyz_789');
|
|
177
|
+
assert.strictEqual(observedCtx.sentence, undefined);
|
|
178
|
+
assert.strictEqual(observedCtx.prompt, undefined);
|
|
179
|
+
assert.strictEqual(observedCtx.raw_sentence, undefined);
|
|
180
|
+
assert.strictEqual(observedCtx.raw_text, undefined);
|
|
181
|
+
|
|
182
|
+
// Env escape hatch must NOT exist during dispatch.
|
|
183
|
+
assert.strictEqual(envAtCall, undefined);
|
|
184
|
+
|
|
185
|
+
// Post-condition: env still clean.
|
|
186
|
+
assert.strictEqual(process.env.MVA_SENTENCE, undefined);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('mva-dispatcher: Test 7 -- all-fail returns N error/timeout results (B7 fallback path)', async () => {
|
|
190
|
+
const agents = Array.from({ length: 6 }, (_, i) =>
|
|
191
|
+
mkAgent(`agent_${i}`, async () => { throw new Error('forced fail'); })
|
|
192
|
+
);
|
|
193
|
+
const results = await dispatchToArray(agents, 'sha_abc');
|
|
194
|
+
assert.strictEqual(results.length, 6);
|
|
195
|
+
for (const r of results) {
|
|
196
|
+
assert.strictEqual(r.status, 'error');
|
|
197
|
+
assert.strictEqual(r.error, 'forced fail');
|
|
198
|
+
}
|
|
199
|
+
// The dispatcher MUST NOT throw on all-fail. Plan 118-03 renderer reads
|
|
200
|
+
// the results array and decides the sharp-question fallback when every
|
|
201
|
+
// status is non-ok.
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('mva-dispatcher: Test 8 -- per-agent throw does not crash sibling agents', async () => {
|
|
205
|
+
const agents = [
|
|
206
|
+
mkAgent('thrower', async () => { throw new Error('boom'); }),
|
|
207
|
+
mkAgent('happy_1', async () => ({ status: 'ok', payload: 'h1' })),
|
|
208
|
+
mkAgent('happy_2', async () => ({ status: 'ok', payload: 'h2' }))
|
|
209
|
+
];
|
|
210
|
+
const results = await dispatchToArray(agents, 'sha_abc');
|
|
211
|
+
assert.strictEqual(results.length, 3);
|
|
212
|
+
const byId = Object.fromEntries(results.map((r) => [r.agent_id, r]));
|
|
213
|
+
assert.strictEqual(byId.thrower.status, 'error');
|
|
214
|
+
assert.strictEqual(byId.happy_1.status, 'ok');
|
|
215
|
+
assert.strictEqual(byId.happy_2.status, 'ok');
|
|
216
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-05 Plan 05 -- mva-option-router.
|
|
5
|
+
*
|
|
6
|
+
* Routes the user's selection from the 3-option footer that renders after the
|
|
7
|
+
* 30-second MVA brief (per binding decision B4 + Canon Part 3 verbs 7/8/5).
|
|
8
|
+
*
|
|
9
|
+
* Option 1 -> stay_in_just_talk (Canon verb 7 Synthesize): operator transitions
|
|
10
|
+
* to JUST_TALK; the brief stays in scrollback; user can ask any
|
|
11
|
+
* follow-up.
|
|
12
|
+
* Option 2 -> phase_119_stub (Canon verb 8 Bank Opportunity, deferred): surfaces
|
|
13
|
+
* the STUB_MESSAGE_119 verbatim per binding decision B6 OPTION A.
|
|
14
|
+
* Does NOT invoke /mos:new-project (that wiring lands in Phase 119
|
|
15
|
+
* v1.13.0-beta.18).
|
|
16
|
+
* Option 3 -> invoke_challenge_assumptions (Canon verb 5 Devil's Advocate):
|
|
17
|
+
* operator transitions to METHODOLOGY; the wrapper invokes
|
|
18
|
+
* /mos:challenge-assumptions --from-brief <sha8>.
|
|
19
|
+
*
|
|
20
|
+
* CRITICAL-3 part 2 wire:
|
|
21
|
+
* resolveCurrentSha8() reads ~/.mindrian/mva/state.json (the manifest written
|
|
22
|
+
* atomically by Plan 118-03 orchestrator after mva_brief_rendered emission)
|
|
23
|
+
* and returns the latest sha8 -- or null if the manifest is absent (fresh
|
|
24
|
+
* install / Hebrew refusal short-circuit). The /mos:mva-option command
|
|
25
|
+
* wrapper uses this to auto-discover the most recent brief when the user
|
|
26
|
+
* types `1`, `2`, or `3` without an explicit sha argument.
|
|
27
|
+
*
|
|
28
|
+
* Canon Part 8 invariants:
|
|
29
|
+
* - This module reads only sentence_sha256 (one-way hash) + agent payload
|
|
30
|
+
* scalars from the side-file. It NEVER reads .sentence, .raw_sentence,
|
|
31
|
+
* .prompt, or any free-text source string.
|
|
32
|
+
* - Telemetry payload carries ONLY sentence_sha256 + option_id +
|
|
33
|
+
* time_to_click_ms. No content. No URLs.
|
|
34
|
+
* - mva_option_selected event uses the ALLOWED_FIELDS frozen schema from
|
|
35
|
+
* mva-telemetry.cjs; validation rejects unknown fields and over-long
|
|
36
|
+
* strings.
|
|
37
|
+
*
|
|
38
|
+
* Em-dash invariant:
|
|
39
|
+
* STUB_MESSAGE_119 + OPTION_BEHAVIOR narratives use `--`, NEVER `—`. Test 7
|
|
40
|
+
* asserts the rendered text; Test 16 asserts both the command markdown and
|
|
41
|
+
* the skill markdown.
|
|
42
|
+
*
|
|
43
|
+
* Hermetic test isolation:
|
|
44
|
+
* All paths are derived from process.env.HOME via _home() at call time, so
|
|
45
|
+
* tests can mkdtempSync a fresh HOME, swap process.env.HOME, and verify the
|
|
46
|
+
* side-file / state.json / telemetry surfaces independently.
|
|
47
|
+
*
|
|
48
|
+
* Pure CJS, node built-ins only. Zero new runtime dependencies.
|
|
49
|
+
*/
|
|
50
|
+
'use strict';
|
|
51
|
+
|
|
52
|
+
const fs = require('node:fs');
|
|
53
|
+
const path = require('node:path');
|
|
54
|
+
const os = require('node:os');
|
|
55
|
+
|
|
56
|
+
const { transitionViaMVAOption } = require('../conversation/operator.cjs');
|
|
57
|
+
const telemetry = require('./mva-telemetry.cjs');
|
|
58
|
+
|
|
59
|
+
// ---------- Frozen strings ----------
|
|
60
|
+
|
|
61
|
+
// Per binding decision B6 OPTION A (verbatim text). Em-dash-free.
|
|
62
|
+
const STUB_MESSAGE_119 = [
|
|
63
|
+
'Building a room around this is the next layer; shipping in beta.18 (Phase 119).',
|
|
64
|
+
'For now, press option 1 to keep this brief visible, or option 3 to go deeper.',
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
const OPTION_BEHAVIOR = Object.freeze({
|
|
68
|
+
1: Object.freeze({
|
|
69
|
+
action: 'stay_in_just_talk',
|
|
70
|
+
next_operator: 'JUST_TALK',
|
|
71
|
+
canon_verb: 7, // Synthesize
|
|
72
|
+
narrative: 'Keeping the brief in scrollback. Ask me anything about what you just saw.',
|
|
73
|
+
}),
|
|
74
|
+
2: Object.freeze({
|
|
75
|
+
action: 'phase_119_stub',
|
|
76
|
+
next_operator: null,
|
|
77
|
+
canon_verb: 8, // Bank Opportunity (deferred)
|
|
78
|
+
narrative: STUB_MESSAGE_119,
|
|
79
|
+
}),
|
|
80
|
+
3: Object.freeze({
|
|
81
|
+
action: 'invoke_challenge_assumptions',
|
|
82
|
+
next_operator: 'METHODOLOGY',
|
|
83
|
+
canon_verb: 5, // Devil's Advocate
|
|
84
|
+
narrative: "Going deeper. Pulling the brief into a Devil's Advocate pass.",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---------- Path resolvers (env-aware for hermetic testing) ----------
|
|
89
|
+
|
|
90
|
+
function _home() {
|
|
91
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _statePath() {
|
|
95
|
+
return path.join(_home(), '.mindrian', 'mva', 'state.json');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _sideFilePath(sha8) {
|
|
99
|
+
return path.join(_home(), '.mindrian', 'mva', 'briefs', sha8 + '.json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _telemetryFilePath() {
|
|
103
|
+
return path.join(_home(), '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------- CRITICAL-3 part 2 wire: resolveCurrentSha8 ----------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* resolveCurrentSha8() -> string | null
|
|
110
|
+
*
|
|
111
|
+
* Reads ~/.mindrian/mva/state.json (the manifest written atomically by Plan
|
|
112
|
+
* 118-03 orchestrator after mva_brief_rendered emission) and returns the
|
|
113
|
+
* `current_sha8` field. Returns null when:
|
|
114
|
+
* - state.json does not exist (fresh install OR Hebrew refusal short-circuit)
|
|
115
|
+
* - state.json is unreadable or invalid JSON
|
|
116
|
+
* - current_sha8 is missing or not a string
|
|
117
|
+
*
|
|
118
|
+
* Staleness is NOT enforced here -- the caller (router or wrapper) can choose
|
|
119
|
+
* to compare rendered_at_ms against an expiration threshold and surface a
|
|
120
|
+
* "brief expired" message. This function's only job is to return the most
|
|
121
|
+
* recent known sha8 from the manifest.
|
|
122
|
+
*/
|
|
123
|
+
function resolveCurrentSha8() {
|
|
124
|
+
try {
|
|
125
|
+
const p = _statePath();
|
|
126
|
+
if (!fs.existsSync(p)) return null;
|
|
127
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
128
|
+
const manifest = JSON.parse(raw);
|
|
129
|
+
if (manifest && typeof manifest.current_sha8 === 'string' && manifest.current_sha8.length > 0) {
|
|
130
|
+
return manifest.current_sha8;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
} catch (_) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- Internal helpers ----------
|
|
139
|
+
|
|
140
|
+
function _readSideFile(sha8) {
|
|
141
|
+
try {
|
|
142
|
+
const p = _sideFilePath(sha8);
|
|
143
|
+
if (!fs.existsSync(p)) return null;
|
|
144
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
145
|
+
} catch (_) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find the most recent mva_brief_rendered event for the given sentence_sha256
|
|
152
|
+
* in ~/.mindrian/telemetry/v1.13/mva.jsonl. Returns the parsed event object
|
|
153
|
+
* or null if none found.
|
|
154
|
+
*/
|
|
155
|
+
function _readLastBriefRenderedEvent(sentenceSha256) {
|
|
156
|
+
try {
|
|
157
|
+
const p = _telemetryFilePath();
|
|
158
|
+
if (!fs.existsSync(p)) return null;
|
|
159
|
+
const text = fs.readFileSync(p, 'utf8').trim();
|
|
160
|
+
if (!text) return null;
|
|
161
|
+
const lines = text.split('\n');
|
|
162
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
163
|
+
if (!lines[i]) continue;
|
|
164
|
+
let evt;
|
|
165
|
+
try { evt = JSON.parse(lines[i]); } catch (_) { continue; }
|
|
166
|
+
if (evt.event === 'mva_brief_rendered' && evt.sentence_sha256 === sentenceSha256) {
|
|
167
|
+
return evt;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
} catch (_) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------- Public: routeOption ----------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* routeOption(optionId, sha8, opts?) -> Promise<{
|
|
180
|
+
* ok: boolean,
|
|
181
|
+
* action?: 'stay_in_just_talk' | 'phase_119_stub' | 'invoke_challenge_assumptions',
|
|
182
|
+
* message?: string,
|
|
183
|
+
* next_state?: string | null,
|
|
184
|
+
* time_to_click_ms?: number,
|
|
185
|
+
* invoke_command?: string, // option 3 only
|
|
186
|
+
* error?: string, // on failure
|
|
187
|
+
* no_transition?: boolean,
|
|
188
|
+
* reason?: string
|
|
189
|
+
* }>
|
|
190
|
+
*
|
|
191
|
+
* opts:
|
|
192
|
+
* roomDir: filesystem path used by transitionViaMVAOption for operator state
|
|
193
|
+
* (defaults to process.cwd())
|
|
194
|
+
*
|
|
195
|
+
* Validation order (matches Test 4 / Test 5 / Test 8 expectations):
|
|
196
|
+
* 1. optionId in {1, 2, 3} else -> invalid_option (no I/O, no telemetry)
|
|
197
|
+
* 2. side-file present for sha8 else -> brief_not_found (no telemetry)
|
|
198
|
+
* 3. mva_brief_rendered event present for the brief's sha256 else ->
|
|
199
|
+
* brief_still_rendering (no telemetry; OQ16 lean)
|
|
200
|
+
* 4. compute time_to_click_ms from (now - mva_brief_rendered.timestamp)
|
|
201
|
+
* 5. transition operator via transitionViaMVAOption(roomDir, optionId)
|
|
202
|
+
* 6. emit mva_option_selected telemetry (sentence_sha256 + option_id +
|
|
203
|
+
* time_to_click_ms; ALLOWED_FIELDS-validated by mva-telemetry)
|
|
204
|
+
* 7. return action + message + next_state (+ invoke_command on option 3)
|
|
205
|
+
*/
|
|
206
|
+
async function routeOption(optionId, sha8, opts) {
|
|
207
|
+
opts = opts || {};
|
|
208
|
+
|
|
209
|
+
// (1) Strict option validation -- runs BEFORE any I/O so invalid options
|
|
210
|
+
// are cheap to reject and never emit telemetry.
|
|
211
|
+
if (!Number.isInteger(optionId) || ![1, 2, 3].includes(optionId)) {
|
|
212
|
+
return { ok: false, error: 'invalid_option', valid_options: [1, 2, 3] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// (2) Side-file lookup.
|
|
216
|
+
if (typeof sha8 !== 'string' || sha8.length === 0) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: 'brief_not_found',
|
|
220
|
+
message: 'The brief data has expired or was not deployed. Type your sentence again to re-fire the pipeline.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const brief = _readSideFile(sha8);
|
|
224
|
+
if (!brief || typeof brief.sha256 !== 'string') {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: 'brief_not_found',
|
|
228
|
+
message: 'The brief data has expired or was not deployed. Type your sentence again to re-fire the pipeline.',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// (3) mva_brief_rendered lookup (the "is the brief done streaming?" signal).
|
|
233
|
+
const lastRendered = _readLastBriefRenderedEvent(brief.sha256);
|
|
234
|
+
if (!lastRendered || !lastRendered.timestamp) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: 'brief_still_rendering',
|
|
238
|
+
message: 'Brief is still rendering -- options will activate when it completes.',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// (4) Compute time-to-click.
|
|
243
|
+
const renderedAtMs = new Date(lastRendered.timestamp).getTime();
|
|
244
|
+
const nowMs = Date.now();
|
|
245
|
+
const time_to_click_ms = Math.max(0, nowMs - renderedAtMs);
|
|
246
|
+
|
|
247
|
+
// (5) Operator transition. transitionViaMVAOption handles the per-option
|
|
248
|
+
// rules (1 -> JUST_TALK, 2 -> no-op, 3 -> METHODOLOGY).
|
|
249
|
+
const roomDir = typeof opts.roomDir === 'string' ? opts.roomDir : process.cwd();
|
|
250
|
+
const transitionResult = transitionViaMVAOption(roomDir, optionId);
|
|
251
|
+
|
|
252
|
+
// (6) Telemetry. Best-effort: telemetry.emit() validates against
|
|
253
|
+
// ALLOWED_FIELDS.mva_option_selected and writes JSONL to
|
|
254
|
+
// ~/.mindrian/telemetry/v1.13/mva.jsonl. Validation throws (so we
|
|
255
|
+
// cannot leak invalid payloads); disk failures are swallowed by emit().
|
|
256
|
+
try {
|
|
257
|
+
telemetry.emit('mva_option_selected', {
|
|
258
|
+
sentence_sha256: brief.sha256,
|
|
259
|
+
option_id: optionId,
|
|
260
|
+
time_to_click_ms: time_to_click_ms,
|
|
261
|
+
});
|
|
262
|
+
} catch (_) {
|
|
263
|
+
// Validation error here would be a programming bug (we control the
|
|
264
|
+
// payload shape). Swallow to keep the user-facing flow intact; the
|
|
265
|
+
// test harness will surface the issue.
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// (7) Build response.
|
|
269
|
+
const behavior = OPTION_BEHAVIOR[optionId];
|
|
270
|
+
const out = {
|
|
271
|
+
ok: true,
|
|
272
|
+
action: behavior.action,
|
|
273
|
+
message: behavior.narrative,
|
|
274
|
+
next_state: transitionResult && transitionResult.ok ? (transitionResult.new_state || null) : null,
|
|
275
|
+
time_to_click_ms: time_to_click_ms,
|
|
276
|
+
};
|
|
277
|
+
if (transitionResult && transitionResult.no_transition) {
|
|
278
|
+
out.no_transition = true;
|
|
279
|
+
out.reason = transitionResult.reason;
|
|
280
|
+
}
|
|
281
|
+
if (optionId === 3) {
|
|
282
|
+
out.invoke_command = '/mos:challenge-assumptions --from-brief ' + sha8;
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
routeOption,
|
|
289
|
+
resolveCurrentSha8,
|
|
290
|
+
OPTION_BEHAVIOR,
|
|
291
|
+
STUB_MESSAGE_119,
|
|
292
|
+
};
|