@mindrian_os/install 1.13.0-beta.14 → 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 +15 -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/cache-prune.cjs +114 -8
- package/lib/core/install-state.cjs +242 -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,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-01 Plan 01 Task 1 -- mva-agent-contract.
|
|
5
|
+
*
|
|
6
|
+
* The AgentResult shape + the runAgent wrapper used by mva-dispatcher.cjs
|
|
7
|
+
* (Task 3 of this plan). Plan 118-02 implements 6 specific agents against
|
|
8
|
+
* the Agent contract documented below.
|
|
9
|
+
*
|
|
10
|
+
* AgentContext interface
|
|
11
|
+
* ----------------------
|
|
12
|
+
* {
|
|
13
|
+
* sentence_sha256: string, // dispatcher-provided -- ONLY identifier passed to agents
|
|
14
|
+
* remaining_budget_ms: number, // dispatcher-provided
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* raw_sentence is NEVER exposed to agents (Canon Part 8 hard invariant).
|
|
18
|
+
* Agents that need linguistic features must derive them from neighborhood-graph
|
|
19
|
+
* queries against room.db (sentence-sha8 only), NOT from the raw sentence string.
|
|
20
|
+
*
|
|
21
|
+
* process.env.MVA_SENTENCE is NEVER set. There is no escape hatch.
|
|
22
|
+
*
|
|
23
|
+
* Agent function signature
|
|
24
|
+
* ------------------------
|
|
25
|
+
* async function agent(context, signal) -> { status: 'ok'|'empty', payload } | null
|
|
26
|
+
*
|
|
27
|
+
* If the agent throws, runAgent wraps with status='error'.
|
|
28
|
+
* If the AbortSignal fires before resolve, runAgent wraps with status='timeout'.
|
|
29
|
+
* If the agent returns null, runAgent wraps with status='empty'.
|
|
30
|
+
*
|
|
31
|
+
* Pure CJS, node built-ins only, zero new runtime dependencies.
|
|
32
|
+
* Canon Part 8 invariants:
|
|
33
|
+
* - NEVER serialize stack traces (stack traces can contain user content).
|
|
34
|
+
* - Error messages capped at 200 chars (sliced to prevent user-content blow-through).
|
|
35
|
+
* - The AgentContext object passed to the agent contains ONLY documented keys.
|
|
36
|
+
*/
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const ALLOWED_STATUSES = new Set(['ok', 'empty', 'error', 'timeout']);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* AGENT_RESULT_SHAPE -- a frozen object documenting the AgentResult shape.
|
|
43
|
+
*
|
|
44
|
+
* AgentResult = {
|
|
45
|
+
* agent_id: string, // 'brain_similar' | ... | 'dashboard_graph'
|
|
46
|
+
* status: 'ok' | 'empty' | 'error' | 'timeout',
|
|
47
|
+
* duration_ms: number, // wall-clock from runAgent invocation
|
|
48
|
+
* payload?: any, // agent-specific; opaque to dispatcher
|
|
49
|
+
* error?: string, // sanitized message (<= 200 chars)
|
|
50
|
+
* };
|
|
51
|
+
*/
|
|
52
|
+
const AGENT_RESULT_SHAPE = Object.freeze({
|
|
53
|
+
agent_id: 'string',
|
|
54
|
+
status: "'ok' | 'empty' | 'error' | 'timeout'",
|
|
55
|
+
duration_ms: 'number >= 0',
|
|
56
|
+
payload: 'optional any',
|
|
57
|
+
error: 'optional string'
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* validateAgentResult -- returns true if the result conforms to AgentResult shape.
|
|
62
|
+
*
|
|
63
|
+
* @param {unknown} result
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function validateAgentResult(result) {
|
|
67
|
+
if (!result || typeof result !== 'object') return false;
|
|
68
|
+
if (typeof result.agent_id !== 'string' || result.agent_id.length === 0) return false;
|
|
69
|
+
if (typeof result.status !== 'string' || !ALLOWED_STATUSES.has(result.status)) return false;
|
|
70
|
+
if (typeof result.duration_ms !== 'number' || result.duration_ms < 0) return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sanitize an error into a short message. Drops stack traces, drops object
|
|
76
|
+
* properties, slices to 200 chars to prevent any user-content blow-through.
|
|
77
|
+
*
|
|
78
|
+
* @param {unknown} err
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function sanitizeError(err) {
|
|
82
|
+
let msg;
|
|
83
|
+
if (err && typeof err === 'object' && typeof err.message === 'string') {
|
|
84
|
+
msg = err.message;
|
|
85
|
+
} else {
|
|
86
|
+
msg = String(err);
|
|
87
|
+
}
|
|
88
|
+
return msg.slice(0, 200);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* runAgent -- invoke an agent function under an AbortController timeout. Catches
|
|
93
|
+
* any throw and converts to a structured AgentResult.
|
|
94
|
+
*
|
|
95
|
+
* @param {{ id: string, fn: Function }} agentDef
|
|
96
|
+
* @param {{ sentence_sha256: string }} context
|
|
97
|
+
* @param {{ timeoutMs?: number }} opts
|
|
98
|
+
* @returns {Promise<{ agent_id: string, status: string, duration_ms: number, payload?: any, error?: string }>}
|
|
99
|
+
*/
|
|
100
|
+
async function runAgent(agentDef, context, opts) {
|
|
101
|
+
const timeoutMs = (opts && typeof opts.timeoutMs === 'number') ? opts.timeoutMs : 35000;
|
|
102
|
+
const t0 = Date.now();
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
|
|
105
|
+
// Build an AgentContext containing ONLY documented keys. We do not spread
|
|
106
|
+
// arbitrary caller props (defense-in-depth against accidental raw_sentence
|
|
107
|
+
// leakage). Canon Part 8 invariant.
|
|
108
|
+
const agentContext = {
|
|
109
|
+
sentence_sha256: context && context.sentence_sha256,
|
|
110
|
+
remaining_budget_ms: timeoutMs
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let timer;
|
|
114
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
controller.abort();
|
|
117
|
+
reject(new Error('__mva_dispatcher_timeout__'));
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const result = await Promise.race([
|
|
123
|
+
Promise.resolve().then(() => agentDef.fn(agentContext, controller.signal)),
|
|
124
|
+
timeoutPromise
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
if (timer) clearTimeout(timer);
|
|
128
|
+
|
|
129
|
+
const duration_ms = Date.now() - t0;
|
|
130
|
+
|
|
131
|
+
if (result === null || result === undefined) {
|
|
132
|
+
return { agent_id: agentDef.id, status: 'empty', duration_ms };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Result is { status: 'ok' | 'empty', payload }.
|
|
136
|
+
if (result && typeof result === 'object' && typeof result.status === 'string') {
|
|
137
|
+
if (result.status === 'ok' || result.status === 'empty') {
|
|
138
|
+
return {
|
|
139
|
+
agent_id: agentDef.id,
|
|
140
|
+
status: result.status,
|
|
141
|
+
duration_ms,
|
|
142
|
+
payload: result.payload
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Unknown shape -- treat as empty (defense-in-depth; agent contract violation).
|
|
148
|
+
return { agent_id: agentDef.id, status: 'empty', duration_ms };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (timer) clearTimeout(timer);
|
|
151
|
+
const duration_ms = Date.now() - t0;
|
|
152
|
+
|
|
153
|
+
if (controller.signal.aborted) {
|
|
154
|
+
return { agent_id: agentDef.id, status: 'timeout', duration_ms };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
agent_id: agentDef.id,
|
|
159
|
+
status: 'error',
|
|
160
|
+
duration_ms,
|
|
161
|
+
error: sanitizeError(err)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
runAgent,
|
|
168
|
+
validateAgentResult,
|
|
169
|
+
AGENT_RESULT_SHAPE
|
|
170
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-01 Plan 01 Task 1 -- mva-agent-contract tests.
|
|
5
|
+
*
|
|
6
|
+
* Tests the AgentResult shape, validateAgentResult, and runAgent wrapper.
|
|
7
|
+
* Per Canon Part 8 (Graph Boundary): AgentContext NEVER carries raw sentence;
|
|
8
|
+
* the only sentence-derived field is sentence_sha256. There is no escape hatch
|
|
9
|
+
* via process.env.MVA_SENTENCE.
|
|
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 {
|
|
19
|
+
runAgent,
|
|
20
|
+
validateAgentResult,
|
|
21
|
+
AGENT_RESULT_SHAPE
|
|
22
|
+
} = require('./mva-agent-contract.cjs');
|
|
23
|
+
|
|
24
|
+
test('mva-agent-contract: Test 1 -- agent returns ok wraps result', async () => {
|
|
25
|
+
const agentDef = {
|
|
26
|
+
id: 'test',
|
|
27
|
+
fn: async (_ctx, _signal) => ({ status: 'ok', payload: { x: 1 } })
|
|
28
|
+
};
|
|
29
|
+
const result = await runAgent(
|
|
30
|
+
agentDef,
|
|
31
|
+
{ sentence_sha256: 'abc123' },
|
|
32
|
+
{ timeoutMs: 1000 }
|
|
33
|
+
);
|
|
34
|
+
assert.strictEqual(result.agent_id, 'test');
|
|
35
|
+
assert.strictEqual(result.status, 'ok');
|
|
36
|
+
assert.deepStrictEqual(result.payload, { x: 1 });
|
|
37
|
+
assert.strictEqual(typeof result.duration_ms, 'number');
|
|
38
|
+
assert.ok(result.duration_ms >= 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('mva-agent-contract: Test 2 -- agent throws becomes error result', async () => {
|
|
42
|
+
const agentDef = {
|
|
43
|
+
id: 'test',
|
|
44
|
+
fn: async (_ctx, _signal) => { throw new Error('boom'); }
|
|
45
|
+
};
|
|
46
|
+
const result = await runAgent(
|
|
47
|
+
agentDef,
|
|
48
|
+
{ sentence_sha256: 'abc123' },
|
|
49
|
+
{ timeoutMs: 1000 }
|
|
50
|
+
);
|
|
51
|
+
assert.strictEqual(result.agent_id, 'test');
|
|
52
|
+
assert.strictEqual(result.status, 'error');
|
|
53
|
+
assert.strictEqual(result.error, 'boom');
|
|
54
|
+
assert.ok(!('stack' in result), 'result must never carry stack');
|
|
55
|
+
assert.strictEqual(typeof result.duration_ms, 'number');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('mva-agent-contract: Test 3 -- agent returns null becomes empty', async () => {
|
|
59
|
+
const agentDef = {
|
|
60
|
+
id: 'test',
|
|
61
|
+
fn: async (_ctx, _signal) => null
|
|
62
|
+
};
|
|
63
|
+
const result = await runAgent(
|
|
64
|
+
agentDef,
|
|
65
|
+
{ sentence_sha256: 'abc123' },
|
|
66
|
+
{ timeoutMs: 1000 }
|
|
67
|
+
);
|
|
68
|
+
assert.strictEqual(result.agent_id, 'test');
|
|
69
|
+
assert.strictEqual(result.status, 'empty');
|
|
70
|
+
assert.strictEqual(typeof result.duration_ms, 'number');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('mva-agent-contract: Test 4 -- agent exceeds timeout returns timeout', async () => {
|
|
74
|
+
const agentDef = {
|
|
75
|
+
id: 'test',
|
|
76
|
+
fn: async (_ctx, signal) => {
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
const t = setTimeout(resolve, 500);
|
|
79
|
+
signal.addEventListener('abort', () => {
|
|
80
|
+
clearTimeout(t);
|
|
81
|
+
reject(new Error('aborted'));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return { status: 'ok', payload: {} };
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const t0 = Date.now();
|
|
88
|
+
const result = await runAgent(
|
|
89
|
+
agentDef,
|
|
90
|
+
{ sentence_sha256: 'abc123' },
|
|
91
|
+
{ timeoutMs: 100 }
|
|
92
|
+
);
|
|
93
|
+
const elapsed = Date.now() - t0;
|
|
94
|
+
assert.strictEqual(result.agent_id, 'test');
|
|
95
|
+
assert.strictEqual(result.status, 'timeout');
|
|
96
|
+
assert.ok(elapsed < 300, `elapsed ${elapsed} must be under 300ms`);
|
|
97
|
+
assert.ok(result.duration_ms >= 90, `duration_ms ${result.duration_ms} should be ~100`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('mva-agent-contract: Test 5 -- agent receives both args, signal observed', async () => {
|
|
101
|
+
let observedSignalAbortedAtStart = null;
|
|
102
|
+
let observedSignalAbortedAtEnd = null;
|
|
103
|
+
let observedSha = null;
|
|
104
|
+
let observedRemaining = null;
|
|
105
|
+
const agentDef = {
|
|
106
|
+
id: 'test',
|
|
107
|
+
fn: async (ctx, signal) => {
|
|
108
|
+
observedSignalAbortedAtStart = signal.aborted;
|
|
109
|
+
observedSha = ctx.sentence_sha256;
|
|
110
|
+
observedRemaining = ctx.remaining_budget_ms;
|
|
111
|
+
// Wait long enough to be cancelled
|
|
112
|
+
await new Promise((resolve, reject) => {
|
|
113
|
+
const t = setTimeout(resolve, 500);
|
|
114
|
+
signal.addEventListener('abort', () => {
|
|
115
|
+
clearTimeout(t);
|
|
116
|
+
observedSignalAbortedAtEnd = signal.aborted;
|
|
117
|
+
reject(new Error('aborted'));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return { status: 'ok', payload: {} };
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const result = await runAgent(
|
|
124
|
+
agentDef,
|
|
125
|
+
{ sentence_sha256: 'def456' },
|
|
126
|
+
{ timeoutMs: 100 }
|
|
127
|
+
);
|
|
128
|
+
assert.strictEqual(observedSignalAbortedAtStart, false, 'signal must start non-aborted');
|
|
129
|
+
assert.strictEqual(observedSignalAbortedAtEnd, true, 'signal must be aborted on timeout');
|
|
130
|
+
assert.strictEqual(observedSha, 'def456');
|
|
131
|
+
assert.strictEqual(observedRemaining, 100);
|
|
132
|
+
assert.strictEqual(result.status, 'timeout');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('mva-agent-contract: Test 6 -- validateAgentResult', () => {
|
|
136
|
+
assert.strictEqual(
|
|
137
|
+
validateAgentResult({ agent_id: 'x', status: 'ok', duration_ms: 10 }),
|
|
138
|
+
true
|
|
139
|
+
);
|
|
140
|
+
assert.strictEqual(
|
|
141
|
+
validateAgentResult({ agent_id: 'x', status: 'empty', duration_ms: 10 }),
|
|
142
|
+
true
|
|
143
|
+
);
|
|
144
|
+
assert.strictEqual(
|
|
145
|
+
validateAgentResult({ agent_id: 'x', status: 'error', duration_ms: 10, error: 'e' }),
|
|
146
|
+
true
|
|
147
|
+
);
|
|
148
|
+
assert.strictEqual(
|
|
149
|
+
validateAgentResult({ agent_id: 'x', status: 'timeout', duration_ms: 10 }),
|
|
150
|
+
true
|
|
151
|
+
);
|
|
152
|
+
assert.strictEqual(validateAgentResult(null), false);
|
|
153
|
+
assert.strictEqual(validateAgentResult({}), false);
|
|
154
|
+
assert.strictEqual(validateAgentResult({ status: 'ok', duration_ms: 10 }), false);
|
|
155
|
+
assert.strictEqual(validateAgentResult({ agent_id: 'x', duration_ms: 10 }), false);
|
|
156
|
+
assert.strictEqual(
|
|
157
|
+
validateAgentResult({ agent_id: 'x', status: 'invalid', duration_ms: 10 }),
|
|
158
|
+
false
|
|
159
|
+
);
|
|
160
|
+
assert.strictEqual(
|
|
161
|
+
validateAgentResult({ agent_id: 'x', status: 'ok', duration_ms: -1 }),
|
|
162
|
+
false
|
|
163
|
+
);
|
|
164
|
+
assert.strictEqual(
|
|
165
|
+
validateAgentResult({ agent_id: 'x', status: 'ok' }),
|
|
166
|
+
false
|
|
167
|
+
);
|
|
168
|
+
assert.ok(AGENT_RESULT_SHAPE, 'shape doc must be exported');
|
|
169
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-01 Plan 01 Task 2 -- mva-budget.
|
|
5
|
+
*
|
|
6
|
+
* Global wall-clock budget tracker used by mva-dispatcher.cjs. Per binding
|
|
7
|
+
* decision B2 (HARD):
|
|
8
|
+
* GLOBAL_BUDGET_MS = 45000 (the hard 30-Second-MVA cap; named for the
|
|
9
|
+
* sharp-question fallback window per source spec
|
|
10
|
+
* line 113 -- "Hard budget: 45 seconds maximum")
|
|
11
|
+
* PER_AGENT_CAP_MS = 35000 (each agent gets at most 35s OR remaining
|
|
12
|
+
* global, whichever is less -- source spec
|
|
13
|
+
* line 123 -- "Agents return structured JSON
|
|
14
|
+
* within 35s budget each")
|
|
15
|
+
*
|
|
16
|
+
* The dispatcher creates ONE budget per dispatch call. Each agent's per-agent
|
|
17
|
+
* abort signal feeds from this budget via perAgentMs(): min(35000, remainingMs()).
|
|
18
|
+
*
|
|
19
|
+
* Pure CJS, leaf module -- no dependencies on any other lib/core file.
|
|
20
|
+
* Canon Part 8: this module touches NO user content; it is wall-clock math only.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const GLOBAL_BUDGET_MS = 45000;
|
|
25
|
+
const PER_AGENT_CAP_MS = 35000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a wall-clock budget tracker.
|
|
29
|
+
*
|
|
30
|
+
* @param {number} [globalBudgetMs=45000]
|
|
31
|
+
* @returns {{
|
|
32
|
+
* startedAt: number,
|
|
33
|
+
* globalBudgetMs: number,
|
|
34
|
+
* remainingMs: () => number,
|
|
35
|
+
* perAgentMs: (cap?: number) => number,
|
|
36
|
+
* isExpired: () => boolean,
|
|
37
|
+
* elapsedMs: () => number,
|
|
38
|
+
* }}
|
|
39
|
+
*/
|
|
40
|
+
function createBudget(globalBudgetMs) {
|
|
41
|
+
const cap = (typeof globalBudgetMs === 'number') ? globalBudgetMs : GLOBAL_BUDGET_MS;
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
|
|
44
|
+
function remainingMs() {
|
|
45
|
+
return Math.max(0, cap - (Date.now() - startedAt));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function perAgentMs(perAgentCapMs) {
|
|
49
|
+
const requested = (typeof perAgentCapMs === 'number') ? perAgentCapMs : PER_AGENT_CAP_MS;
|
|
50
|
+
return Math.max(0, Math.min(requested, remainingMs()));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isExpired() {
|
|
54
|
+
return remainingMs() === 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function elapsedMs() {
|
|
58
|
+
return Date.now() - startedAt;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
startedAt,
|
|
63
|
+
globalBudgetMs: cap,
|
|
64
|
+
remainingMs,
|
|
65
|
+
perAgentMs,
|
|
66
|
+
isExpired,
|
|
67
|
+
elapsedMs
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
createBudget,
|
|
73
|
+
GLOBAL_BUDGET_MS,
|
|
74
|
+
PER_AGENT_CAP_MS
|
|
75
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-01 Plan 01 Task 2 -- mva-budget tests.
|
|
5
|
+
*
|
|
6
|
+
* Per binding decision B2 (HARD):
|
|
7
|
+
* 45-second global wall-clock budget + 35-second per-agent budget.
|
|
8
|
+
* Per-agent abort signal feeds from the global deadline: each agent gets
|
|
9
|
+
* min(35s, remaining_global_budget).
|
|
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 {
|
|
19
|
+
createBudget,
|
|
20
|
+
GLOBAL_BUDGET_MS,
|
|
21
|
+
PER_AGENT_CAP_MS
|
|
22
|
+
} = require('./mva-budget.cjs');
|
|
23
|
+
|
|
24
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
|
|
26
|
+
test('mva-budget: Test 1 -- remaining decreases over time', async () => {
|
|
27
|
+
const budget = createBudget(45000);
|
|
28
|
+
const r0 = budget.remainingMs();
|
|
29
|
+
assert.ok(r0 >= 44990 && r0 <= 45000, `expected ~45000, got ${r0}`);
|
|
30
|
+
await sleep(100);
|
|
31
|
+
const r1 = budget.remainingMs();
|
|
32
|
+
// Allow generous slop for slow CI / GC pauses.
|
|
33
|
+
assert.ok(r1 >= 44800 && r1 <= 44910, `expected ~44900, got ${r1}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('mva-budget: Test 2 -- default globalBudgetMs is 45000', () => {
|
|
37
|
+
const budget = createBudget();
|
|
38
|
+
const r = budget.remainingMs();
|
|
39
|
+
assert.ok(r >= 44990 && r <= 45000, `expected ~45000, got ${r}`);
|
|
40
|
+
assert.strictEqual(budget.globalBudgetMs, 45000);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('mva-budget: Test 3 -- perAgentMs returns 35000 when remaining is 45000', () => {
|
|
44
|
+
const budget = createBudget(45000);
|
|
45
|
+
assert.strictEqual(budget.perAgentMs(35000), 35000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('mva-budget: Test 4 -- perAgentMs caps at remaining', async () => {
|
|
49
|
+
const budget = createBudget(200);
|
|
50
|
+
await sleep(150);
|
|
51
|
+
const cap = budget.perAgentMs(180);
|
|
52
|
+
// remaining is ~50; perAgentCap 180 -> min(180, 50) = ~50
|
|
53
|
+
assert.ok(cap >= 0 && cap <= 80, `expected ~50, got ${cap}`);
|
|
54
|
+
assert.ok(cap < 180, 'must NOT return 180 when remaining is lower');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('mva-budget: Test 5 -- isExpired flips true after globalBudgetMs', async () => {
|
|
58
|
+
const budget = createBudget(80);
|
|
59
|
+
assert.strictEqual(budget.isExpired(), false);
|
|
60
|
+
await sleep(120);
|
|
61
|
+
assert.strictEqual(budget.isExpired(), true);
|
|
62
|
+
assert.strictEqual(budget.remainingMs(), 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('mva-budget: Test 6 -- exported constants', () => {
|
|
66
|
+
assert.strictEqual(GLOBAL_BUDGET_MS, 45000);
|
|
67
|
+
assert.strictEqual(PER_AGENT_CAP_MS, 35000);
|
|
68
|
+
});
|