@mindrian_os/install 1.13.0-beta.11 → 1.13.0-beta.12
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 +26 -3
- package/bin/cli.js +114 -57
- package/commands/act.md +16 -2
- package/commands/pipeline.md +16 -1
- package/commands/suggest-next.md +17 -3
- package/lib/core/active-plugin-root.cjs +142 -0
- package/lib/core/framework-chain-composer.cjs +156 -43
- package/lib/core/migrations/phase-109-nodes-provenance.cjs +47 -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 +17 -0
- 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 +1 -1
- package/references/brain/command-triggers-schema.md +10 -221
- package/references/methodology/index.md +11 -74
- package/skills/brain-connector/SKILL.md +3 -5
- 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,14 @@ 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'),
|
|
1169
1177
|
// Phase 89-07 Wave 0 (graph-native HARD RULE; ReverseSalientAgent dual-surface).
|
|
1170
1178
|
path.join(REPO_ROOT, 'tests', 'test-reverse-salient-agent.cjs'),
|
|
1171
1179
|
path.join(REPO_ROOT, 'tests', 'test-reverse-salient-cascade-emit.cjs'),
|
|
@@ -1208,6 +1216,15 @@ const TEST_FILES = [
|
|
|
1208
1216
|
path.join(REPO_ROOT, 'lib', 'memory', 'command-registry.test.cjs'),
|
|
1209
1217
|
// Phase 122-03: the chain recommender (recommendFrameworkChain via FEEDS_INTO).
|
|
1210
1218
|
path.join(REPO_ROOT, 'lib', 'memory', 'chain-recommender.test.cjs'),
|
|
1219
|
+
// Phase 122-04: the navigation-hook surgical edit (framework-chain-composer
|
|
1220
|
+
// routed through the resolver) + the suggest-next integration test.
|
|
1221
|
+
path.join(REPO_ROOT, 'lib', 'memory', 'navigation-hook-resolver.test.cjs'),
|
|
1222
|
+
path.join(REPO_ROOT, 'lib', 'memory', 'suggest-next-workflow.test.cjs'),
|
|
1223
|
+
// Phase 122-05: the workflow-layer end-to-end test (frontmatter -> registry
|
|
1224
|
+
// --check -> resolver.composeWorkflow(acceptance example) -> the degrade case
|
|
1225
|
+
// -> validateChainAutonomy stop-point) + the Canon Part 8 zero-Brain-mutation
|
|
1226
|
+
// grep sweep.
|
|
1227
|
+
path.join(REPO_ROOT, 'lib', 'memory', 'workflow-layer-e2e.test.cjs'),
|
|
1211
1228
|
];
|
|
1212
1229
|
|
|
1213
1230
|
// Exit code convention for child tests:
|
|
@@ -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);
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 122-05 -- workflow-layer end-to-end test (new suite; registered in the Feynman runner).
|
|
8
|
+
* =============================================================================================
|
|
9
|
+
* Walks the FULL chain frontmatter -> registry -> resolver -> composed chain,
|
|
10
|
+
* then runs the Canon Part 8 zero-Brain-mutation grep sweep:
|
|
11
|
+
*
|
|
12
|
+
* Test 1 (frontmatter -> registry consistency):
|
|
13
|
+
* spawnSync('node', ['scripts/build-command-registry.cjs', '--check']) -> status === 0
|
|
14
|
+
* (the committed data/command-registry.json is in sync with commands/*.md frontmatter).
|
|
15
|
+
*
|
|
16
|
+
* Test 2 (the spec's acceptance example):
|
|
17
|
+
* composeWorkflow(["Beautiful Question Framework","Domain Selection","Jobs to Be Done (JTBD)"])
|
|
18
|
+
* -> a 3-step array; each step 1-indexed, optional === false, command a non-null /mos:
|
|
19
|
+
* string that exists in data/command-registry.json commands[].command.
|
|
20
|
+
* (Shape + registered-ness asserted, not hardcoded slugs -- the 122-01 retrofit owns the
|
|
21
|
+
* exact strings.)
|
|
22
|
+
*
|
|
23
|
+
* Test 3 (the command-less degrade case):
|
|
24
|
+
* composeWorkflow([<a framework with no command in the registry>])
|
|
25
|
+
* -> [{step:1, framework:<that>, command:null, optional:true}].
|
|
26
|
+
* The command-less framework is picked dynamically: a name in data/framework-names.json
|
|
27
|
+
* that is NOT a key in framework_index (so it has no /mos: command). Falls back to
|
|
28
|
+
* "Red Teaming" if the dynamic pick fails (Red Teaming is command-less in the retrofit).
|
|
29
|
+
*
|
|
30
|
+
* Test 4 (the /mos:act --chain stop-point):
|
|
31
|
+
* a workflow including a command whose registry entry has autonomous_safe: false
|
|
32
|
+
* (dynamically: the first command of any framework whose first command is not
|
|
33
|
+
* autonomous_safe -- in the retrofit, "Six Thinking Hats" -> /mos:hat-briefing)
|
|
34
|
+
* -> validateChainAutonomy -> runnable === false AND blockers names that step.
|
|
35
|
+
*
|
|
36
|
+
* Test 5 (Canon Part 8 grep sweep):
|
|
37
|
+
* - for every .cjs under lib/brain/ and lib/workflow/: no line matches /mos:[a-z-]+ within
|
|
38
|
+
* ~80 chars of a brain/query/fetch/http token;
|
|
39
|
+
* - lib/workflow/command-resolver.cjs has no require(...brain-client...);
|
|
40
|
+
* - scripts/build-command-registry.cjs has no write-Cypher (/CREATE |MERGE |SET |DELETE /i);
|
|
41
|
+
* - grep -rE "Brain has Command|brain_proactive_command|FOLLOWS_FRAMEWORK.*Command|:Command"
|
|
42
|
+
* skills/ agents/ references/ -> empty (no Command-node assertion left anywhere).
|
|
43
|
+
*
|
|
44
|
+
* Hermetic: zero network. The only Brain touch in the whole Workflow Layer is the build-time
|
|
45
|
+
* --refresh-names (not exercised here -- Test 1 runs --check, which regenerates from frontmatter
|
|
46
|
+
* against the committed allowlist and never queries the Brain).
|
|
47
|
+
*
|
|
48
|
+
* Registered in lib/memory/run-feynman-tests.cjs TEST_FILES[] and tests/run-all-122.sh CJS_SUITES
|
|
49
|
+
* (as ../lib/memory/workflow-layer-e2e.test.cjs).
|
|
50
|
+
* Run: node lib/memory/workflow-layer-e2e.test.cjs
|
|
51
|
+
* Exit 0 on pass; throws (node:assert) on any fail.
|
|
52
|
+
*
|
|
53
|
+
* License: BSL 1.1.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const assert = require('node:assert/strict');
|
|
57
|
+
const fs = require('node:fs');
|
|
58
|
+
const path = require('node:path');
|
|
59
|
+
const { spawnSync } = require('node:child_process');
|
|
60
|
+
|
|
61
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
62
|
+
const REGISTRY_PATH = path.join(REPO_ROOT, 'data', 'command-registry.json');
|
|
63
|
+
const FW_NAMES_PATH = path.join(REPO_ROOT, 'data', 'framework-names.json');
|
|
64
|
+
const BUILD_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-command-registry.cjs');
|
|
65
|
+
|
|
66
|
+
const resolver = require('../workflow/command-resolver.cjs');
|
|
67
|
+
|
|
68
|
+
const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
69
|
+
const REGISTERED_COMMANDS = new Set((registry.commands || []).map(function (c) { return c && c.command; }));
|
|
70
|
+
const COMMAND_BY_NAME = new Map((registry.commands || []).map(function (c) { return [c && c.command, c]; }));
|
|
71
|
+
|
|
72
|
+
let passed = 0;
|
|
73
|
+
function test(name, fn) {
|
|
74
|
+
fn();
|
|
75
|
+
passed += 1;
|
|
76
|
+
process.stdout.write(' ok ' + name + '\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------- helpers ----------
|
|
80
|
+
|
|
81
|
+
function listCjsFilesUnder(relDir) {
|
|
82
|
+
const dir = path.join(REPO_ROOT, relDir);
|
|
83
|
+
const out = [];
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
} catch (_e) {
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
for (const e of entries) {
|
|
91
|
+
const p = path.join(dir, e.name);
|
|
92
|
+
if (e.isDirectory()) {
|
|
93
|
+
out.push.apply(out, listCjsFilesUnder(path.join(relDir, e.name)));
|
|
94
|
+
} else if (e.isFile() && /\.cjs$/.test(e.name)) {
|
|
95
|
+
out.push(p);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// True iff a /mos:<slug> literal appears within `window` chars of a
|
|
102
|
+
// brain/query/fetch/http token on the same source (a heuristic for "a command
|
|
103
|
+
// string in a Brain-query payload builder"). Test files are excluded -- they
|
|
104
|
+
// legitimately mention /mos: literals near assertion text.
|
|
105
|
+
function hasCommandNearBrainToken(src, window) {
|
|
106
|
+
const w = (typeof window === 'number' && window > 0) ? window : 80;
|
|
107
|
+
const re = /\/mos:[a-z][a-z0-9-]*/g;
|
|
108
|
+
let m;
|
|
109
|
+
while ((m = re.exec(src)) !== null) {
|
|
110
|
+
const start = Math.max(0, m.index - w);
|
|
111
|
+
const end = Math.min(src.length, m.index + m[0].length + w);
|
|
112
|
+
const around = src.slice(start, end).toLowerCase();
|
|
113
|
+
if (/\b(brain|query|fetch|http)\b/.test(around)) return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Recursively grep a directory for any line matching `re`. Returns [{file, line}].
|
|
119
|
+
function grepDirForRegex(relDir, re) {
|
|
120
|
+
const dir = path.join(REPO_ROOT, relDir);
|
|
121
|
+
const hits = [];
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
|
+
} catch (_e) {
|
|
126
|
+
return hits;
|
|
127
|
+
}
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
const p = path.join(dir, e.name);
|
|
130
|
+
if (e.isDirectory()) {
|
|
131
|
+
hits.push.apply(hits, grepDirForRegex(path.join(relDir, e.name), re));
|
|
132
|
+
} else if (e.isFile()) {
|
|
133
|
+
let src;
|
|
134
|
+
try { src = fs.readFileSync(p, 'utf8'); } catch (_e2) { continue; }
|
|
135
|
+
const lines = src.split(/\r?\n/);
|
|
136
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
137
|
+
if (re.test(lines[i])) hits.push({ file: path.relative(REPO_ROOT, p), line: i + 1, text: lines[i].trim() });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return hits;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------- Test 1: frontmatter -> registry consistency ----------
|
|
145
|
+
|
|
146
|
+
test('build-command-registry.cjs --check exits 0 (the committed registry is in sync with commands/*.md frontmatter)', function () {
|
|
147
|
+
assert.ok(fs.existsSync(BUILD_SCRIPT), 'scripts/build-command-registry.cjs must exist');
|
|
148
|
+
const res = spawnSync(process.execPath, [BUILD_SCRIPT, '--check'], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
149
|
+
assert.equal(res.status, 0, 'build-command-registry.cjs --check should exit 0; stderr: ' + (res.stderr || '') + ' stdout: ' + (res.stdout || ''));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---------- Test 2: the spec's acceptance example ----------
|
|
153
|
+
|
|
154
|
+
test('composeWorkflow(["Beautiful Question Framework","Domain Selection","Jobs to Be Done (JTBD)"]) -> a 3-step workflow, every command a registered /mos: string', function () {
|
|
155
|
+
const chain = ['Beautiful Question Framework', 'Domain Selection', 'Jobs to Be Done (JTBD)'];
|
|
156
|
+
const wf = resolver.composeWorkflow(chain);
|
|
157
|
+
assert.ok(Array.isArray(wf), 'composeWorkflow must return an array');
|
|
158
|
+
assert.equal(wf.length, 3, 'the acceptance example is a 3-step workflow');
|
|
159
|
+
for (let i = 0; i < wf.length; i += 1) {
|
|
160
|
+
const s = wf[i];
|
|
161
|
+
assert.equal(s.step, i + 1, 'step ' + (i + 1) + ' must be 1-indexed in order');
|
|
162
|
+
assert.equal(s.framework, chain[i], 'step ' + (i + 1) + ' framework must match the input chain');
|
|
163
|
+
assert.equal(s.optional, false, 'step ' + (i + 1) + ' must not be optional (all three frameworks have a command)');
|
|
164
|
+
assert.equal(typeof s.command, 'string', 'step ' + (i + 1) + ' command must be a string');
|
|
165
|
+
assert.match(s.command, /^\/mos:[a-z][a-z0-9-]*$/, 'step ' + (i + 1) + ' command must be a /mos: slug');
|
|
166
|
+
assert.ok(REGISTERED_COMMANDS.has(s.command), 'step ' + (i + 1) + ' command ' + s.command + ' must exist in data/command-registry.json (no hallucinated command)');
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------- Test 3: the command-less degrade case ----------
|
|
171
|
+
|
|
172
|
+
test('composeWorkflow([<a command-less framework>]) -> [{step:1, command:null, optional:true}] (degrade, do not fabricate)', function () {
|
|
173
|
+
// Pick a framework with no /mos: command dynamically: a name in
|
|
174
|
+
// data/framework-names.json that is NOT a key in framework_index. Fall back
|
|
175
|
+
// to "Red Teaming" if the dynamic pick somehow fails.
|
|
176
|
+
let commandless = 'Red Teaming';
|
|
177
|
+
try {
|
|
178
|
+
const fwNames = JSON.parse(fs.readFileSync(FW_NAMES_PATH, 'utf8'));
|
|
179
|
+
const names = Array.isArray(fwNames && fwNames.framework_names) ? fwNames.framework_names : [];
|
|
180
|
+
const idxKeys = new Set(Object.keys(registry.framework_index || {}));
|
|
181
|
+
const cand = names.find(function (n) { return !idxKeys.has(n) && resolver.commandsForFramework(n).length === 0; });
|
|
182
|
+
if (cand) commandless = cand;
|
|
183
|
+
} catch (_e) { /* keep the Red Teaming fallback */ }
|
|
184
|
+
assert.equal(resolver.commandsForFramework(commandless).length, 0, 'the chosen framework "' + commandless + '" must have no /mos: command');
|
|
185
|
+
const wf = resolver.composeWorkflow([commandless]);
|
|
186
|
+
assert.deepEqual(wf, [{ step: 1, framework: commandless, command: null, optional: true }],
|
|
187
|
+
'a command-less framework must yield exactly [{step:1, framework, command:null, optional:true}]');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------- Test 4: the /mos:act --chain stop-point ----------
|
|
191
|
+
|
|
192
|
+
test('validateChainAutonomy(a workflow including an autonomous_safe:false command) -> runnable false, the step is a blocker', function () {
|
|
193
|
+
// Pick a framework whose FIRST command (the one composeWorkflow picks) is
|
|
194
|
+
// autonomous_safe: false -- in the 122-01 retrofit that is "Six Thinking
|
|
195
|
+
// Hats" -> /mos:hat-briefing. Find it dynamically; fail loudly if none.
|
|
196
|
+
let blockedFramework = null;
|
|
197
|
+
let blockedCommand = null;
|
|
198
|
+
for (const [fw, cmds] of Object.entries(registry.framework_index || {})) {
|
|
199
|
+
if (Array.isArray(cmds) && cmds.length > 0) {
|
|
200
|
+
const c = COMMAND_BY_NAME.get(cmds[0]);
|
|
201
|
+
if (c && c.autonomous_safe === false) { blockedFramework = fw; blockedCommand = cmds[0]; break; }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
assert.ok(blockedFramework, 'expected at least one framework whose first command is autonomous_safe:false (e.g. Six Thinking Hats -> /mos:hat-briefing)');
|
|
205
|
+
const wf = resolver.composeWorkflow([blockedFramework]);
|
|
206
|
+
assert.equal(wf.length, 1);
|
|
207
|
+
assert.equal(wf[0].command, blockedCommand);
|
|
208
|
+
const v = resolver.validateChainAutonomy(wf);
|
|
209
|
+
assert.equal(v.runnable, false, '/mos:act --chain must NOT be runnable when it includes a non-autonomous_safe command (' + blockedCommand + ')');
|
|
210
|
+
assert.ok(Array.isArray(v.blockers) && v.blockers.length >= 1, 'there must be at least one blocker');
|
|
211
|
+
assert.ok(v.blockers.some(function (b) { return b && b.command === blockedCommand && b.step === 1; }),
|
|
212
|
+
'the blocker list must name step 1 / ' + blockedCommand);
|
|
213
|
+
// Sanity: an all-autonomous_safe workflow is runnable.
|
|
214
|
+
const v2 = resolver.validateChainAutonomy([
|
|
215
|
+
{ step: 1, framework: 'Beautiful Question Framework', command: '/mos:beautiful-question', optional: false },
|
|
216
|
+
]);
|
|
217
|
+
// Only assert runnable if /mos:beautiful-question is autonomous_safe in the registry (it is, in the retrofit).
|
|
218
|
+
if ((COMMAND_BY_NAME.get('/mos:beautiful-question') || {}).autonomous_safe === true) {
|
|
219
|
+
assert.equal(v2.runnable, true, 'an all-autonomous_safe workflow must be runnable');
|
|
220
|
+
assert.deepEqual(v2.blockers, []);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---------- Test 5: Canon Part 8 grep sweep ----------
|
|
225
|
+
|
|
226
|
+
test('Canon Part 8 grep sweep: no /mos: literal near a brain/query/fetch/http token in lib/brain/ or lib/workflow/ (non-test .cjs)', function () {
|
|
227
|
+
const files = listCjsFilesUnder('lib/brain').concat(listCjsFilesUnder('lib/workflow'));
|
|
228
|
+
assert.ok(files.length > 0, 'expected at least one .cjs under lib/brain/ + lib/workflow/');
|
|
229
|
+
for (const f of files) {
|
|
230
|
+
if (/\.test\.cjs$/.test(f)) continue; // test files legitimately mention /mos: near assertion prose
|
|
231
|
+
const src = fs.readFileSync(f, 'utf8');
|
|
232
|
+
assert.equal(hasCommandNearBrainToken(src, 80), false,
|
|
233
|
+
'a /mos: command literal appears within ~80 chars of a brain/query/fetch/http token in ' + path.relative(REPO_ROOT, f) + ' -- a Canon Part 8 breach signal');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('Canon Part 8 grep sweep: lib/workflow/command-resolver.cjs does not require a brain client', function () {
|
|
238
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'workflow', 'command-resolver.cjs'), 'utf8');
|
|
239
|
+
assert.equal(/require\([^)]*brain-client[^)]*\)/.test(src), false, 'command-resolver.cjs must not require brain-client');
|
|
240
|
+
assert.equal(/require\([^)]*brain-ask[^)]*\)/.test(src), false, 'command-resolver.cjs must not require brain-ask');
|
|
241
|
+
assert.equal(/\bfetch\s*\(/.test(src), false, 'command-resolver.cjs must not call fetch()');
|
|
242
|
+
assert.equal(/require\(['"]node:https?['"]\)/.test(src), false, 'command-resolver.cjs must not require node:http(s)');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('Canon Part 8 grep sweep: scripts/build-command-registry.cjs has no write-Cypher', function () {
|
|
246
|
+
const src = fs.readFileSync(BUILD_SCRIPT, 'utf8');
|
|
247
|
+
// Write-Cypher keywords as standalone tokens (avoid matching e.g. "settings" containing "SET").
|
|
248
|
+
assert.equal(/\b(CREATE|MERGE|SET|DELETE|DETACH)\s/i.test(src), false, 'build-command-registry.cjs must contain no write-Cypher');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('Canon Part 8 grep sweep: no Command-node assertion left in skills/ agents/ references/', function () {
|
|
252
|
+
const re = /Brain has Command|brain_proactive_command|FOLLOWS_FRAMEWORK.*Command|:Command/;
|
|
253
|
+
const hits = grepDirForRegex('skills', re)
|
|
254
|
+
.concat(grepDirForRegex('agents', re))
|
|
255
|
+
.concat(grepDirForRegex('references', re));
|
|
256
|
+
assert.deepEqual(hits, [], 'no Command-node assertion may survive anywhere in skills/ agents/ references/; found: ' + JSON.stringify(hits, null, 2));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ---------- summary ----------
|
|
260
|
+
|
|
261
|
+
process.stdout.write('\nworkflow-layer-e2e.test.cjs: ' + passed + ' assertion groups PASSED\n');
|
|
262
|
+
process.exit(0);
|