@mindrian_os/install 1.13.0-beta.11 → 1.13.0-beta.13
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 +68 -3
- package/bin/cli.js +114 -57
- package/commands/act.md +16 -2
- package/commands/auto-explore.md +1 -0
- package/commands/doctor.md +1 -1
- package/commands/operator.md +1 -1
- package/commands/pipeline.md +16 -1
- package/commands/setup.md +7 -3
- package/commands/suggest-next.md +17 -3
- package/lib/core/active-plugin-root.cjs +207 -0
- package/lib/core/brain-client.cjs +451 -36
- package/lib/core/cache-prune.cjs +208 -0
- package/lib/core/framework-chain-composer.cjs +156 -43
- package/lib/core/migrations/phase-109-nodes-provenance.cjs +47 -0
- package/lib/core/navigation/memory-events.cjs +17 -1
- package/lib/core/navigation/neighborhood.cjs +5 -4
- package/lib/core/navigation/packet.cjs +87 -1
- package/lib/core/navigation.cjs +6 -0
- package/lib/core/resolve-brain-key.cjs +201 -0
- package/lib/hmi/jtbd-taxonomy.json +2 -1
- package/lib/memory/framework-chain-composer.test.cjs +54 -20
- package/lib/memory/navigation-hook-resolver.test.cjs +177 -0
- package/lib/memory/run-feynman-tests.cjs +102 -0
- package/lib/memory/security-trifecta.test.cjs +23 -6
- package/lib/memory/suggest-next-workflow.test.cjs +176 -0
- package/lib/memory/workflow-layer-e2e.test.cjs +262 -0
- package/lib/workflow/ROOM.md +1 -1
- package/package.json +4 -1
- package/references/brain/command-triggers-schema.md +10 -221
- package/references/methodology/index.md +11 -74
- package/skills/brain-connector/SKILL.md +12 -8
- package/skills/pws-methodology/SKILL.md +7 -5
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 122-04 -- navigation-hook-resolver tests (new suite; registered in the Feynman runner).
|
|
8
|
+
* ============================================================================================
|
|
9
|
+
* Asserts the ONE surgical edit in lib/core/framework-chain-composer.cjs:
|
|
10
|
+
* proposeNextFramework() now resolves the next framework's /mos: command via
|
|
11
|
+
* lib/workflow/command-resolver.cjs commandsForFramework() (the only door),
|
|
12
|
+
* degrades to command:null when the registry has no command for it (degrade,
|
|
13
|
+
* do not fabricate -- WORKFLOW-LAYER-SPEC reliability rule 5), and carries a
|
|
14
|
+
* workflow array (the resolver's composeWorkflow for the multi-hop chain) as
|
|
15
|
+
* data on the proposal. mapFrameworkToCommandSlug() delegates to the resolver.
|
|
16
|
+
* The navigation engine / offer presenter / hooks are NOT touched.
|
|
17
|
+
*
|
|
18
|
+
* Registered in lib/memory/run-feynman-tests.cjs TEST_FILES[] and in
|
|
19
|
+
* tests/run-all-122.sh CJS_SUITES (as ../lib/memory/navigation-hook-resolver.test.cjs).
|
|
20
|
+
* Run: node lib/memory/navigation-hook-resolver.test.cjs
|
|
21
|
+
* Exit 0 on pass; throws (node:assert) on any fail.
|
|
22
|
+
*
|
|
23
|
+
* Assertion groups:
|
|
24
|
+
* 1. framework-chain-composer.cjs source requires ../workflow/command-resolver
|
|
25
|
+
* (the surgical edit is in place) and references composeWorkflow.
|
|
26
|
+
* 2. proposeNextFramework(completed, edges).command === commandsForFramework(next)[0]
|
|
27
|
+
* (or null when there is none). Tested on a registered next framework
|
|
28
|
+
* (Business Model Canvas -> Lean Canvas -> /mos:lean-canvas) and an
|
|
29
|
+
* unregistered one (X -> A Wholly Imaginary Framework -> null).
|
|
30
|
+
* 3. proposeNextFramework returns a workflow array shaped like composeWorkflow
|
|
31
|
+
* ([{ step, framework, command|null, optional }], 1-indexed, in order),
|
|
32
|
+
* and for a multi-hop FEEDS_INTO chain it includes the successors.
|
|
33
|
+
* 4. mapFrameworkToCommandSlug delegates to the resolver (a registered
|
|
34
|
+
* framework slug round-trips), falls back to the legacy table for an
|
|
35
|
+
* unknown name, and stays exported alongside FRAMEWORK_TO_COMMAND_SLUG
|
|
36
|
+
* and KNOWN_FRAMEWORKS (122-05 prunes the legacy table, not this plan).
|
|
37
|
+
* 5. The engine / presenter / hooks are NOT touched: framework-chain-composer.cjs
|
|
38
|
+
* is the only file that gained the resolver require (we assert the
|
|
39
|
+
* navigation-engine source did NOT gain a command-resolver require).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const assert = require('node:assert/strict');
|
|
43
|
+
const fs = require('node:fs');
|
|
44
|
+
const path = require('node:path');
|
|
45
|
+
|
|
46
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
47
|
+
const COMPOSER_PATH = path.join(REPO_ROOT, 'lib', 'core', 'framework-chain-composer.cjs');
|
|
48
|
+
const ENGINE_PATH = path.join(REPO_ROOT, 'lib', 'core', 'navigation-engine.cjs');
|
|
49
|
+
|
|
50
|
+
const composer = require('../core/framework-chain-composer.cjs');
|
|
51
|
+
const resolver = require('../workflow/command-resolver.cjs');
|
|
52
|
+
|
|
53
|
+
let passed = 0;
|
|
54
|
+
function test(name, fn) {
|
|
55
|
+
fn();
|
|
56
|
+
process.stdout.write(' ok ' + name + '\n');
|
|
57
|
+
passed += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// 1. the surgical edit is in place (source requires the resolver)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
test('framework-chain-composer.cjs requires ../workflow/command-resolver and references composeWorkflow', function () {
|
|
64
|
+
const src = fs.readFileSync(COMPOSER_PATH, 'utf8');
|
|
65
|
+
assert.ok(/require\(['"]\.\.\/workflow\/command-resolver(\.cjs)?['"]\)/.test(src),
|
|
66
|
+
'framework-chain-composer.cjs must require ../workflow/command-resolver');
|
|
67
|
+
assert.ok(/command-resolver/.test(src), 'source mentions command-resolver');
|
|
68
|
+
assert.ok(/composeWorkflow/.test(src), 'source mentions composeWorkflow (the multi-step path)');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// 2. proposeNextFramework.command === resolver answer (or null)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
test('proposeNextFramework.command === commandsForFramework(next)[0] (or null when there is none)', function () {
|
|
75
|
+
// Registered next framework: Lean Canvas -> /mos:lean-canvas.
|
|
76
|
+
const registeredEdges = [
|
|
77
|
+
{ from: 'Business Model Canvas', to: 'Lean Canvas', confidence: 0.85, phase_indicator: 'thesis' },
|
|
78
|
+
];
|
|
79
|
+
const p = composer.proposeNextFramework('Business Model Canvas', registeredEdges);
|
|
80
|
+
assert.ok(p !== null, 'a 0.85-confidence edge yields a proposal');
|
|
81
|
+
const expected = resolver.commandsForFramework('Lean Canvas');
|
|
82
|
+
assert.strictEqual(p.command, expected.length > 0 ? expected[0] : null,
|
|
83
|
+
'command must equal commandsForFramework(next)[0]; got: ' + p.command);
|
|
84
|
+
assert.ok(typeof p.command === 'string' && p.command.indexOf('/mos:') === 0,
|
|
85
|
+
'Lean Canvas IS registered, so command must be a /mos: string; got: ' + p.command);
|
|
86
|
+
|
|
87
|
+
// Unregistered next framework -> command is null (degrade, not fabricate).
|
|
88
|
+
const unregEdges = [
|
|
89
|
+
{ from: 'X', to: 'A Wholly Imaginary Framework', confidence: 0.8, phase_indicator: null },
|
|
90
|
+
];
|
|
91
|
+
const u = composer.proposeNextFramework('X', unregEdges);
|
|
92
|
+
assert.ok(u !== null, 'a 0.8-confidence edge yields a proposal even when the next has no command');
|
|
93
|
+
assert.strictEqual(u.command, null, 'unregistered next framework -> command:null; got: ' + u.command);
|
|
94
|
+
// It still matches the resolver: commandsForFramework returns [] for it.
|
|
95
|
+
assert.deepStrictEqual(resolver.commandsForFramework('A Wholly Imaginary Framework'), []);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// 3. proposeNextFramework.workflow is a composeWorkflow-shaped array
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
test('proposeNextFramework.workflow is a composeWorkflow array; a multi-hop chain includes the successors', function () {
|
|
102
|
+
// Single hop: workflow = composeWorkflow([completed, next]).
|
|
103
|
+
const single = composer.proposeNextFramework('Business Model Canvas', [
|
|
104
|
+
{ from: 'Business Model Canvas', to: 'Lean Canvas', confidence: 0.85, phase_indicator: null },
|
|
105
|
+
]);
|
|
106
|
+
assert.ok(Array.isArray(single.workflow), 'workflow is an array');
|
|
107
|
+
assert.ok(single.workflow.length >= 2, 'workflow includes [completed, next]');
|
|
108
|
+
single.workflow.forEach(function (s, i) {
|
|
109
|
+
assert.strictEqual(s.step, i + 1, 'workflow is 1-indexed and in order');
|
|
110
|
+
assert.ok('framework' in s && 'command' in s && 'optional' in s, 'workflow step shape');
|
|
111
|
+
assert.ok(s.command === null || (typeof s.command === 'string' && s.command.indexOf('/mos:') === 0),
|
|
112
|
+
'workflow step command is a /mos: string or null');
|
|
113
|
+
});
|
|
114
|
+
assert.strictEqual(single.workflow[0].framework, 'Business Model Canvas');
|
|
115
|
+
assert.strictEqual(single.workflow[1].framework, 'Lean Canvas');
|
|
116
|
+
|
|
117
|
+
// Multi-hop FEEDS_INTO: BQ -> Domain Selection -> JTBD. The workflow walks
|
|
118
|
+
// the highest-confidence chain and composeWorkflow attaches the commands.
|
|
119
|
+
const multi = composer.proposeNextFramework('Beautiful Question Framework', [
|
|
120
|
+
{ from: 'Beautiful Question Framework', to: 'Domain Selection', confidence: 0.9, phase_indicator: null },
|
|
121
|
+
{ from: 'Domain Selection', to: 'Jobs to Be Done (JTBD)', confidence: 0.85, phase_indicator: null },
|
|
122
|
+
{ from: 'Jobs to Be Done (JTBD)', to: 'PWS Value Proposition', confidence: 0.8, phase_indicator: null },
|
|
123
|
+
]);
|
|
124
|
+
assert.ok(Array.isArray(multi.workflow));
|
|
125
|
+
// [completed, +3 hops] -> 4 steps (collectForwardChain caps at 3 hops).
|
|
126
|
+
assert.strictEqual(multi.workflow.length, 4, 'workflow includes the 3-hop chain; got ' + multi.workflow.length);
|
|
127
|
+
assert.deepStrictEqual(multi.workflow.map(function (s) { return s.framework; }),
|
|
128
|
+
['Beautiful Question Framework', 'Domain Selection', 'Jobs to Be Done (JTBD)', 'PWS Value Proposition']);
|
|
129
|
+
// And the workflow equals what the resolver would compose for that chain.
|
|
130
|
+
assert.deepStrictEqual(multi.workflow,
|
|
131
|
+
resolver.composeWorkflow(['Beautiful Question Framework', 'Domain Selection', 'Jobs to Be Done (JTBD)', 'PWS Value Proposition']));
|
|
132
|
+
|
|
133
|
+
// composer.collectForwardChain is also directly exercisable.
|
|
134
|
+
assert.deepStrictEqual(
|
|
135
|
+
composer.collectForwardChain('Beautiful Question Framework', [
|
|
136
|
+
{ from: 'Beautiful Question Framework', to: 'Domain Selection', confidence: 0.9 },
|
|
137
|
+
{ from: 'Domain Selection', to: 'Jobs to Be Done (JTBD)', confidence: 0.85 },
|
|
138
|
+
], 3),
|
|
139
|
+
['Beautiful Question Framework', 'Domain Selection', 'Jobs to Be Done (JTBD)']
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// 4. mapFrameworkToCommandSlug delegates to the resolver; exports preserved
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
test('mapFrameworkToCommandSlug delegates to the resolver; FRAMEWORK_TO_COMMAND_SLUG + KNOWN_FRAMEWORKS still exported', function () {
|
|
147
|
+
// A registered framework: the slug round-trips through the resolver.
|
|
148
|
+
const cmds = resolver.commandsForFramework('Lean Canvas');
|
|
149
|
+
assert.ok(cmds.length > 0);
|
|
150
|
+
assert.strictEqual(composer.mapFrameworkToCommandSlug('Lean Canvas'), cmds[0].replace(/^\/mos:/, ''));
|
|
151
|
+
assert.strictEqual(composer.mapFrameworkToCommandSlug('Domain Selection'),
|
|
152
|
+
resolver.commandsForFramework('Domain Selection')[0].replace(/^\/mos:/, ''));
|
|
153
|
+
// An unknown name: falls back to the legacy table / FALLBACK_COMMAND_SLUG.
|
|
154
|
+
assert.strictEqual(composer.mapFrameworkToCommandSlug('A Wholly Imaginary Framework'), composer.FALLBACK_COMMAND_SLUG);
|
|
155
|
+
assert.strictEqual(typeof composer.mapFrameworkToCommandSlug('Lean Canvas'), 'string',
|
|
156
|
+
'mapFrameworkToCommandSlug always returns a slug string (it keeps a non-null fallback)');
|
|
157
|
+
// Exports preserved (122-05 prunes FRAMEWORK_TO_COMMAND_SLUG; not this plan).
|
|
158
|
+
assert.strictEqual(typeof composer.proposeNextFramework, 'function');
|
|
159
|
+
assert.strictEqual(typeof composer.mapFrameworkToCommandSlug, 'function');
|
|
160
|
+
assert.ok(composer.FRAMEWORK_TO_COMMAND_SLUG && typeof composer.FRAMEWORK_TO_COMMAND_SLUG === 'object');
|
|
161
|
+
assert.ok(Array.isArray(composer.KNOWN_FRAMEWORKS) && composer.KNOWN_FRAMEWORKS.length > 0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// 5. the engine / presenter / hooks are NOT touched (only the composer gained the resolver require)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
test('navigation-engine.cjs did NOT gain a command-resolver require (the surgical edit is composer-only)', function () {
|
|
168
|
+
const engineSrc = fs.readFileSync(ENGINE_PATH, 'utf8');
|
|
169
|
+
assert.ok(!/command-resolver/.test(engineSrc),
|
|
170
|
+
'navigation-engine.cjs must not require command-resolver (the resolver wiring is in framework-chain-composer.cjs only)');
|
|
171
|
+
// Sanity: the composer's require IS there (the only file that should have it among these two).
|
|
172
|
+
const composerSrc = fs.readFileSync(COMPOSER_PATH, 'utf8');
|
|
173
|
+
assert.ok(/command-resolver/.test(composerSrc));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
process.stdout.write('\nnavigation-hook-resolver.test.cjs: ' + passed + ' assertion groups PASSED\n');
|
|
177
|
+
process.exit(0);
|
|
@@ -1166,6 +1166,49 @@ const TEST_FILES = [
|
|
|
1166
1166
|
path.join(REPO_ROOT, 'tests', 'test-precommit-hook-aliases.cjs'),
|
|
1167
1167
|
path.join(REPO_ROOT, 'tests', 'test-canon-crossref-completeness.cjs'),
|
|
1168
1168
|
path.join(REPO_ROOT, 'tests', 'test-part-9-invariant.cjs'),
|
|
1169
|
+
// Phase 109-01 regression: the nodes-provenance migration drops + recreates
|
|
1170
|
+
// every view/trigger that references `nodes` around the table rebuild. Seeds
|
|
1171
|
+
// a room.db with an rs_discoveries-style view (+ a second view + a trigger),
|
|
1172
|
+
// runs the migration, asserts no throw + views/trigger still work + idempotent
|
|
1173
|
+
// re-run. Guards against the phase-109-migration-view-drop-collision bug
|
|
1174
|
+
// (migration used to crash openRoomDb for any room.db carrying the Phase-89
|
|
1175
|
+
// rs_discoveries view: "error in view rs_discoveries: no such table: main.nodes").
|
|
1176
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-migration-views.cjs'),
|
|
1177
|
+
// Phase 109 SQL Context-Memory Navigation Spine: the 15 test suites that the
|
|
1178
|
+
// Plan 109-00 Task 4 registration originally specified but that a later refactor
|
|
1179
|
+
// dropped from this array before main HEAD (the originals ran via direct
|
|
1180
|
+
// `node tests/test-*.cjs` and `bash tests/run-all.sh` throughout the phase).
|
|
1181
|
+
// Re-registered by Plan 109-12 (bookkeeping reconciliation). Mapping:
|
|
1182
|
+
// test-navigation-acceptance.cjs -> 109-10 (NAV-109-09 load-bearing acceptance gate; zero non-SQLite reads)
|
|
1183
|
+
// test-navigation-focus.cjs -> 109-02 (NAV-109-01 session_focus + auto-focus cascade)
|
|
1184
|
+
// test-navigation-neighborhood.cjs -> 109-04 (NAV-109-02 recursive-CTE ranking correctness)
|
|
1185
|
+
// test-navigation-perf-10k.cjs -> 109-04 (NAV-109-02 perf: cold p95 <200ms / warm <50ms)
|
|
1186
|
+
// test-navigation-memory-events.cjs -> 109-03 (NAV-109-03 closed-15 event enum + findRecentChanges)
|
|
1187
|
+
// test-navigation-insights.cjs -> 109-05 (NAV-109-04 7 insight primitives + templated explanations)
|
|
1188
|
+
// test-navigation-chokepoint-hook.cjs -> 109-06 (NAV-109-05 no direct room-db.cjs imports outside allow-list)
|
|
1189
|
+
// test-navigation-packet-builder.cjs -> 109-07 (NAV-109-06 buildBrainPacket shape per D-06)
|
|
1190
|
+
// test-navigation-packet-part8-leak.cjs -> 109-07 (NAV-109-06 Part 8 JSON.stringify leak tripwires)
|
|
1191
|
+
// test-brain-ingestion-part-9-invariant.cjs -> 109-08 (NAV-109-07 storeBrainSuggestions proposed-only + invariant SQL = 0)
|
|
1192
|
+
// test-room-home-vs-brain-derivation-regression.cjs -> 109-09 (NAV-109-08 getRoomHomeView + Phase 90 deriveSection regression fence)
|
|
1193
|
+
// test-canon-part-9-ratification.cjs -> 109-11 (NAV-109-09 Canon Part 9 structural assertions)
|
|
1194
|
+
// test-navigation-migration-idempotent.cjs -> 109-01 (migration twice = no-op; 12-column nodes schema)
|
|
1195
|
+
// test-navigation-migration-backfill.cjs -> 109-01 (properties JSON backfill + status_aliases mapping)
|
|
1196
|
+
// test-navigation-migration-coexistence.cjs -> 109-01 (navigation API + assumptions.validity coexist mid-migration)
|
|
1197
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-acceptance.cjs'),
|
|
1198
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-focus.cjs'),
|
|
1199
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-neighborhood.cjs'),
|
|
1200
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-perf-10k.cjs'),
|
|
1201
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-memory-events.cjs'),
|
|
1202
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-insights.cjs'),
|
|
1203
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-chokepoint-hook.cjs'),
|
|
1204
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-packet-builder.cjs'),
|
|
1205
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-packet-part8-leak.cjs'),
|
|
1206
|
+
path.join(REPO_ROOT, 'tests', 'test-brain-ingestion-part-9-invariant.cjs'),
|
|
1207
|
+
path.join(REPO_ROOT, 'tests', 'test-room-home-vs-brain-derivation-regression.cjs'),
|
|
1208
|
+
path.join(REPO_ROOT, 'tests', 'test-canon-part-9-ratification.cjs'),
|
|
1209
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-migration-idempotent.cjs'),
|
|
1210
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-migration-backfill.cjs'),
|
|
1211
|
+
path.join(REPO_ROOT, 'tests', 'test-navigation-migration-coexistence.cjs'),
|
|
1169
1212
|
// Phase 89-07 Wave 0 (graph-native HARD RULE; ReverseSalientAgent dual-surface).
|
|
1170
1213
|
path.join(REPO_ROOT, 'tests', 'test-reverse-salient-agent.cjs'),
|
|
1171
1214
|
path.join(REPO_ROOT, 'tests', 'test-reverse-salient-cascade-emit.cjs'),
|
|
@@ -1208,6 +1251,65 @@ const TEST_FILES = [
|
|
|
1208
1251
|
path.join(REPO_ROOT, 'lib', 'memory', 'command-registry.test.cjs'),
|
|
1209
1252
|
// Phase 122-03: the chain recommender (recommendFrameworkChain via FEEDS_INTO).
|
|
1210
1253
|
path.join(REPO_ROOT, 'lib', 'memory', 'chain-recommender.test.cjs'),
|
|
1254
|
+
// Phase 122-04: the navigation-hook surgical edit (framework-chain-composer
|
|
1255
|
+
// routed through the resolver) + the suggest-next integration test.
|
|
1256
|
+
path.join(REPO_ROOT, 'lib', 'memory', 'navigation-hook-resolver.test.cjs'),
|
|
1257
|
+
path.join(REPO_ROOT, 'lib', 'memory', 'suggest-next-workflow.test.cjs'),
|
|
1258
|
+
// Phase 122-05: the workflow-layer end-to-end test (frontmatter -> registry
|
|
1259
|
+
// --check -> resolver.composeWorkflow(acceptance example) -> the degrade case
|
|
1260
|
+
// -> validateChainAutonomy stop-point) + the Canon Part 8 zero-Brain-mutation
|
|
1261
|
+
// grep sweep.
|
|
1262
|
+
path.join(REPO_ROOT, 'lib', 'memory', 'workflow-layer-e2e.test.cjs'),
|
|
1263
|
+
// Phase 110-00: Brain Context Packet Contract Wave 0 substrate (4 stubs filled by Plans 110-01 / 110-04 / 110-05).
|
|
1264
|
+
// test-brain-packet-schema-check.cjs -> 110-01 (PACKET-110-01 + -02: the --check schema tripwire)
|
|
1265
|
+
// test-brain-packet-validation-per-job.cjs -> 110-05 (PACKET-110-03 + -04 + -07 + -08: 12-job in/out + privacy + dual-path)
|
|
1266
|
+
// test-brain-packet-part8-invariant-per-job.cjs -> 110-05 (PACKET-110-06 round-trip + D-11(d) adversarial sweep)
|
|
1267
|
+
// test-brain-packet-precommit-hook.cjs -> 110-04 (PACKET-110-05 D-08 layer-2 hook)
|
|
1268
|
+
path.join(REPO_ROOT, 'tests', 'test-brain-packet-schema-check.cjs'),
|
|
1269
|
+
path.join(REPO_ROOT, 'tests', 'test-brain-packet-validation-per-job.cjs'),
|
|
1270
|
+
path.join(REPO_ROOT, 'tests', 'test-brain-packet-part8-invariant-per-job.cjs'),
|
|
1271
|
+
path.join(REPO_ROOT, 'tests', 'test-brain-packet-precommit-hook.cjs'),
|
|
1272
|
+
// Phase 123 (install-lifecycle-harness) block.
|
|
1273
|
+
// Plan 123-01: release.sh semver bump algebra + two-commit form + dirty-repo guard + Step 9.5 rename.
|
|
1274
|
+
// Tests A-E (semver assertions) GREEN immediately; Tests F/G (release.sh
|
|
1275
|
+
// structural) RED until Plan 123-01 Task 2 rewrites release.sh -- intended RED->GREEN.
|
|
1276
|
+
// Plan 123-02: install-state record + data/deployment-surfaces.json manifest +
|
|
1277
|
+
// active-plugin-root.cjs topology classification. Tests 1+4+6 (topology /
|
|
1278
|
+
// Canon Part 8 / early-write ordering) GREEN after Task 1; Tests 2+3
|
|
1279
|
+
// (record write hermetic / idempotent re-run) GREEN after Task 2;
|
|
1280
|
+
// Test 5 (manifest schema) GREEN after Task 3.
|
|
1281
|
+
// Plan 123-03: doctor classes I (install-state + topology + 6-way version-
|
|
1282
|
+
// of-record consistency) + J (deployment-surface manifest reconciliation)
|
|
1283
|
+
// + aggressive --fix (legacy migration backup-verify-remove; never
|
|
1284
|
+
// touches a dev-clone) + Bug-7 fix (marketplace-cache topology is
|
|
1285
|
+
// HEALTHY, not drift). Tests RED until Task 2 lands class I + class J
|
|
1286
|
+
// in scripts/doctor.cjs -- intended RED->GREEN.
|
|
1287
|
+
// Plan 123-04: doctor --acceptance (release-gate-as-a-command) -- 5-point
|
|
1288
|
+
// pre-tag checklist + 7-point full checklist + --light-npx opt-in; wired
|
|
1289
|
+
// into release.sh as hard aborts (Step 6.6 pre-tag, Step 9.6 post-publish);
|
|
1290
|
+
// scripts/release-beta-smoke.sh retired. Tests RED until Task 2 lands
|
|
1291
|
+
// --acceptance in scripts/doctor.cjs AND Task 3 wires release.sh + deletes
|
|
1292
|
+
// release-beta-smoke.sh -- intended RED->GREEN.
|
|
1293
|
+
// Plan 123-05: cache-prune helper (HARNESS-123-13) + doc/test sweep
|
|
1294
|
+
// (HARNESS-123-14). 6 hermetic scenarios for pruneMarketplaceCache
|
|
1295
|
+
// (active + N most-recent kept; corrupt installed_plugins.json -> skip;
|
|
1296
|
+
// dryRun -> no mutation; belt+suspenders active-dir protection; Canon
|
|
1297
|
+
// Part 8 grep clean). Tests GREEN after Task 1 lands
|
|
1298
|
+
// lib/core/cache-prune.cjs.
|
|
1299
|
+
// Plan 123-07: resolve-brain-key.cjs (HARNESS-123-15) + brain-client.cjs
|
|
1300
|
+
// getApiKey() delegation (HARNESS-123-16). 9 hermetic scenarios cover
|
|
1301
|
+
// order (env -> ~/.mindrian.env -> CWD .env -> not-found), SEC-02
|
|
1302
|
+
// POSIX 0o077 reject, Canon Part 8 zero-network grep, the brain-client
|
|
1303
|
+
// delegation spy, brain-client preconditions, and the FLAG-3
|
|
1304
|
+
// env-aware-home structural assertion. rbk.1-6 + rbk.9 GREEN after
|
|
1305
|
+
// Task 1; rbk.7 + rbk.8 GREEN after Task 2 (brain-client rewire).
|
|
1306
|
+
path.join(REPO_ROOT, 'tests', 'test-release-bump-algebra.cjs'),
|
|
1307
|
+
path.join(REPO_ROOT, 'tests', 'test-install-state-record.cjs'),
|
|
1308
|
+
path.join(REPO_ROOT, 'tests', 'test-doctor-class-i.cjs'),
|
|
1309
|
+
path.join(REPO_ROOT, 'tests', 'test-doctor-class-j.cjs'),
|
|
1310
|
+
path.join(REPO_ROOT, 'tests', 'test-doctor-acceptance.cjs'),
|
|
1311
|
+
path.join(REPO_ROOT, 'tests', 'test-cache-prune.cjs'),
|
|
1312
|
+
path.join(REPO_ROOT, 'tests', 'test-resolve-brain-key.cjs'),
|
|
1211
1313
|
];
|
|
1212
1314
|
|
|
1213
1315
|
// Exit code convention for child tests:
|
|
@@ -290,14 +290,31 @@ test('brain-client.cjs has zero legacy .replace(/"/g, ...) injection patterns',
|
|
|
290
290
|
);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
test('
|
|
293
|
+
test('SEC-02 .env gating lives in resolve-brain-key.cjs (Phase 123 Plan-07)', () => {
|
|
294
|
+
// Phase 123 Plan-07: getApiKey() now delegates to lib/core/resolve-brain-key.cjs,
|
|
295
|
+
// which owns the SEC-02 POSIX 0o077 permission check for both ~/.mindrian.env
|
|
296
|
+
// and CWD .env. The brain-client.cjs::checkFilePermissions() helper remains
|
|
297
|
+
// exported via _test for backward-compat (and unit-test surface above), but
|
|
298
|
+
// the live gating call sites moved one layer down. The invariant is:
|
|
299
|
+
// 1. brain-client.cjs requires resolve-brain-key.cjs (delegation lives).
|
|
300
|
+
// 2. resolve-brain-key.cjs contains the 0o077 mask check (SEC-02 lives).
|
|
301
|
+
// Both must hold; either failure is a regression that re-introduces the
|
|
302
|
+
// pre-Plan-07 multiple-resolver disease.
|
|
294
303
|
const brainPath = path.resolve(__dirname, '..', 'core', 'brain-client.cjs');
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
|
|
304
|
+
const resolverPath = path.resolve(__dirname, '..', 'core', 'resolve-brain-key.cjs');
|
|
305
|
+
const brainSrc = fs.readFileSync(brainPath, 'utf8');
|
|
306
|
+
const resolverSrc = fs.readFileSync(resolverPath, 'utf8');
|
|
298
307
|
assert.ok(
|
|
299
|
-
|
|
300
|
-
|
|
308
|
+
/require\(['"][^'"]*resolve-brain-key[^'"]*['"]\)/.test(brainSrc),
|
|
309
|
+
'brain-client.cjs must require resolve-brain-key.cjs (Phase 123 Plan-07 delegation)'
|
|
310
|
+
);
|
|
311
|
+
assert.ok(
|
|
312
|
+
/0o077/.test(resolverSrc),
|
|
313
|
+
'resolve-brain-key.cjs must contain the SEC-02 0o077 mask check'
|
|
314
|
+
);
|
|
315
|
+
assert.ok(
|
|
316
|
+
/process\.platform/.test(resolverSrc),
|
|
317
|
+
'resolve-brain-key.cjs must short-circuit the SEC-02 check on Windows (process.platform)'
|
|
301
318
|
);
|
|
302
319
|
});
|
|
303
320
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 122-04 -- suggest-next-workflow integration test (new suite; registered in the Feynman runner).
|
|
8
|
+
* ====================================================================================================
|
|
9
|
+
* The end-to-end check for the Workflow Layer's user-facing slice:
|
|
10
|
+
* - a fixture room with a ProblemType -> /mos:suggest-next returns a COMMAND
|
|
11
|
+
* SEQUENCE (a step-numbered framework list AND at least one /mos: token),
|
|
12
|
+
* and every /mos: it emits exists in data/command-registry.json (no
|
|
13
|
+
* hallucinated command);
|
|
14
|
+
* - a synthetic chain that includes /mos:hat-briefing -> validateChainAutonomy
|
|
15
|
+
* reports it as a blocker (the /mos:act --chain stop point);
|
|
16
|
+
* - /mos:pipeline --from-problem-type <x> derives a Brain-derived command
|
|
17
|
+
* chain (the helper prints a run order of registered /mos: commands);
|
|
18
|
+
* - /mos:act --chain stops at the first non-autonomous_safe step (here,
|
|
19
|
+
* /mos:hat-briefing for Six Thinking Hats).
|
|
20
|
+
*
|
|
21
|
+
* Hermetic: a tmp room dir + a STATE.md with "Problem Type: ill-defined" (no
|
|
22
|
+
* network -- recommendFrameworkChain degrades to [seed] without offline edges,
|
|
23
|
+
* and that seed is what the room's ProblemType maps to via problem-type-router).
|
|
24
|
+
*
|
|
25
|
+
* Registered in lib/memory/run-feynman-tests.cjs TEST_FILES[] and in
|
|
26
|
+
* tests/run-all-122.sh CJS_SUITES (as ../lib/memory/suggest-next-workflow.test.cjs).
|
|
27
|
+
* Run: node lib/memory/suggest-next-workflow.test.cjs
|
|
28
|
+
* Exit 0 on pass; throws (node:assert) on any fail.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const assert = require('node:assert/strict');
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const os = require('node:os');
|
|
34
|
+
const path = require('node:path');
|
|
35
|
+
const { spawnSync } = require('node:child_process');
|
|
36
|
+
|
|
37
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
38
|
+
const REGISTRY_PATH = path.join(REPO_ROOT, 'data', 'command-registry.json');
|
|
39
|
+
const SUGGEST_SCRIPT = path.join(REPO_ROOT, 'scripts', 'suggest-next-command.cjs');
|
|
40
|
+
const PIPELINE_SCRIPT = path.join(REPO_ROOT, 'scripts', 'pipeline-command.cjs');
|
|
41
|
+
const ACT_SCRIPT = path.join(REPO_ROOT, 'scripts', 'act-command.cjs');
|
|
42
|
+
|
|
43
|
+
const resolver = require('../workflow/command-resolver.cjs');
|
|
44
|
+
const recommender = require('../brain/chain-recommender.cjs');
|
|
45
|
+
|
|
46
|
+
const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
47
|
+
const REGISTERED_COMMANDS = new Set((registry.commands || []).map(function (c) { return c && c.command; }));
|
|
48
|
+
|
|
49
|
+
let passed = 0;
|
|
50
|
+
function test(name, fn) {
|
|
51
|
+
fn();
|
|
52
|
+
process.stdout.write(' ok ' + name + '\n');
|
|
53
|
+
passed += 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract /mos:... tokens from a string.
|
|
57
|
+
function extractCommands(text) {
|
|
58
|
+
const m = String(text).match(/\/mos:[a-z][a-z0-9-]*/g);
|
|
59
|
+
return m ? m.slice() : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A fixture room: a tmp dir with a STATE.md carrying a ProblemType line.
|
|
63
|
+
function makeFixtureRoom(problemTypeLine) {
|
|
64
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'p122-04-suggest-'));
|
|
65
|
+
const roomDir = path.join(root, 'room');
|
|
66
|
+
fs.mkdirSync(roomDir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(path.join(roomDir, 'STATE.md'),
|
|
68
|
+
'# Room State\n\n' + problemTypeLine + '\n\nStage: discovery\n');
|
|
69
|
+
return { root: root, roomDir: roomDir };
|
|
70
|
+
}
|
|
71
|
+
function rmFixture(fx) { try { fs.rmSync(fx.root, { recursive: true, force: true }); } catch (_e) {} }
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// 1. fixture room with a ProblemType -> /mos:suggest-next returns a command SEQUENCE
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
test('a fixture room with a ProblemType -> /mos:suggest-next prints a step-numbered command sequence; every /mos: is registered', function () {
|
|
77
|
+
const fx = makeFixtureRoom('Problem Type: ill-defined');
|
|
78
|
+
try {
|
|
79
|
+
const res = spawnSync(process.execPath, [SUGGEST_SCRIPT, '--room', fx.roomDir], { encoding: 'utf8' });
|
|
80
|
+
assert.strictEqual(res.status, 0, 'suggest-next-command.cjs exits 0; stderr: ' + (res.stderr || ''));
|
|
81
|
+
const out = res.stdout || '';
|
|
82
|
+
// It is a SEQUENCE: a "Command sequence" header + a step-numbered list + at least one /mos: token.
|
|
83
|
+
assert.ok(/command sequence/i.test(out), 'output names a command sequence; got:\n' + out);
|
|
84
|
+
assert.ok(/^\s*\d+\.\s/m.test(out), 'output has a step-numbered list; got:\n' + out);
|
|
85
|
+
assert.ok(/recommended framework chain/i.test(out), 'output shows the framework chain too');
|
|
86
|
+
const cmds = extractCommands(out);
|
|
87
|
+
assert.ok(cmds.length >= 1, 'output emits at least one /mos: command; got:\n' + out);
|
|
88
|
+
// Every /mos: it emits in the SEQUENCE/chain is a registered command (the
|
|
89
|
+
// footer cross-references /mos:status, /mos:pipeline -- also registered).
|
|
90
|
+
for (const c of cmds) {
|
|
91
|
+
assert.ok(REGISTERED_COMMANDS.has(c), 'emitted command must exist in data/command-registry.json: ' + c + '\nfull output:\n' + out);
|
|
92
|
+
}
|
|
93
|
+
// And the methodology command in the sequence is the resolver's answer for
|
|
94
|
+
// the IDP seed framework -- not a hallucinated one (in particular, NOT
|
|
95
|
+
// /mos:jtbd, which does not exist).
|
|
96
|
+
assert.ok(!/\/mos:jtbd\b/.test(out), 'must not emit the non-existent /mos:jtbd');
|
|
97
|
+
} finally {
|
|
98
|
+
rmFixture(fx);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// 2. direct: recommendFrameworkChain + composeWorkflow for the IDP room state
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
test('recommendFrameworkChain({roomState:{problemType:"ill-defined"}}) -> composeWorkflow -> a chain of registered commands (no hallucination)', function () {
|
|
106
|
+
const chain = recommender.recommendFrameworkChain({ roomState: { problemType: 'ill-defined' } });
|
|
107
|
+
assert.ok(Array.isArray(chain) && chain.length >= 1, 'a non-empty framework chain');
|
|
108
|
+
const wf = resolver.composeWorkflow(chain);
|
|
109
|
+
assert.ok(Array.isArray(wf) && wf.length === chain.length);
|
|
110
|
+
wf.forEach(function (s, i) {
|
|
111
|
+
assert.strictEqual(s.step, i + 1);
|
|
112
|
+
assert.ok('framework' in s && 'command' in s && 'optional' in s);
|
|
113
|
+
if (s.command !== null) {
|
|
114
|
+
assert.ok(typeof s.command === 'string' && s.command.indexOf('/mos:') === 0);
|
|
115
|
+
assert.ok(REGISTERED_COMMANDS.has(s.command), 'composeWorkflow only ever yields registered commands: ' + s.command);
|
|
116
|
+
} else {
|
|
117
|
+
assert.strictEqual(s.optional, true, 'a command-less step is marked optional (run it manually)');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// 3. a synthetic chain including /mos:hat-briefing -> validateChainAutonomy flags it
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
test('a synthetic chain that includes /mos:hat-briefing -> validateChainAutonomy reports it as a blocker (the /mos:act --chain stop point)', function () {
|
|
126
|
+
// Six Thinking Hats resolves to /mos:hat-briefing (autonomous_safe: false in
|
|
127
|
+
// the registry) as its first command -- so a chain through it is the stop point.
|
|
128
|
+
const sixHatsCmds = resolver.commandsForFramework('Six Thinking Hats');
|
|
129
|
+
assert.ok(sixHatsCmds.length > 0 && sixHatsCmds[0] === '/mos:hat-briefing',
|
|
130
|
+
'Six Thinking Hats resolves to /mos:hat-briefing first; got: ' + JSON.stringify(sixHatsCmds));
|
|
131
|
+
const wf = resolver.composeWorkflow(['Beautiful Question Framework', 'Six Thinking Hats']);
|
|
132
|
+
const report = resolver.validateChainAutonomy(wf);
|
|
133
|
+
assert.strictEqual(report.runnable, false, 'a chain with /mos:hat-briefing is not fully runnable');
|
|
134
|
+
assert.ok(Array.isArray(report.blockers) && report.blockers.length >= 1);
|
|
135
|
+
const blocked = report.blockers.find(function (b) { return b && b.command === '/mos:hat-briefing'; });
|
|
136
|
+
assert.ok(blocked, 'the blocker names /mos:hat-briefing; report: ' + JSON.stringify(report));
|
|
137
|
+
assert.strictEqual(blocked.step, 2, 'the blocker is step 2 (the second step in the chain)');
|
|
138
|
+
// Step 1 (/mos:beautiful-question, autonomous_safe) is NOT a blocker.
|
|
139
|
+
assert.ok(!report.blockers.some(function (b) { return b && b.step === 1; }), 'step 1 is autonomous_safe -- not a blocker');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// 4. /mos:pipeline --from-problem-type ill-defined prints a Brain-derived command chain
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
test('/mos:pipeline --from-problem-type ill-defined prints a run order of registered /mos: commands', function () {
|
|
146
|
+
const res = spawnSync(process.execPath, [PIPELINE_SCRIPT, '--from-problem-type', 'ill-defined'], { encoding: 'utf8' });
|
|
147
|
+
assert.strictEqual(res.status, 0, 'pipeline-command.cjs exits 0; stderr: ' + (res.stderr || ''));
|
|
148
|
+
const out = res.stdout || '';
|
|
149
|
+
assert.ok(/brain-derived framework chain/i.test(out), 'output names the Brain-derived chain; got:\n' + out);
|
|
150
|
+
assert.ok(/run order/i.test(out), 'output shows a run order; got:\n' + out);
|
|
151
|
+
const cmds = extractCommands(out);
|
|
152
|
+
assert.ok(cmds.length >= 1, 'output emits at least one /mos: command');
|
|
153
|
+
for (const c of cmds) {
|
|
154
|
+
assert.ok(REGISTERED_COMMANDS.has(c), 'pipeline run-order command must be registered: ' + c + '\nfull output:\n' + out);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// 5. /mos:act --chain --from-framework "Six Thinking Hats" stops at the first non-autonomous step
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
test('/mos:act --chain stops at the first non-autonomous_safe step (Six Thinking Hats -> /mos:hat-briefing)', function () {
|
|
162
|
+
const res = spawnSync(process.execPath, [ACT_SCRIPT, '--chain', '--from-framework', 'Six Thinking Hats'], { encoding: 'utf8' });
|
|
163
|
+
assert.strictEqual(res.status, 0, 'act-command.cjs exits 0; stderr: ' + (res.stderr || ''));
|
|
164
|
+
const out = res.stdout || '';
|
|
165
|
+
assert.ok(/\[GATE\]/.test(out), 'output renders a needs-you-here gate; got:\n' + out);
|
|
166
|
+
assert.ok(/not autonomous_safe/i.test(out), 'the gate cites the autonomy reason');
|
|
167
|
+
assert.ok(/\/mos:hat-briefing/.test(out), 'the gate names /mos:hat-briefing (the stop point)');
|
|
168
|
+
assert.ok(/runnable:\s*false/i.test(out), 'validateChainAutonomy reported runnable=false');
|
|
169
|
+
// Every /mos: it prints is registered.
|
|
170
|
+
for (const c of extractCommands(out)) {
|
|
171
|
+
assert.ok(REGISTERED_COMMANDS.has(c), 'act --chain command must be registered: ' + c + '\nfull output:\n' + out);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
process.stdout.write('\nsuggest-next-workflow.test.cjs: ' + passed + ' assertion groups PASSED\n');
|
|
176
|
+
process.exit(0);
|