@mindrian_os/install 1.13.0-beta.16 → 1.13.0-beta.19
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 +36 -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 +4 -1
- 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 +4 -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 +4 -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 +58 -0
- package/commands/mva-option.md +91 -0
- package/commands/new-project.md +4 -0
- package/commands/onboard.md +22 -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 +31 -88
- package/lib/agents/auto-explore-agent.cjs +82 -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/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/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/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 +365 -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 +58 -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/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -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/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/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 +197 -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/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/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/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 +240 -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/mva-pipeline/SKILL.md +129 -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,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 119-01 -- LLM-suggested room name resolver.
|
|
4
|
+
*
|
|
5
|
+
* One-shot Haiku 4.5 call seeded with LOCAL Phase 117 auto-explore finding +
|
|
6
|
+
* Phase 118 MVA brief sentence. Returns a venture-shaped slug suggestion (e.g.
|
|
7
|
+
* 'acme-robotics', 'quantum-imaging', 'biotech-translation').
|
|
8
|
+
*
|
|
9
|
+
* Canon Part 8 invariant: this module MUST NOT invoke any Brain endpoint.
|
|
10
|
+
* The Brain repository holds GENERIC methodology -- framework chaining rules,
|
|
11
|
+
* phase progressions -- never user-specific content. A LLM call seeded with
|
|
12
|
+
* the user's auto-explore output + MVA brief sentence is purely LOCAL: the
|
|
13
|
+
* model sees the user's content directly via the local Anthropic API path,
|
|
14
|
+
* and the model's response is consumed locally.
|
|
15
|
+
*
|
|
16
|
+
* Canon Part 8 NOTE (REVISION 2026-05-16 Warning 5 fix): this module's fetch
|
|
17
|
+
* carries user content (the auto_explore_finding summary + the mva_brief_sentence)
|
|
18
|
+
* to api.anthropic.com. Per the standard plugin LLM usage pattern (precedent:
|
|
19
|
+
* lib/core/mva-classifier.cjs, lib/agents/mva/*.cjs, lib/chat/fabric-chat.cjs),
|
|
20
|
+
* this is acceptable: the Anthropic API is the LOCAL LLM transport for the
|
|
21
|
+
* plugin. The Canon Part 8 boundary covers ONLY the Mindrian-owned Brain MCP
|
|
22
|
+
* host (the Mindrian-owned methodology repository that must never receive user
|
|
23
|
+
* data) -- NOT api.anthropic.com (the Anthropic LLM transport). The two are
|
|
24
|
+
* distinct: Brain is a Mindrian-owned methodology repository that must never
|
|
25
|
+
* receive user data; api.anthropic.com is a stateless LLM transport.
|
|
26
|
+
*
|
|
27
|
+
* Tripwire: scaffold harness Gate 3 + Test 9 grep this module for any Brain-host
|
|
28
|
+
* substring AND any brain-client require AND any fetch to a brain.* URL; all
|
|
29
|
+
* three must return 0. This module body therefore avoids the literal Brain-host
|
|
30
|
+
* hostname string entirely (the scaffold harness uses literal-grep on the
|
|
31
|
+
* forbidden substring).
|
|
32
|
+
*
|
|
33
|
+
* Cost: ~$0.0005 per first-MVA completion (Haiku 4.5 input ~800 tokens,
|
|
34
|
+
* output ~10 tokens). See CONTEXT.md Architectural Decisions item 1.
|
|
35
|
+
*
|
|
36
|
+
* Em-dash discipline: uses `--` never the U+2014 character per memory
|
|
37
|
+
* feedback_no_emdashes.md.
|
|
38
|
+
*
|
|
39
|
+
* Graceful degradation: on LLM error, returns {ok: false, suggested_name:
|
|
40
|
+
* 'untitled', ...} so the F.1 selector still renders correctly with the
|
|
41
|
+
* fallback label `[name this room: untitled]`.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const FALLBACK_SUGGESTION = 'untitled';
|
|
45
|
+
|
|
46
|
+
// Phase 119-01 REVISION 2026-05-16 (Blocker 2 Option A): HAIKU_MODEL_ID is the
|
|
47
|
+
// project-wide source-of-truth constant. The plan's REVISION text says to
|
|
48
|
+
// import HAIKU_MODEL from lib/core/mva-classifier.cjs::HAIKU_MODEL, BUT
|
|
49
|
+
// inspection of that module's module.exports (verified at lib/core/mva-classifier.cjs
|
|
50
|
+
// line 359-370) shows HAIKU_MODEL is a module-internal const NOT exported. Per
|
|
51
|
+
// Rule 1 deviation, the constant is inlined here with provenance pointing to
|
|
52
|
+
// the source-of-truth declaration at lib/core/mva-classifier.cjs:53. If a
|
|
53
|
+
// future phase exports HAIKU_MODEL, replace the inline literal with a require.
|
|
54
|
+
const HAIKU_MODEL_ID = 'claude-haiku-4-5';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* suggestRoomName({auto_explore_finding, mva_brief_sentence, opts})
|
|
58
|
+
* @returns {Promise<{ok, suggested_name, model_used, latency_ms, error_short?}>}
|
|
59
|
+
*/
|
|
60
|
+
async function suggestRoomName(args) {
|
|
61
|
+
const opts = (args && typeof args === 'object') ? args : {};
|
|
62
|
+
const autoExploreFinding = opts.auto_explore_finding || null;
|
|
63
|
+
const mvaBriefSentence = (typeof opts.mva_brief_sentence === 'string') ? opts.mva_brief_sentence : '';
|
|
64
|
+
const llmClient = opts.llmClient || null;
|
|
65
|
+
|
|
66
|
+
const t0 = Date.now();
|
|
67
|
+
try {
|
|
68
|
+
const client = llmClient || _resolveProductionLlmClient();
|
|
69
|
+
const prompt = _buildLocalPrompt(autoExploreFinding, mvaBriefSentence);
|
|
70
|
+
const response = await client.complete({
|
|
71
|
+
model: HAIKU_MODEL_ID,
|
|
72
|
+
messages: [{ role: 'user', content: prompt }],
|
|
73
|
+
max_tokens: 20,
|
|
74
|
+
});
|
|
75
|
+
const raw = (response && typeof response.content === 'string') ? response.content : '';
|
|
76
|
+
const normalized = _normalizeSlug(raw);
|
|
77
|
+
const latency_ms = Date.now() - t0;
|
|
78
|
+
if (!normalized || normalized.length === 0) {
|
|
79
|
+
return { ok: false, suggested_name: FALLBACK_SUGGESTION, model_used: HAIKU_MODEL_ID, latency_ms, error_short: 'empty_response' };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, suggested_name: normalized, model_used: HAIKU_MODEL_ID, latency_ms };
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const latency_ms = Date.now() - t0;
|
|
84
|
+
const error_short = String(err && err.message || err).slice(0, 60);
|
|
85
|
+
return { ok: false, suggested_name: FALLBACK_SUGGESTION, model_used: HAIKU_MODEL_ID, latency_ms, error_short };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _buildLocalPrompt(autoExploreFinding, mvaBriefSentence) {
|
|
90
|
+
// Build a short prompt from LOCAL signals ONLY. The findings array carries
|
|
91
|
+
// the top whitespace + reverse-salient + cross-domain hits from Phase 117;
|
|
92
|
+
// the MVA brief sentence is the user's first conversational turn (already
|
|
93
|
+
// local). NEVER include any Brain-derived suggestion in this prompt.
|
|
94
|
+
const findingsSummary = _summarizeFindings(autoExploreFinding);
|
|
95
|
+
return [
|
|
96
|
+
'You are naming a venture. Suggest a 2-3 word kebab-case slug that captures',
|
|
97
|
+
'the core domain. Return ONLY the slug -- no prose, no quotes, no markdown.',
|
|
98
|
+
'',
|
|
99
|
+
'Brief: ' + (mvaBriefSentence || '(no brief)'),
|
|
100
|
+
'Findings: ' + findingsSummary,
|
|
101
|
+
'',
|
|
102
|
+
'Slug:',
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _summarizeFindings(autoExploreFinding) {
|
|
107
|
+
if (!autoExploreFinding || typeof autoExploreFinding !== 'object') return '(none)';
|
|
108
|
+
const findings = Array.isArray(autoExploreFinding.findings) ? autoExploreFinding.findings : [];
|
|
109
|
+
if (findings.length === 0) return '(empty)';
|
|
110
|
+
return findings.slice(0, 3).map(function (f) {
|
|
111
|
+
const sp = (typeof f.source_pipeline === 'string') ? f.source_pipeline : 'unknown';
|
|
112
|
+
const hsi = (typeof f.hsi_score === 'number') ? f.hsi_score.toFixed(2) : '?';
|
|
113
|
+
return sp + ':' + hsi;
|
|
114
|
+
}).join(', ');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _normalizeSlug(raw) {
|
|
118
|
+
if (typeof raw !== 'string') return '';
|
|
119
|
+
let slug = raw.trim().toLowerCase();
|
|
120
|
+
// Collapse whitespace to single hyphen; drop everything that's not [a-z0-9-].
|
|
121
|
+
slug = slug.replace(/\s+/g, '-');
|
|
122
|
+
slug = slug.replace(/[^a-z0-9-]/g, '');
|
|
123
|
+
slug = slug.replace(/-{2,}/g, '-');
|
|
124
|
+
slug = slug.replace(/^-+|-+$/g, '');
|
|
125
|
+
return slug;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Phase 119-01 REVISION 2026-05-16 (Blocker 2 Option A): mva-agent-contract.cjs
|
|
129
|
+
// exports {runAgent, validateAgentResult, AGENT_RESULT_SHAPE} ONLY -- no
|
|
130
|
+
// createLlmClient factory exists. The project-wide LLM-call idiom is direct
|
|
131
|
+
// fetch to https://api.anthropic.com/v1/messages with x-api-key header
|
|
132
|
+
// (precedent: lib/core/mva-classifier.cjs::_callHaiku same Haiku 4.5 model,
|
|
133
|
+
// same anthropic-version header pattern, same AbortController timeout).
|
|
134
|
+
//
|
|
135
|
+
// This module mirrors that precedent verbatim. No @anthropic-ai/sdk dependency
|
|
136
|
+
// added. Canon Part 8 invariant preserved: api.anthropic.com is the LOCAL
|
|
137
|
+
// Anthropic API endpoint; the Mindrian-owned Brain MCP host is NEVER contacted.
|
|
138
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
139
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
140
|
+
const LLM_TIMEOUT_MS = 5000;
|
|
141
|
+
|
|
142
|
+
function _resolveProductionLlmClient() {
|
|
143
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
144
|
+
if (!apiKey || typeof apiKey !== 'string' || apiKey.length === 0) {
|
|
145
|
+
throw new Error('anthropic_api_key_missing');
|
|
146
|
+
}
|
|
147
|
+
if (typeof fetch !== 'function') {
|
|
148
|
+
throw new Error('global_fetch_unavailable');
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
complete: async function (req) {
|
|
152
|
+
const model = req.model || HAIKU_MODEL_ID;
|
|
153
|
+
const messages = Array.isArray(req.messages) ? req.messages : [];
|
|
154
|
+
const max_tokens = (typeof req.max_tokens === 'number') ? req.max_tokens : 20;
|
|
155
|
+
const ctrl = (typeof AbortController === 'function') ? new AbortController() : null;
|
|
156
|
+
const timer = ctrl ? setTimeout(function () { try { ctrl.abort(); } catch (_e) {} }, LLM_TIMEOUT_MS) : null;
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(ANTHROPIC_URL, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'content-type': 'application/json',
|
|
162
|
+
'x-api-key': apiKey,
|
|
163
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({ model: model, max_tokens: max_tokens, messages: messages }),
|
|
166
|
+
signal: ctrl ? ctrl.signal : undefined,
|
|
167
|
+
});
|
|
168
|
+
if (!res || !res.ok) {
|
|
169
|
+
throw new Error('anthropic_http_' + (res ? res.status : 'no_response'));
|
|
170
|
+
}
|
|
171
|
+
const j = await res.json();
|
|
172
|
+
let text = '';
|
|
173
|
+
if (j && Array.isArray(j.content)) {
|
|
174
|
+
for (const blk of j.content) {
|
|
175
|
+
if (blk && blk.type === 'text' && typeof blk.text === 'string') {
|
|
176
|
+
text += blk.text;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { content: text };
|
|
181
|
+
} finally {
|
|
182
|
+
if (timer) clearTimeout(timer);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
suggestRoomName: suggestRoomName,
|
|
190
|
+
FALLBACK_SUGGESTION: FALLBACK_SUGGESTION,
|
|
191
|
+
HAIKU_MODEL_ID: HAIKU_MODEL_ID,
|
|
192
|
+
_normalizeSlug: _normalizeSlug,
|
|
193
|
+
_buildLocalPrompt: _buildLocalPrompt,
|
|
194
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 119-01 Task 1 tests for lib/core/llm-name-suggester.cjs.
|
|
3
|
+
// Validates HAIKU_MODEL_ID + FALLBACK_SUGGESTION + suggestRoomName happy path +
|
|
4
|
+
// graceful degradation + Canon Part 8 LOCAL invariant (zero brain.mindrian
|
|
5
|
+
// substrings in module source) + EVENT_TYPES floor + room_discard_partial_failure
|
|
6
|
+
// membership.
|
|
7
|
+
|
|
8
|
+
const test = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
|
|
15
|
+
const suggester = require('./llm-name-suggester.cjs');
|
|
16
|
+
const memoryEvents = require('./navigation/memory-events.cjs');
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// EVENT_TYPES tests (floor + named membership)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
test('Test 1: EVENT_TYPES Set size grows by at least 1 above the Plan 119-00 baseline (floor >= 42)', function () {
|
|
23
|
+
assert.ok(memoryEvents.EVENT_TYPES.size >= 42,
|
|
24
|
+
'EVENT_TYPES.size must be >= 42 (38 baseline + 3 from Plan 119-00 + 1 from this plan); got ' + memoryEvents.EVENT_TYPES.size);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('Test 2: room_discard_partial_failure is in EVENT_TYPES; regression check on Plan 119-00 strings preserved', function () {
|
|
28
|
+
assert.ok(memoryEvents.EVENT_TYPES.has('room_discard_partial_failure'), 'room_discard_partial_failure missing');
|
|
29
|
+
assert.ok(memoryEvents.EVENT_TYPES.has('room_auto_created'), 'Plan 119-00 string room_auto_created regressed');
|
|
30
|
+
assert.ok(memoryEvents.EVENT_TYPES.has('room_naming_decided'), 'Plan 119-00 string room_naming_decided regressed');
|
|
31
|
+
assert.ok(memoryEvents.EVENT_TYPES.has('room_discarded'), 'Plan 119-00 string room_discarded regressed');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Test 3: EVENT_TYPES object is Object.frozen (own-properties invariant)', function () {
|
|
35
|
+
assert.ok(Object.isFrozen(memoryEvents.EVENT_TYPES),
|
|
36
|
+
'EVENT_TYPES Set must be Object.frozen (own-properties; the internal Set slot is documentation-only frozen per ECMAScript spec)');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Test 4: logEvent acceptance round-trip for room_discard_partial_failure', function () {
|
|
40
|
+
// Use a real tmp room.db via room-db.cjs::openRoomDb (the canonical opener).
|
|
41
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'lln-suggester-test-'));
|
|
42
|
+
try {
|
|
43
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
44
|
+
const db = openRoomDb(tmpRoot);
|
|
45
|
+
try {
|
|
46
|
+
const result = memoryEvents.logEvent(db, 'room_discard_partial_failure', {
|
|
47
|
+
previous_slug: 'untitled-2026-05-16-1845',
|
|
48
|
+
partial_state: { fs_removed: true, registry_purged: false },
|
|
49
|
+
error_short: 'EBUSY',
|
|
50
|
+
source_path: 'system:room-discard-cascade',
|
|
51
|
+
created_by: 'system',
|
|
52
|
+
});
|
|
53
|
+
assert.strictEqual(result.ok, true, 'logEvent must accept room_discard_partial_failure as a valid event_type');
|
|
54
|
+
assert.match(result.eventId, /^memory_event:room_discard_partial_failure:\d+:[0-9a-f]{8}$/);
|
|
55
|
+
} finally {
|
|
56
|
+
closeRoomDb(db);
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_e) {}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// suggestRoomName tests
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
test('Test 5: HAIKU_MODEL_ID constant equals claude-haiku-4-5 (mirrors lib/core/mva-classifier.cjs:53)', function () {
|
|
68
|
+
assert.strictEqual(suggester.HAIKU_MODEL_ID, 'claude-haiku-4-5');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Test 6: FALLBACK_SUGGESTION constant equals untitled (verbatim)', function () {
|
|
72
|
+
assert.strictEqual(suggester.FALLBACK_SUGGESTION, 'untitled');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Test 7: suggestRoomName happy path returns normalized slug', async function () {
|
|
76
|
+
const stub = { complete: async function () { return { content: 'acme-robotics' }; } };
|
|
77
|
+
const r = await suggester.suggestRoomName({
|
|
78
|
+
auto_explore_finding: { findings: [{ source_pipeline: 'whitespace', hsi_score: 0.83 }] },
|
|
79
|
+
mva_brief_sentence: 'Robotic precision for industrial automation.',
|
|
80
|
+
llmClient: stub,
|
|
81
|
+
});
|
|
82
|
+
assert.strictEqual(r.ok, true);
|
|
83
|
+
assert.strictEqual(r.suggested_name, 'acme-robotics');
|
|
84
|
+
assert.strictEqual(r.model_used, 'claude-haiku-4-5');
|
|
85
|
+
assert.ok(typeof r.latency_ms === 'number' && r.latency_ms >= 0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('Test 8: suggestRoomName graceful degradation on LLM error -> FALLBACK_SUGGESTION', async function () {
|
|
89
|
+
const stub = { complete: async function () { throw new Error('timeout'); } };
|
|
90
|
+
const r = await suggester.suggestRoomName({
|
|
91
|
+
auto_explore_finding: null,
|
|
92
|
+
mva_brief_sentence: '',
|
|
93
|
+
llmClient: stub,
|
|
94
|
+
});
|
|
95
|
+
assert.strictEqual(r.ok, false);
|
|
96
|
+
assert.strictEqual(r.suggested_name, 'untitled');
|
|
97
|
+
assert.strictEqual(r.model_used, 'claude-haiku-4-5');
|
|
98
|
+
assert.ok(typeof r.latency_ms === 'number' && r.latency_ms >= 0);
|
|
99
|
+
assert.ok(typeof r.error_short === 'string' && r.error_short.length > 0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Test 9: Canon Part 8 LOCAL invariant -- grep tripwire on llm-name-suggester source', function () {
|
|
103
|
+
const src = fs.readFileSync(require.resolve('./llm-name-suggester.cjs'), 'utf8');
|
|
104
|
+
// The forbidden Brain coupling regex (from the plan scaffold harness Gate 3):
|
|
105
|
+
// brain.mindrian | require.+brain-client | fetch.+brain
|
|
106
|
+
assert.ok(!/brain\.mindrian/.test(src), 'source contains brain.mindrian substring (Canon Part 8 breach)');
|
|
107
|
+
assert.ok(!/require\([^)]*brain-client[^)]*\)/.test(src), 'source requires a brain-client module (Canon Part 8 breach)');
|
|
108
|
+
// The fetch.+brain regex would match commentary about "brain" near a fetch
|
|
109
|
+
// discussion; we narrow to actual fetch() invocations to brain.* hosts:
|
|
110
|
+
assert.ok(!/fetch\([^)]*['\"][^'\"]*brain[^'\"]*['\"]/.test(src), 'source fetches a brain.* URL (Canon Part 8 breach)');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('Test 10: Canon Part 8 LOCAL invariant -- payload audit (constructed prompt contains no Brain handle)', function () {
|
|
114
|
+
const prompt = suggester._buildLocalPrompt({
|
|
115
|
+
findings: [{ source_pipeline: 'whitespace', hsi_score: 0.5 }],
|
|
116
|
+
}, 'AI-assisted protein folding for drug discovery.');
|
|
117
|
+
const serialized = JSON.stringify(prompt);
|
|
118
|
+
assert.ok(serialized.indexOf('brain.mindrian') === -1, 'prompt must not contain brain.mindrian');
|
|
119
|
+
assert.ok(serialized.indexOf('brain-client') === -1, 'prompt must not contain brain-client handle');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Em-dash invariant on the test file itself + module source
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
test('Test 11: zero em-dashes in llm-name-suggester source + this test file', function () {
|
|
127
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
128
|
+
const src = fs.readFileSync(require.resolve('./llm-name-suggester.cjs'), 'utf8');
|
|
129
|
+
const testSrc = fs.readFileSync(__filename, 'utf8');
|
|
130
|
+
assert.ok(src.indexOf(EMDASH) === -1, 'em-dash present in llm-name-suggester.cjs (HARD RULE)');
|
|
131
|
+
assert.ok(testSrc.indexOf(EMDASH) === -1, 'em-dash present in llm-name-suggester.test.cjs (HARD RULE)');
|
|
132
|
+
});
|
|
@@ -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
|
+
});
|