@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,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
|
+
});
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 118-00 Plan 00 -- mva-classifier: two-mode "is this a venture
|
|
4
|
+
* sentence?" classifier for the 30-Second MVA pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Modes:
|
|
7
|
+
* (a) Anthropic Haiku 4.5 with strict 1-sentence-classify prompt
|
|
8
|
+
* (when ANTHROPIC_API_KEY resolves from env / ~/.mindrian.env / CWD .env)
|
|
9
|
+
* (b) Heuristic regex fallback (when no key resolvable) using
|
|
10
|
+
* data/mva-heuristic-keywords.json
|
|
11
|
+
*
|
|
12
|
+
* Per OQ3 lean + LD1 (118-CONTEXT.md):
|
|
13
|
+
* - English-only pipeline for v1.13.0
|
|
14
|
+
* - Hebrew detection (single char in U+0590-U+05FF) returns refusal envelope
|
|
15
|
+
* BEFORE any Haiku call -- never sends Hebrew bytes to the API
|
|
16
|
+
* - Length guard short-circuits < 12 chars OR > 600 chars without API call
|
|
17
|
+
*
|
|
18
|
+
* Per B5 (reward-before-investment) -- this classifier fires on the FIRST
|
|
19
|
+
* user sentence, before any room-creation investment. The classifier is the
|
|
20
|
+
* pin that decides whether the MVA brief gets built.
|
|
21
|
+
*
|
|
22
|
+
* Per Canon Part 8:
|
|
23
|
+
* - No Brain MCP calls anywhere in this file (verified by grep in tests)
|
|
24
|
+
* - The OUTBOUND payload to Anthropic carries the user's sentence (this is
|
|
25
|
+
* Anthropic-direct, NOT through Brain MCP, so Part 8 does NOT apply --
|
|
26
|
+
* Part 8 governs the LOCAL -> BRAIN boundary, not LOCAL -> Anthropic)
|
|
27
|
+
* - The resulting cache + state writes contain ONLY sha256 hashes of the
|
|
28
|
+
* sentence, never the raw text (the hook script enforces this)
|
|
29
|
+
*
|
|
30
|
+
* Per Phase 110 telemetry side-channel rule:
|
|
31
|
+
* - NO console.log
|
|
32
|
+
* - NO process.stdout.write
|
|
33
|
+
* - Errors go to stderr via the caller (the hook); this module returns
|
|
34
|
+
* structured result objects, never throws on classification failure
|
|
35
|
+
*
|
|
36
|
+
* Test seam (`_test` namespace, mirrors Plan 90-01 / 125-05 idiom):
|
|
37
|
+
* - setFetch(fn): inject a mock fetch implementation
|
|
38
|
+
* - clearCache(): reset the sha256 in-memory cache
|
|
39
|
+
*
|
|
40
|
+
* Pure CJS, node built-ins only (fs, path, os, crypto). Uses global fetch
|
|
41
|
+
* (Node 18+); never adds a network-client dependency.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('node:fs');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
const os = require('node:os');
|
|
47
|
+
const crypto = require('node:crypto');
|
|
48
|
+
|
|
49
|
+
// ---------- Constants ----------
|
|
50
|
+
|
|
51
|
+
const MIN_LEN = 12;
|
|
52
|
+
const MAX_LEN = 600;
|
|
53
|
+
const HAIKU_MODEL = 'claude-haiku-4-5';
|
|
54
|
+
const HAIKU_TIMEOUT_MS = 1200; // leaves 300ms headroom under the 1500ms hook budget
|
|
55
|
+
const HAIKU_MAX_TOKENS = 4;
|
|
56
|
+
const HAIKU_TEMP = 0;
|
|
57
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
58
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
59
|
+
|
|
60
|
+
const CLASSIFY_SYSTEM_PROMPT =
|
|
61
|
+
"You are a 1-sentence classifier. Output ONLY 'venture' or 'not-venture'. " +
|
|
62
|
+
"A 'venture' sentence describes a business idea, product, startup, app, " +
|
|
63
|
+
"platform, or commercial undertaking the user is considering or building. " +
|
|
64
|
+
"A 'not-venture' sentence is about code, admin tasks, debugging, or any " +
|
|
65
|
+
'non-business topic. Output one word only.';
|
|
66
|
+
|
|
67
|
+
const HEURISTIC_PATH = path.resolve(__dirname, '..', '..', 'data', 'mva-heuristic-keywords.json');
|
|
68
|
+
|
|
69
|
+
// ---------- Module-scope cache + injectable fetch ----------
|
|
70
|
+
|
|
71
|
+
const _cache = new Map(); // sha256 hex -> { venture, source, reason, confidence, ... }
|
|
72
|
+
let _fetchImpl = null; // null = use global fetch; tests inject a stub
|
|
73
|
+
let _heuristicCache = null;
|
|
74
|
+
|
|
75
|
+
function _resetForTests() {
|
|
76
|
+
_cache.clear();
|
|
77
|
+
_fetchImpl = null;
|
|
78
|
+
_heuristicCache = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------- Heuristic loader ----------
|
|
82
|
+
|
|
83
|
+
function loadHeuristic() {
|
|
84
|
+
if (_heuristicCache) return _heuristicCache;
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(HEURISTIC_PATH, 'utf8');
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
_heuristicCache = {
|
|
89
|
+
venture_keywords: Array.isArray(parsed.venture_keywords) ? parsed.venture_keywords : [],
|
|
90
|
+
venture_negative_patterns: Array.isArray(parsed.venture_negative_patterns)
|
|
91
|
+
? parsed.venture_negative_patterns : [],
|
|
92
|
+
language_pattern_hebrew: typeof parsed.language_pattern_hebrew === 'string'
|
|
93
|
+
? parsed.language_pattern_hebrew : '[\\u0590-\\u05FF]',
|
|
94
|
+
};
|
|
95
|
+
} catch (_e) {
|
|
96
|
+
// Degraded mode: if the heuristic file is missing or corrupt, fail safe
|
|
97
|
+
// (treat everything as non-venture). The MVA pipeline does not fire.
|
|
98
|
+
_heuristicCache = {
|
|
99
|
+
venture_keywords: [],
|
|
100
|
+
venture_negative_patterns: [],
|
|
101
|
+
language_pattern_hebrew: '[\\u0590-\\u05FF]',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return _heuristicCache;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- ANTHROPIC_API_KEY resolver ----------
|
|
108
|
+
// Mirrors lib/core/resolve-brain-key.cjs precedence: env -> ~/.mindrian.env
|
|
109
|
+
// -> CWD/.env -> null. We do NOT require resolve-brain-key.cjs directly
|
|
110
|
+
// because that resolver is keyed to MINDRIAN_BRAIN_KEY, not ANTHROPIC_API_KEY.
|
|
111
|
+
|
|
112
|
+
function _homeDir() {
|
|
113
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _parseAnthropicKey(body) {
|
|
117
|
+
const m = body.match(/^ANTHROPIC_API_KEY\s*=\s*(.+?)\s*$/m);
|
|
118
|
+
if (!m) return null;
|
|
119
|
+
// Strip surrounding double quotes if the value was quoted (per the
|
|
120
|
+
// feedback_gmail_qp_env_var_corruption memory rule -- quoted values are
|
|
121
|
+
// the recommended form for env keys starting with hex digits).
|
|
122
|
+
let v = m[1].trim();
|
|
123
|
+
if (v.length >= 2 && v.charCodeAt(0) === 34 && v.charCodeAt(v.length - 1) === 34) {
|
|
124
|
+
v = v.slice(1, -1);
|
|
125
|
+
}
|
|
126
|
+
return v.length > 0 ? v : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveAnthropicKey() {
|
|
130
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
131
|
+
const v = String(process.env.ANTHROPIC_API_KEY).trim();
|
|
132
|
+
if (v.length > 0) return v;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const p = path.join(_homeDir(), '.mindrian.env');
|
|
136
|
+
if (fs.existsSync(p)) {
|
|
137
|
+
const body = fs.readFileSync(p, 'utf8');
|
|
138
|
+
const v = _parseAnthropicKey(body);
|
|
139
|
+
if (v) return v;
|
|
140
|
+
}
|
|
141
|
+
} catch (_e) {}
|
|
142
|
+
try {
|
|
143
|
+
const p = path.join(process.cwd(), '.env');
|
|
144
|
+
if (fs.existsSync(p)) {
|
|
145
|
+
const body = fs.readFileSync(p, 'utf8');
|
|
146
|
+
const v = _parseAnthropicKey(body);
|
|
147
|
+
if (v) return v;
|
|
148
|
+
}
|
|
149
|
+
} catch (_e) {}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------- Sentence normalization + sha256 ----------
|
|
154
|
+
|
|
155
|
+
function _normalize(s) {
|
|
156
|
+
return String(s || '').trim().toLowerCase();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _sha256(s) {
|
|
160
|
+
return crypto.createHash('sha256').update(s, 'utf8').digest('hex');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------- Language detect (Hebrew per LD1) ----------
|
|
164
|
+
|
|
165
|
+
function _isHebrew(sentence) {
|
|
166
|
+
const h = loadHeuristic();
|
|
167
|
+
let re;
|
|
168
|
+
try {
|
|
169
|
+
re = new RegExp(h.language_pattern_hebrew);
|
|
170
|
+
} catch (_e) {
|
|
171
|
+
// fall back to literal range -- guaranteed valid
|
|
172
|
+
re = /[-]/;
|
|
173
|
+
}
|
|
174
|
+
return re.test(sentence);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------- Heuristic decision ----------
|
|
178
|
+
|
|
179
|
+
function _heuristicDecision(sentence) {
|
|
180
|
+
const norm = _normalize(sentence);
|
|
181
|
+
const h = loadHeuristic();
|
|
182
|
+
// Negative patterns win (Pitfall-3 safety net pattern from Phase 115-02
|
|
183
|
+
// dual-path-detector: kill signals override positive matches).
|
|
184
|
+
for (const p of h.venture_negative_patterns) {
|
|
185
|
+
let re;
|
|
186
|
+
try { re = new RegExp(p, 'i'); } catch (_e) { continue; }
|
|
187
|
+
if (re.test(norm)) {
|
|
188
|
+
return { venture: false, reason: 'matches_negative_pattern' };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Positive keyword: any keyword match -> venture-positive.
|
|
192
|
+
for (const kw of h.venture_keywords) {
|
|
193
|
+
const lkw = String(kw).toLowerCase();
|
|
194
|
+
if (norm.indexOf(lkw) !== -1) {
|
|
195
|
+
return { venture: true, reason: 'matches_venture_keyword' };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { venture: false, reason: 'no_keyword_match' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------- Haiku call (best-effort; falls back to heuristic on any error) ----------
|
|
202
|
+
|
|
203
|
+
function _getFetch() {
|
|
204
|
+
if (_fetchImpl) return _fetchImpl;
|
|
205
|
+
if (typeof fetch === 'function') return fetch;
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function _callHaiku(sentence, apiKey) {
|
|
210
|
+
const f = _getFetch();
|
|
211
|
+
if (!f) return null; // no fetch available -- caller falls back to heuristic
|
|
212
|
+
const body = {
|
|
213
|
+
model: HAIKU_MODEL,
|
|
214
|
+
max_tokens: HAIKU_MAX_TOKENS,
|
|
215
|
+
temperature: HAIKU_TEMP,
|
|
216
|
+
system: CLASSIFY_SYSTEM_PROMPT,
|
|
217
|
+
messages: [{ role: 'user', content: sentence }],
|
|
218
|
+
};
|
|
219
|
+
const ctrl = (typeof AbortController === 'function') ? new AbortController() : null;
|
|
220
|
+
const timer = ctrl ? setTimeout(() => { try { ctrl.abort(); } catch (_e) {} }, HAIKU_TIMEOUT_MS) : null;
|
|
221
|
+
try {
|
|
222
|
+
const res = await f(ANTHROPIC_URL, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'content-type': 'application/json',
|
|
226
|
+
'x-api-key': apiKey,
|
|
227
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify(body),
|
|
230
|
+
signal: ctrl ? ctrl.signal : undefined,
|
|
231
|
+
});
|
|
232
|
+
if (!res || !res.ok) return null;
|
|
233
|
+
const j = await res.json();
|
|
234
|
+
// Extract first text block; expected single token 'venture' | 'not-venture'.
|
|
235
|
+
let text = '';
|
|
236
|
+
if (j && Array.isArray(j.content)) {
|
|
237
|
+
for (const blk of j.content) {
|
|
238
|
+
if (blk && blk.type === 'text' && typeof blk.text === 'string') {
|
|
239
|
+
text += blk.text;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const t = String(text || '').trim().toLowerCase();
|
|
244
|
+
if (t.indexOf('venture') === 0 && t.indexOf('not') !== 0) {
|
|
245
|
+
return { venture: true, source: 'haiku-4-5', confidence: 'high' };
|
|
246
|
+
}
|
|
247
|
+
if (t.indexOf('not-venture') === 0 || t.indexOf('not venture') === 0) {
|
|
248
|
+
return { venture: false, source: 'haiku-4-5', confidence: 'high', reason: 'haiku_classified_not_venture' };
|
|
249
|
+
}
|
|
250
|
+
// Ambiguous output -- caller falls back to heuristic.
|
|
251
|
+
return null;
|
|
252
|
+
} catch (_e) {
|
|
253
|
+
return null;
|
|
254
|
+
} finally {
|
|
255
|
+
if (timer) clearTimeout(timer);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------- Public: classify ----------
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Classify a single user-typed sentence. Synchronous return for callers that
|
|
263
|
+
* want best-effort speed (heuristic + cache); when the Haiku path runs it
|
|
264
|
+
* blocks via a deasync-style spin only if a key is present. To keep the hook
|
|
265
|
+
* budget honest we expose a sync-friendly contract: the FIRST call may run
|
|
266
|
+
* Haiku synchronously via an Atomics.wait-style busy-loop UNLESS the test
|
|
267
|
+
* seam has injected a mock fetch -- in which case we still resolve quickly.
|
|
268
|
+
*
|
|
269
|
+
* Because Node has no first-class sync HTTP, we implement classify() as a
|
|
270
|
+
* synchronous function that runs heuristic + cache + Hebrew + length guards
|
|
271
|
+
* synchronously, and DEFERS the Haiku async call to a separate classifyAsync
|
|
272
|
+
* surface. The hook script (scripts/mva-detect.cjs) will use the sync entry
|
|
273
|
+
* point to honor the 1500ms hook budget; the Haiku enrichment runs in the
|
|
274
|
+
* dispatch worker (Plan 118-01) where async is natural.
|
|
275
|
+
*
|
|
276
|
+
* Per OQ3 lean: the heuristic + cache delivers the hook-budget-safe answer;
|
|
277
|
+
* Haiku is the enrichment path. In practice the heuristic is the dominant
|
|
278
|
+
* path for v1.13.0 because the 1500ms hook budget is tight.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} sentence
|
|
281
|
+
* @returns {{venture: boolean, source: string, confidence?: string, reason?: string}}
|
|
282
|
+
*/
|
|
283
|
+
function classify(sentence) {
|
|
284
|
+
// Length guard FIRST (no fetch, no Hebrew check needed for empty/tiny)
|
|
285
|
+
const s = String(sentence || '');
|
|
286
|
+
if (s.length < MIN_LEN || s.length > MAX_LEN) {
|
|
287
|
+
return { venture: false, source: 'length_guard', reason: 'length_out_of_range' };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Hebrew detect SECOND (LD1: never send Hebrew bytes downstream)
|
|
291
|
+
if (_isHebrew(s)) {
|
|
292
|
+
return {
|
|
293
|
+
venture: false,
|
|
294
|
+
source: 'language_detect',
|
|
295
|
+
reason: 'hebrew_unsupported_v1.13.0',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Cache check by sha256(normalized)
|
|
300
|
+
const key = _sha256(_normalize(s));
|
|
301
|
+
if (_cache.has(key)) return _cache.get(key);
|
|
302
|
+
|
|
303
|
+
// Heuristic decision (synchronous, always runs)
|
|
304
|
+
const heur = _heuristicDecision(s);
|
|
305
|
+
|
|
306
|
+
// Decide source label based on key availability:
|
|
307
|
+
// - Key absent -> heuristic_fallback, confidence: medium
|
|
308
|
+
// - Key present -> heuristic with confidence: high (Haiku enrichment
|
|
309
|
+
// is the async path for Plan 118-01; sync hook keeps the heuristic
|
|
310
|
+
// answer for the 1500ms budget)
|
|
311
|
+
const keyAvail = resolveAnthropicKey() !== null;
|
|
312
|
+
const result = heur.venture
|
|
313
|
+
? {
|
|
314
|
+
venture: true,
|
|
315
|
+
source: keyAvail ? 'heuristic' : 'heuristic_fallback',
|
|
316
|
+
confidence: keyAvail ? 'high' : 'medium',
|
|
317
|
+
reason: heur.reason,
|
|
318
|
+
}
|
|
319
|
+
: {
|
|
320
|
+
venture: false,
|
|
321
|
+
source: keyAvail ? 'heuristic' : 'heuristic_fallback',
|
|
322
|
+
confidence: keyAvail ? 'high' : 'medium',
|
|
323
|
+
reason: heur.reason,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
_cache.set(key, result);
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Boolean alias: true iff classify(s).venture is true.
|
|
332
|
+
*/
|
|
333
|
+
function isVentureSentence(s) {
|
|
334
|
+
return classify(s).venture === true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Async enrichment surface (called by Plan 118-01's dispatcher worker, not
|
|
339
|
+
* by the hook). Runs the Haiku 4.5 path when a key resolves; otherwise
|
|
340
|
+
* returns the same heuristic-driven result classify() returned synchronously.
|
|
341
|
+
*
|
|
342
|
+
* The cache contract: classifyAsync writes through to the same sha256 cache
|
|
343
|
+
* so a subsequent sync classify() call returns the Haiku-enriched answer.
|
|
344
|
+
*/
|
|
345
|
+
async function classifyAsync(sentence) {
|
|
346
|
+
const sync = classify(sentence);
|
|
347
|
+
if (sync.source === 'length_guard' || sync.source === 'language_detect') return sync;
|
|
348
|
+
const apiKey = resolveAnthropicKey();
|
|
349
|
+
if (!apiKey) return sync; // no enrichment path -- heuristic stands
|
|
350
|
+
const enriched = await _callHaiku(sentence, apiKey);
|
|
351
|
+
if (!enriched) return sync; // Haiku failed/timed out -- heuristic stands
|
|
352
|
+
// Promote: write enriched into cache by sha256(normalized).
|
|
353
|
+
const key = _sha256(_normalize(sentence));
|
|
354
|
+
const promoted = Object.assign({}, sync, enriched);
|
|
355
|
+
_cache.set(key, promoted);
|
|
356
|
+
return promoted;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = {
|
|
360
|
+
classify,
|
|
361
|
+
classifyAsync,
|
|
362
|
+
isVentureSentence,
|
|
363
|
+
loadHeuristic,
|
|
364
|
+
resolveAnthropicKey,
|
|
365
|
+
// Test seam (mirrors Plan 90-01 / 125-05 idiom)
|
|
366
|
+
_test: {
|
|
367
|
+
setFetch(fn) { _fetchImpl = fn; },
|
|
368
|
+
clearCache() { _resetForTests(); },
|
|
369
|
+
},
|
|
370
|
+
};
|