moflo 4.12.3 → 4.12.4-rc.2
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/agents/core/coder.md +2 -3
- package/.claude/agents/core/researcher.md +3 -4
- package/.claude/agents/core/reviewer.md +6 -8
- package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
- package/.claude/helpers/gate-hook.mjs +67 -2
- package/.claude/helpers/gate.cjs +173 -12
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/prompt-hook.mjs +0 -0
- package/.claude/skills/connector-builder/SKILL.md +5 -4
- package/.claude/skills/fl/phases.md +1 -1
- package/.claude/skills/fl/sdd.md +1 -1
- package/.claude/skills/spell-builder/SKILL.md +5 -4
- package/.claude/skills/spell-builder/architecture.md +4 -4
- package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
- package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
- package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
- package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
- package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
- package/.claude/skills/verify/SKILL.md +24 -3
- package/bin/gate-hook.mjs +67 -2
- package/bin/gate.cjs +173 -12
- package/bin/generate-code-map.mjs +70 -17
- package/bin/index-all.mjs +38 -0
- package/bin/index-guidance.mjs +8 -41
- package/bin/index-patterns.mjs +2 -2
- package/bin/index-tests.mjs +6 -4
- package/bin/lib/guidance-config.mjs +119 -0
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/dist/src/cli/commands/agent.js +1 -89
- package/dist/src/cli/commands/analyze.js +1 -1
- package/dist/src/cli/commands/benchmark.js +11 -8
- package/dist/src/cli/commands/claims.js +2 -2
- package/dist/src/cli/commands/completions.js +1 -1
- package/dist/src/cli/commands/config.js +256 -96
- package/dist/src/cli/commands/deployment.js +6 -6
- package/dist/src/cli/commands/diagnose.js +1 -1
- package/dist/src/cli/commands/doctor-checks-config.js +18 -22
- package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
- package/dist/src/cli/commands/doctor-fixes.js +36 -11
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +64 -23
- package/dist/src/cli/commands/github.js +1 -1
- package/dist/src/cli/commands/guidance.js +10 -10
- package/dist/src/cli/commands/hive-mind.js +8 -70
- package/dist/src/cli/commands/hooks.js +32 -219
- package/dist/src/cli/commands/mcp.js +5 -2
- package/dist/src/cli/commands/memory.js +59 -46
- package/dist/src/cli/commands/neural.js +21 -21
- package/dist/src/cli/commands/performance.js +14 -7
- package/dist/src/cli/commands/plugins.js +2 -2
- package/dist/src/cli/commands/providers.js +2 -2
- package/dist/src/cli/commands/retire.js +1 -1
- package/dist/src/cli/commands/route.js +1 -1
- package/dist/src/cli/commands/security.js +4 -4
- package/dist/src/cli/commands/status.js +18 -13
- package/dist/src/cli/commands/task.js +8 -0
- package/dist/src/cli/config/cli-config-store.js +326 -0
- package/dist/src/cli/hooks/workers/index.js +15 -9
- package/dist/src/cli/index.js +43 -33
- package/dist/src/cli/init/claudemd-generator.js +3 -3
- package/dist/src/cli/init/helpers-generator.js +203 -20
- package/dist/src/cli/init/moflo-yaml-template.js +11 -3
- package/dist/src/cli/init/settings-generator.js +10 -2
- package/dist/src/cli/mcp-client.js +12 -2
- package/dist/src/cli/mcp-server.js +5 -23
- package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
- package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
- package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
- package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
- package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
- package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
- package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
- package/dist/src/cli/mcp-tools/security-tools.js +1 -1
- package/dist/src/cli/mcp-tools/session-tools.js +116 -0
- package/dist/src/cli/mcp-tools/synthetic.js +74 -0
- package/dist/src/cli/mcp-tools/system-tools.js +126 -78
- package/dist/src/cli/mcp-tools/task-tools.js +101 -1
- package/dist/src/cli/movector/coverage-router.js +48 -6
- package/dist/src/cli/movector/diff-classifier.js +8 -6
- package/dist/src/cli/movector/flash-attention.js +1 -1
- package/dist/src/cli/movector/vector-db.js +1 -1
- package/dist/src/cli/parser.js +81 -12
- package/dist/src/cli/services/daemon-lock.js +5 -2
- package/dist/src/cli/services/hook-block-hash.js +3 -1
- package/dist/src/cli/services/moflo-version.js +62 -0
- package/dist/src/cli/services/movector-training.js +1 -1
- package/dist/src/cli/services/spell-gate.js +6 -4
- package/dist/src/cli/services/worker-daemon.js +37 -7
- package/dist/src/cli/shared/utils/load-average.js +59 -0
- package/dist/src/cli/spells/commands/agent-command.js +55 -25
- package/dist/src/cli/suggest.js +1 -1
- package/dist/src/cli/swarm/swarm-persistence.js +112 -0
- package/dist/src/cli/swarm/unified-coordinator.js +118 -3
- package/dist/src/cli/version.js +1 -1
- package/package.json +5 -3
- package/dist/src/cli/mcp-tools/github-tools.js +0 -318
package/dist/src/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* V3 CLI Main Entry Point
|
|
3
3
|
* MoFlo V4 CLI
|
|
4
4
|
*
|
|
5
|
-
* Created with ❤️ by
|
|
5
|
+
* Created with ❤️ by cielolimitada.com
|
|
6
6
|
*/
|
|
7
7
|
import { commandParser } from './parser.js';
|
|
8
8
|
import { output } from './output.js';
|
|
@@ -105,8 +105,45 @@ export class CLI {
|
|
|
105
105
|
// Parse arguments
|
|
106
106
|
let parseResult = this.parser.parse(args);
|
|
107
107
|
let { command: commandPath, flags, positional } = parseResult;
|
|
108
|
-
// Handle
|
|
109
|
-
|
|
108
|
+
// Handle lazy-loaded commands that weren't recognized by the parser.
|
|
109
|
+
// If commandPath is empty but positional has a command name, check if
|
|
110
|
+
// it's lazy-loadable.
|
|
111
|
+
if (commandPath.length === 0 && positional.length > 0 && !positional[0].startsWith('-')) {
|
|
112
|
+
const potentialCommand = positional[0];
|
|
113
|
+
if (hasCommand(potentialCommand)) {
|
|
114
|
+
// This is a lazy-loaded command, treat it as the command
|
|
115
|
+
commandPath.push(potentialCommand);
|
|
116
|
+
positional.shift();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Resolve the command BEFORE the global-flag handling below, so those
|
|
120
|
+
// checks see flags parsed against the command's own option tree.
|
|
121
|
+
// The first parse happens without knowledge of a lazily-loaded command's
|
|
122
|
+
// options, so `plugins install pkg --version 1.2.3` parsed `--version`
|
|
123
|
+
// under the GLOBAL boolean rule, came out as `version: true`, and hit the
|
|
124
|
+
// version handler below — printing "flo v4.12.4-rc.1" and returning
|
|
125
|
+
// without installing anything. Re-parsing here makes flags authoritative.
|
|
126
|
+
let command = commandPath[0]
|
|
127
|
+
? this.parser.getCommand(commandPath[0]) || getCommand(commandPath[0])
|
|
128
|
+
: undefined;
|
|
129
|
+
if (!command && commandPath[0] && hasCommand(commandPath[0])) {
|
|
130
|
+
command = await getCommandAsync(commandPath[0]);
|
|
131
|
+
if (command) {
|
|
132
|
+
// Registering the newly-loaded command also restores scoped
|
|
133
|
+
// short-flag aliases for its nested subcommand options, which the
|
|
134
|
+
// first parse could not apply.
|
|
135
|
+
this.parser.registerCommand(command);
|
|
136
|
+
parseResult = this.parser.parse(args);
|
|
137
|
+
({ command: commandPath, flags, positional } = parseResult);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Strict `=== true`: the global `--version` is a boolean, so a genuine
|
|
141
|
+
// global usage always yields exactly `true`. Commands that redefine
|
|
142
|
+
// `--version` as a value-taking option (plugins install/upgrade,
|
|
143
|
+
// deployment deploy/rollback) yield a string — or '' when the value is
|
|
144
|
+
// omitted — and those must NOT be hijacked into printing the moflo
|
|
145
|
+
// version and returning.
|
|
146
|
+
if (flags.version === true || flags.V === true) {
|
|
110
147
|
this.showVersion();
|
|
111
148
|
return;
|
|
112
149
|
}
|
|
@@ -136,16 +173,6 @@ export class CLI {
|
|
|
136
173
|
if (commandPath[0] !== 'daemon') {
|
|
137
174
|
this.maybeAutoStartDaemon().catch(() => { });
|
|
138
175
|
}
|
|
139
|
-
// Handle lazy-loaded commands that weren't recognized by the parser
|
|
140
|
-
// If commandPath is empty but positional has a command name, check if it's lazy-loadable
|
|
141
|
-
if (commandPath.length === 0 && positional.length > 0 && !positional[0].startsWith('-')) {
|
|
142
|
-
const potentialCommand = positional[0];
|
|
143
|
-
if (hasCommand(potentialCommand)) {
|
|
144
|
-
// This is a lazy-loaded command, treat it as the command
|
|
145
|
-
commandPath.push(potentialCommand);
|
|
146
|
-
positional.shift();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
176
|
// No command - show help or suggest correction
|
|
150
177
|
if (commandPath.length === 0 || flags.help || flags.h) {
|
|
151
178
|
if (commandPath.length > 0) {
|
|
@@ -166,26 +193,9 @@ export class CLI {
|
|
|
166
193
|
}
|
|
167
194
|
return;
|
|
168
195
|
}
|
|
169
|
-
// Find and execute command
|
|
196
|
+
// Find and execute command. Resolution + any lazy load already happened
|
|
197
|
+
// above, before global-flag handling — see the note there.
|
|
170
198
|
const commandName = commandPath[0];
|
|
171
|
-
// First check the parser's registry (for dynamically registered commands)
|
|
172
|
-
// Then fall back to the static registry, then try lazy loading
|
|
173
|
-
let command = this.parser.getCommand(commandName) || getCommand(commandName);
|
|
174
|
-
// If not found in sync registry, try lazy loading
|
|
175
|
-
if (!command && hasCommand(commandName)) {
|
|
176
|
-
command = await getCommandAsync(commandName);
|
|
177
|
-
if (command) {
|
|
178
|
-
// The first parse happened without knowledge of this command's
|
|
179
|
-
// subcommand tree, so scoped short-flag aliases for any nested
|
|
180
|
-
// subcommand options weren't applied — and a globally-registered
|
|
181
|
-
// command's `-x` would have hijacked the value. Register the
|
|
182
|
-
// newly-loaded command and re-parse so flags reflect the leaf
|
|
183
|
-
// command's options.
|
|
184
|
-
this.parser.registerCommand(command);
|
|
185
|
-
parseResult = this.parser.parse(args);
|
|
186
|
-
({ command: commandPath, flags, positional } = parseResult);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
199
|
if (!command) {
|
|
190
200
|
this.output.printError(`Unknown command: ${commandName}`);
|
|
191
201
|
// Smart suggestions - include lazy-loadable commands in suggestions
|
|
@@ -378,7 +388,7 @@ export class CLI {
|
|
|
378
388
|
this.output.writeln();
|
|
379
389
|
this.output.writeln(this.output.dim(`Run "${this.name} <command> --help" for command help`));
|
|
380
390
|
this.output.writeln();
|
|
381
|
-
this.output.writeln(this.output.dim('Created with ❤️ by
|
|
391
|
+
this.output.writeln(this.output.dim('Created with ❤️ by cielolimitada.com'));
|
|
382
392
|
this.output.writeln();
|
|
383
393
|
}
|
|
384
394
|
/**
|
|
@@ -35,10 +35,10 @@ Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/G
|
|
|
35
35
|
|
|
36
36
|
Search results carry a compact \`navigation\` crumb (parentDoc, prev/next, chunkTitle). For adjacent/sibling/hierarchical context use \`mcp__moflo__memory_get_neighbors\`; for full chunk content use \`mcp__moflo__memory_retrieve\`; \`Read\` the source doc only via \`parentPath\` when truly needed. Full protocol + worked examples per namespace: \`.claude/guidance/moflo-memory-protocol.md\`.
|
|
37
37
|
|
|
38
|
-
###
|
|
38
|
+
### Gates
|
|
39
39
|
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
40
|
+
- **Blocking** (hook exits non-zero): memory search before Glob/Grep/guidance Read; tests + \`/flo-simplify\` + learnings + \`/verify\` before \`gh pr create\`; \`swarm_init\`/\`hive-mind_init\` before Agent under \`/fl -s|-h\`.
|
|
41
|
+
- **Advisory** (reminder only): \`TaskCreate\` before spawning the Agent tool, entries in ICON+[Role] format — see \`.claude/guidance/moflo-task-icons.md\`.
|
|
42
42
|
|
|
43
43
|
### Tools
|
|
44
44
|
|
|
@@ -325,6 +325,9 @@ var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-sta
|
|
|
325
325
|
// bin/gate.cjs GATE_ORIGIN_NOTE / GATE_DISABLE_NOTE.
|
|
326
326
|
var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule — allow-rules cannot override it.';
|
|
327
327
|
var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
|
|
328
|
+
// #1348 — the pre-PR gates are order-dependent; naming only the missing one left
|
|
329
|
+
// callers to rediscover the sequence by trial. SYNC: mirrors bin/gate.cjs.
|
|
330
|
+
var ORDER_HINT = 'Order that satisfies all of them: tests green -> /flo-simplify (re-run tests if it edits) -> /verify -> its memory_store verdict -> gh pr create\\n';
|
|
328
331
|
// #1294 Finding 3 — exempt ephemeral reads/scans under the OS temp dir
|
|
329
332
|
// (background-task output, scratchpads) from the memory-first gate. Mirrors
|
|
330
333
|
// bin/gate.cjs isEphemeralPath. Cross-platform via os.tmpdir(); normalizes a
|
|
@@ -546,9 +549,58 @@ function applyPromptStateReset(state, promptText) {
|
|
|
546
549
|
state.activeSddSlug = null;
|
|
547
550
|
}
|
|
548
551
|
var TEST_RUNNER_RE = /(?:^|[^a-z])(?:npm|yarn|pnpm|bun)\\s+(?:run\\s+)?(?:test|t)(?:[:\\s]|$)|\\b(?:npx|pnpx)\\s+(?:vitest|jest|mocha|ava|tap|jasmine|pytest)\\b|(?:^|;|&&|\\|\\|)\\s*(?:vitest|jest|pytest|mocha|jasmine|tap|ava)\\s|\\b(?:cargo|go|deno|dotnet|mvn)\\s+test\\b|\\bgradle\\w*\\s+test\\b/i;
|
|
552
|
+
// #1322 — failure markers in a test runner's own OUTPUT.
|
|
553
|
+
//
|
|
554
|
+
// This is deliberately not an exit-code check: Claude Code's PostToolUse payload
|
|
555
|
+
// carries no exit status, and PostToolUse does not fire at all when a command
|
|
556
|
+
// exits non-zero — so an unmasked red suite already leaves testsRun false, by
|
|
557
|
+
// accident of the hook lifecycle rather than by design. What DOES defeat the
|
|
558
|
+
// gate is a masked exit (\`npm test | tail -20\`, \`npm test || true\`,
|
|
559
|
+
// \`npm test 2>&1 | grep -i fail\`): the pipeline exits 0, PostToolUse fires with
|
|
560
|
+
// a clean-looking response, and a red suite credits the gate. Output is the only
|
|
561
|
+
// signal left, and it is genuinely weaker than a status — see the ticket.
|
|
562
|
+
//
|
|
563
|
+
// Every arm matches a SUMMARY shape a runner emits, never a bare "fail", which
|
|
564
|
+
// occurs constantly in ordinary passing test names ("returns null when the
|
|
565
|
+
// lookup failed"). The count arm excludes an explicit zero so jest's
|
|
566
|
+
// \`0 failed, 12 passed\` cannot self-block.
|
|
567
|
+
//
|
|
568
|
+
// The count arm's trailing lookahead is what keeps a GREEN run from blocking
|
|
569
|
+
// itself. Mocha's default spec reporter prints every passing test name, so
|
|
570
|
+
// \`npm test | tail -20\` on a green suite legitimately contains lines like
|
|
571
|
+
// \`✓ handles 2 failed retries\`. A real summary is followed by a delimiter or a
|
|
572
|
+
// line end (\`3 failed | 40 passed\`, \`1 failed, 2 passed\`, \`1 failing\`), never by
|
|
573
|
+
// more prose — so a lowercase word after the count means it is a sentence, not a
|
|
574
|
+
// tally. \`tests\`/\`test\` is exempted because \`2 failed tests\` is a real summary.
|
|
575
|
+
// Same-line whitespace only: at a line end there is nothing to disqualify.
|
|
576
|
+
var TEST_FAILURE_RE = new RegExp([
|
|
577
|
+
'\\\\b(?!0\\\\b)\\\\d+\\\\s+(?:tests?\\\\s+)?(?:failed|failing|failures?)\\\\b(?![^\\\\S\\\\n]+(?!tests?\\\\b)[a-z])', // vitest/jest/pytest/mocha counts
|
|
578
|
+
'^\\\\s*(?:FAIL|FAILED)\\\\b', // vitest + jest per-file, pytest FAILED
|
|
579
|
+
'^\\\\s*---\\\\s*FAIL:', // go test
|
|
580
|
+
'\\\\btest result:\\\\s*FAILED\\\\b', // cargo
|
|
581
|
+
'^npm ERR!', // npm wrapper around any of the above
|
|
582
|
+
].join('|'), 'im');
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* #1322 — why a just-fired record-test-run must NOT be credited, or null.
|
|
586
|
+
*
|
|
587
|
+
* Absent output is not evidence of failure: a quiet green \`npm test > /dev/null\`
|
|
588
|
+
* and a silently-masked red one are indistinguishable, and treating the pair as
|
|
589
|
+
* failures would block every consumer who redirects test output. Absent means
|
|
590
|
+
* unknown, and unknown keeps the pre-#1322 behaviour.
|
|
591
|
+
*/
|
|
592
|
+
function detectTestFailure() {
|
|
593
|
+
if (process.env.TOOL_RESPONSE_interrupted === 'true') return 'the run was interrupted';
|
|
594
|
+
var out = (process.env.TOOL_RESPONSE_stdout || '') + '\\n' + (process.env.TOOL_RESPONSE_stderr || '');
|
|
595
|
+
if (!out.trim()) return null;
|
|
596
|
+
var hit = out.match(TEST_FAILURE_RE);
|
|
597
|
+
return hit ? 'output reports "' + hit[0].trim().slice(0, 40) + '"' : null;
|
|
598
|
+
}
|
|
549
599
|
var EDIT_RESET_SKIP_BOTH_RE = /\\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\\\\/])(CHANGELOG(?:\\.md)?|\\.env\\.example|package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock|bun\\.lockb)$/i;
|
|
550
600
|
// #1297 — path-inert dirs (.github/workflows etc.); SYNC: mirrors bin/gate.cjs EDIT_RESET_SKIP_PATH_RE.
|
|
551
|
-
|
|
601
|
+
// #1348 — plus \`.moflo/\`, moflo's own gitignored state dir: nothing written
|
|
602
|
+
// there can reach the branch diff, so it must not invalidate a gate.
|
|
603
|
+
var EDIT_RESET_SKIP_PATH_RE = /(?:^|[\\\\\\/])\\.github[\\\\\\/](?:workflows|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)(?:[\\\\\\/.]|$)|(?:^|[\\\\\\/])\\.moflo[\\\\\\/]/i;
|
|
552
604
|
// Test files: invalidate testsRun but preserve simplifyRun (#908) — /simplify
|
|
553
605
|
// already reviewed the production code, touching tests/fixtures doesn't expose
|
|
554
606
|
// new untested surface for code review.
|
|
@@ -556,14 +608,17 @@ var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\\\\/])(__tests__|__mocks__|test
|
|
|
556
608
|
|
|
557
609
|
switch (command) {
|
|
558
610
|
case 'check-before-agent': {
|
|
559
|
-
//
|
|
560
|
-
//
|
|
611
|
+
// Mostly advisory. The TaskCreate + memory reminders below go to stdout and
|
|
612
|
+
// never block — their wording must not claim otherwise (#1326). The one
|
|
613
|
+
// exception is the #952 swarm/hive check at the bottom of this case, which
|
|
614
|
+
// writes to stderr and exits 2.
|
|
615
|
+
// Memory-first enforcement otherwise happens at the scan/read gate layer.
|
|
561
616
|
// SubagentStart hook injects guidance directive into subagent context.
|
|
562
617
|
// #931 — TaskCreate REMINDER + namespace hint moved here from
|
|
563
618
|
// prompt-reminder so they emit only when Claude is about to spawn an Agent.
|
|
564
619
|
var s = readState();
|
|
565
620
|
if (config.task_create_first && !s.tasksCreated) {
|
|
566
|
-
process.stdout.write('REMINDER: Use TaskCreate before spawning agents
|
|
621
|
+
process.stdout.write('REMINDER: Use TaskCreate before spawning agents.\\n');
|
|
567
622
|
}
|
|
568
623
|
if (config.memory_first && s.memoryRequired && !s.memorySearched) {
|
|
569
624
|
process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents.\\n');
|
|
@@ -703,8 +758,14 @@ switch (command) {
|
|
|
703
758
|
case 'record-test-run': {
|
|
704
759
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
705
760
|
if (TEST_RUNNER_RE.test(cmd)) {
|
|
761
|
+
// #1322 — a red run is evidence AGAINST the gate, so it also clears a
|
|
762
|
+
// flag an earlier green run earned.
|
|
763
|
+
var failure = detectTestFailure();
|
|
706
764
|
var s = readState();
|
|
707
|
-
if (
|
|
765
|
+
if (failure) {
|
|
766
|
+
if (s.testsRun) { s.testsRun = false; writeState(s); }
|
|
767
|
+
process.stderr.write('gate: record-test-run not credited — ' + failure + '\\n');
|
|
768
|
+
} else if (!s.testsRun) {
|
|
708
769
|
s.testsRun = true;
|
|
709
770
|
writeState(s);
|
|
710
771
|
}
|
|
@@ -730,25 +791,58 @@ switch (command) {
|
|
|
730
791
|
// end-to-end verification (see fl/sdd.md).
|
|
731
792
|
if (vName === 'verify') {
|
|
732
793
|
var s = readState();
|
|
733
|
-
|
|
794
|
+
// #1332: starting a verification clears any prior verdict, so the run
|
|
795
|
+
// in progress cannot inherit a previous issue's PASS.
|
|
796
|
+
if (!s.verifyRun || s.verifyOutcome) {
|
|
797
|
+
s.verifyRun = true;
|
|
798
|
+
s.verifyOutcome = null;
|
|
799
|
+
writeState(s);
|
|
800
|
+
}
|
|
734
801
|
}
|
|
735
802
|
break;
|
|
736
803
|
}
|
|
804
|
+
case 'record-verify-outcome': {
|
|
805
|
+
// #1332 — record HOW the verification ended, from the structured record
|
|
806
|
+
// #1328 has /verify write to memory_store's \`metadata\`. Never parsed out
|
|
807
|
+
// of the prose \`value\`; gate-hook.mjs forwards the object as JSON.
|
|
808
|
+
var mKey = process.env.TOOL_INPUT_key || '';
|
|
809
|
+
if (mKey.indexOf('verify:') !== 0) break;
|
|
810
|
+
var rawMeta = process.env.TOOL_INPUT_metadata || '';
|
|
811
|
+
if (!rawMeta) break;
|
|
812
|
+
var parsedMeta = null;
|
|
813
|
+
try { parsedMeta = JSON.parse(rawMeta); } catch (e) { parsedMeta = null; }
|
|
814
|
+
if (!parsedMeta || typeof parsedMeta !== 'object' || parsedMeta.type !== 'verify-record') break;
|
|
815
|
+
var overall = typeof parsedMeta.overall === 'string' ? parsedMeta.overall.toUpperCase() : '';
|
|
816
|
+
if (overall !== 'PASS' && overall !== 'FAIL' && overall !== 'UNVERIFIED') overall = 'UNVERIFIED';
|
|
817
|
+
var vs = readState();
|
|
818
|
+
// #1348 — a verdict that arrives after a code edit cleared verifyRun
|
|
819
|
+
// describes pre-edit code; recording it leaves state self-contradictory.
|
|
820
|
+
if (!vs.verifyRun) break;
|
|
821
|
+
vs.verifyOutcome = overall;
|
|
822
|
+
writeState(vs);
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
737
825
|
case 'reset-edit-gates': {
|
|
738
826
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
739
|
-
// Inert files (markdown, lockfiles, CHANGELOG, .env.example)
|
|
740
|
-
|
|
827
|
+
// Inert files (markdown, lockfiles, CHANGELOG, .env.example) and inert paths
|
|
828
|
+
// (.github meta dirs, .moflo state): no gate reset.
|
|
829
|
+
if (fp && (EDIT_RESET_SKIP_BOTH_RE.test(fp) || EDIT_RESET_SKIP_PATH_RE.test(fp))) break;
|
|
830
|
+
// #1348 — a scratchpad write under the OS temp dir is transient tool I/O,
|
|
831
|
+
// never a code edit, so it must not reset tests/simplify/verify. SYNC:
|
|
832
|
+
// mirrors bin/gate.cjs, which carries the full rationale.
|
|
833
|
+
if (isEphemeralPath(fp)) break;
|
|
741
834
|
var s = readState();
|
|
742
835
|
// Test-only edits invalidate testsRun but preserve simplifyRun (#908).
|
|
743
836
|
var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
|
|
744
837
|
var resetTests = s.testsRun;
|
|
745
838
|
// A code edit invalidates a prior verification (Story #1274), like tests.
|
|
746
|
-
|
|
839
|
+
// #1332: also fires on a lingering verdict, so no stale PASS survives.
|
|
840
|
+
var resetVerify = s.verifyRun || !!s.verifyOutcome;
|
|
747
841
|
var resetSimplify = s.simplifyRun && !isTestOnly;
|
|
748
842
|
if (!resetTests && !resetSimplify && !resetVerify) break;
|
|
749
843
|
var gates = [];
|
|
750
844
|
if (resetTests) { s.testsRun = false; gates.push('tests'); }
|
|
751
|
-
if (resetVerify) { s.verifyRun = false; gates.push('verify'); }
|
|
845
|
+
if (resetVerify) { s.verifyRun = false; s.verifyOutcome = null; gates.push('verify'); }
|
|
752
846
|
if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
|
|
753
847
|
if (fp) {
|
|
754
848
|
s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
|
|
@@ -789,7 +883,7 @@ switch (command) {
|
|
|
789
883
|
if (!/(?:^|&&\\s*|\\|\\|\\s*|;\\s*)\\s*(?:[A-Z_][A-Z0-9_]*=\\S+\\s+)*gh\\s+pr\\s+create\\b/.test(cmd)) break;
|
|
790
884
|
var s = readState();
|
|
791
885
|
var missing = [];
|
|
792
|
-
if (config.testing_gate && !s.testsRun) missing.push('tests have not run since the last code edit (run npm test, vitest, jest, pytest, or similar)');
|
|
886
|
+
if (config.testing_gate && !s.testsRun) missing.push('tests have not run green since the last code edit (run npm test, vitest, jest, pytest, or similar — a run whose output reports failures does not count, #1322)');
|
|
793
887
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
|
|
794
888
|
if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
|
|
795
889
|
if (missing.length === 0) break;
|
|
@@ -800,6 +894,7 @@ switch (command) {
|
|
|
800
894
|
if (s.lastResetBy && s.lastResetBy.file) {
|
|
801
895
|
process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\\n');
|
|
802
896
|
}
|
|
897
|
+
process.stderr.write(ORDER_HINT);
|
|
803
898
|
process.stderr.write('Disable per-gate via moflo.yaml:\\n');
|
|
804
899
|
process.stderr.write(' gates:\\n testing_gate: false\\n simplify_gate: false\\n learnings_gate: false\\n');
|
|
805
900
|
process.exit(2);
|
|
@@ -815,9 +910,21 @@ switch (command) {
|
|
|
815
910
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
816
911
|
if (!/(?:^|&&\\s*|\\|\\|\\s*|;\\s*)\\s*(?:[A-Z_][A-Z0-9_]*=\\S+\\s+)*gh\\s+pr\\s+create\\b/.test(cmd)) break;
|
|
817
912
|
var s = readState();
|
|
818
|
-
|
|
913
|
+
// #1332 — gate on the OUTCOME, not on attendance: a /verify returning FAIL
|
|
914
|
+
// is still a successful tool invocation, so verifyRun alone let it through.
|
|
915
|
+
if (s.verifyRun && s.verifyOutcome === 'PASS') break;
|
|
819
916
|
process.stderr.write('BLOCKED: gh pr create requires verification before done:\\n');
|
|
820
|
-
|
|
917
|
+
if (!s.verifyRun) {
|
|
918
|
+
process.stderr.write(' - the change has not been verified since the last code edit (run /verify)\\n');
|
|
919
|
+
} else if (s.verifyOutcome === 'FAIL' || s.verifyOutcome === 'UNVERIFIED') {
|
|
920
|
+
process.stderr.write(' - /verify ran and returned ' + s.verifyOutcome + ' — fix the failing criteria, then re-run /verify\\n');
|
|
921
|
+
} else {
|
|
922
|
+
process.stderr.write(' - /verify ran but recorded no verdict — re-run it so it stores a structured result\\n');
|
|
923
|
+
// #1348 — re-invoking /verify clears the prior verdict by design (#1332),
|
|
924
|
+
// so the obvious recovery lands back here unless Step 5 completes.
|
|
925
|
+
process.stderr.write(' Re-invoking /verify clears the prior verdict, so a re-run that skips Step 5 lands here again.\\n');
|
|
926
|
+
}
|
|
927
|
+
process.stderr.write(ORDER_HINT);
|
|
821
928
|
process.stderr.write('Disable via moflo.yaml:\\n');
|
|
822
929
|
process.stderr.write(' gates:\\n verify_before_done: false\\n');
|
|
823
930
|
process.exit(2);
|
|
@@ -906,15 +1013,24 @@ switch (command) {
|
|
|
906
1013
|
}
|
|
907
1014
|
/**
|
|
908
1015
|
* Generate gate-hook.mjs — ESM wrapper that reads Claude Code stdin JSON
|
|
909
|
-
* and passes tool_name + tool_input to gate.cjs via
|
|
1016
|
+
* and passes tool_name + tool_input + tool_response to gate.cjs via env vars.
|
|
910
1017
|
*
|
|
911
1018
|
* Claude Code hooks receive context as JSON on stdin but don't set env vars
|
|
912
1019
|
* for tool input. This script bridges that gap. It also translates exit code 1
|
|
913
1020
|
* from gate.cjs into exit code 2 (which Claude Code requires to block tools).
|
|
1021
|
+
*
|
|
1022
|
+
* **This must stay byte-identical to `bin/gate-hook.mjs`** — the launcher syncs
|
|
1023
|
+
* that file into the same `.claude/helpers/gate-hook.mjs` this generator writes,
|
|
1024
|
+
* so any divergence means `flo init` emits one bridge and the next session start
|
|
1025
|
+
* silently swaps in another. It had drifted exactly that way before #1322: the
|
|
1026
|
+
* generated copy never received #1332's structured-input forwarding and still
|
|
1027
|
+
* shelled out via `execSync` string concatenation. Parity is pinned by
|
|
1028
|
+
* `tests/guards/gate-hook-parity-guard.test.ts` — when you change one, copy the
|
|
1029
|
+
* whole file across; do not hand-merge.
|
|
914
1030
|
*/
|
|
915
1031
|
export function generateGateHookScript() {
|
|
916
1032
|
return `#!/usr/bin/env node
|
|
917
|
-
import {
|
|
1033
|
+
import { execFileSync } from 'child_process';
|
|
918
1034
|
import { resolve } from 'path';
|
|
919
1035
|
|
|
920
1036
|
var command = process.argv[2];
|
|
@@ -941,24 +1057,91 @@ try { if (stdinData.trim()) hookContext = JSON.parse(stdinData); } catch (e) {}
|
|
|
941
1057
|
var env = Object.assign({}, process.env);
|
|
942
1058
|
if (hookContext.tool_name) env.TOOL_NAME = hookContext.tool_name;
|
|
943
1059
|
// Forward Claude Code's session_id so gate.cjs can enforce memory-first
|
|
944
|
-
// per-actor (#838) — each spawned subagent
|
|
1060
|
+
// per-actor (#838) — each spawned subagent gets its own session_id, so a
|
|
1061
|
+
// shared workflow-state.json no longer lets one subagent's directive be
|
|
1062
|
+
// silently satisfied by the parent's earlier search.
|
|
945
1063
|
if (typeof hookContext.session_id === 'string' && hookContext.session_id) {
|
|
946
1064
|
env.HOOK_SESSION_ID = hookContext.session_id;
|
|
947
1065
|
}
|
|
1066
|
+
// #1332: structured tool inputs are forwarded as JSON, not dropped.
|
|
1067
|
+
//
|
|
1068
|
+
// This previously forwarded ONLY string values, so any object-valued input was
|
|
1069
|
+
// invisible to gate.cjs. That blocked the verify-before-done gate from reading
|
|
1070
|
+
// \`/verify\`'s per-criterion verdict, which #1328 stores in memory_store's
|
|
1071
|
+
// \`metadata\` — an object. Parsing the verdict out of the prose \`value\` string
|
|
1072
|
+
// instead would re-create exactly the free-text dependency #1328 removed.
|
|
1073
|
+
//
|
|
1074
|
+
// Cross-platform (Rule #1): Windows caps a single environment variable at
|
|
1075
|
+
// ~32KB and the whole block at ~32K wide chars, and exceeding it fails the
|
|
1076
|
+
// spawn rather than truncating. Newly-forwarded values are therefore skipped
|
|
1077
|
+
// when oversized, not clipped — a truncated JSON blob would parse as malformed
|
|
1078
|
+
// on the far side and read as a corrupt record rather than an absent one.
|
|
1079
|
+
// \`metadata\` is capped at 64KB by memory_store, so a real verdict never nears
|
|
1080
|
+
// this. STRING values keep their previous uncapped behaviour byte-for-byte:
|
|
1081
|
+
// gate.cjs reads TOOL_INPUT_command, and dropping an oversized heredoc command
|
|
1082
|
+
// would silently stop check-dangerous-command from firing on the exact inputs
|
|
1083
|
+
// most worth checking.
|
|
1084
|
+
var MAX_STRUCTURED_LEN = 16384;
|
|
948
1085
|
if (hookContext.tool_input && typeof hookContext.tool_input === 'object') {
|
|
949
1086
|
Object.keys(hookContext.tool_input).forEach(function(key) {
|
|
950
|
-
|
|
951
|
-
|
|
1087
|
+
var raw = hookContext.tool_input[key];
|
|
1088
|
+
if (typeof raw === 'string') {
|
|
1089
|
+
env['TOOL_INPUT_' + key] = raw;
|
|
1090
|
+
return;
|
|
952
1091
|
}
|
|
1092
|
+
var val;
|
|
1093
|
+
if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
1094
|
+
val = String(raw);
|
|
1095
|
+
} else if (raw && typeof raw === 'object') {
|
|
1096
|
+
try { val = JSON.stringify(raw); } catch (e) { return; }
|
|
1097
|
+
} else {
|
|
1098
|
+
return; // null/undefined/function — nothing meaningful to forward
|
|
1099
|
+
}
|
|
1100
|
+
if (val.length > MAX_STRUCTURED_LEN) return;
|
|
1101
|
+
env['TOOL_INPUT_' + key] = val;
|
|
953
1102
|
});
|
|
954
1103
|
}
|
|
955
1104
|
|
|
1105
|
+
// #1322: forward the parts of tool_response that actually exist, so a gate can
|
|
1106
|
+
// observe an OUTCOME rather than only the intent it was handed.
|
|
1107
|
+
//
|
|
1108
|
+
// Claude Code's PostToolUse payload carries NO exit status — probed on v2.1.220,
|
|
1109
|
+
// tool_response for a Bash call is {stdout, stderr, interrupted, isImage,
|
|
1110
|
+
// noOutputExpected}. PostToolUse also does not fire at all when the command
|
|
1111
|
+
// exits non-zero, so the only case a gate can still be fooled by is an exit code
|
|
1112
|
+
// MASKED by a pipe or \`|| true\`, where the response looks clean. The runner's
|
|
1113
|
+
// own output is the sole remaining signal; record-test-run reads it in gate.cjs.
|
|
1114
|
+
//
|
|
1115
|
+
// Tail, not head. Every test runner prints its pass/fail summary LAST, so
|
|
1116
|
+
// clipping the front of a long log would discard the exact lines this exists to
|
|
1117
|
+
// read. Bounds are deliberately tight — Windows caps the whole environment
|
|
1118
|
+
// block at ~32K wide chars and fails the spawn rather than truncating, and
|
|
1119
|
+
// TOOL_INPUT_command is already forwarded uncapped alongside these.
|
|
1120
|
+
var MAX_RESPONSE_STDOUT = 4096;
|
|
1121
|
+
var MAX_RESPONSE_STDERR = 2048;
|
|
1122
|
+
function tailOf(value, max) {
|
|
1123
|
+
return value.length > max ? value.slice(value.length - max) : value;
|
|
1124
|
+
}
|
|
1125
|
+
if (hookContext.tool_response && typeof hookContext.tool_response === 'object') {
|
|
1126
|
+
var resp = hookContext.tool_response;
|
|
1127
|
+
if (typeof resp.stdout === 'string' && resp.stdout) {
|
|
1128
|
+
env.TOOL_RESPONSE_stdout = tailOf(resp.stdout, MAX_RESPONSE_STDOUT);
|
|
1129
|
+
}
|
|
1130
|
+
if (typeof resp.stderr === 'string' && resp.stderr) {
|
|
1131
|
+
env.TOOL_RESPONSE_stderr = tailOf(resp.stderr, MAX_RESPONSE_STDERR);
|
|
1132
|
+
}
|
|
1133
|
+
// Boolean — the string-typed forwarding above would drop it silently.
|
|
1134
|
+
if (typeof resp.interrupted === 'boolean') {
|
|
1135
|
+
env.TOOL_RESPONSE_interrupted = String(resp.interrupted);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
956
1139
|
// Run gate.cjs with the enriched environment
|
|
957
1140
|
var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\\/([a-z])\\//i, '$1:/');
|
|
958
1141
|
var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
|
|
959
1142
|
try {
|
|
960
|
-
var output =
|
|
961
|
-
env: env, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
1143
|
+
var output = execFileSync('node', [gateScript, command], {
|
|
1144
|
+
env: env, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true
|
|
962
1145
|
});
|
|
963
1146
|
if (output.trim()) process.stdout.write(output);
|
|
964
1147
|
process.exit(0);
|
|
@@ -22,8 +22,14 @@ const WALK_SKIP_DIRS = new Set([
|
|
|
22
22
|
'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.reports',
|
|
23
23
|
'.swarm', '.moflo', 'packages',
|
|
24
24
|
]);
|
|
25
|
+
// `.mjs`/`.cjs` are first-class JS sources, not build artifacts — moflo's own
|
|
26
|
+
// hook/gate/launcher runtime is written in them. Omitting them here made every
|
|
27
|
+
// consumer's generated `code_map.extensions` exclude them, so an explicit list
|
|
28
|
+
// from `flo init` shadowed the indexer defaults and left that code unsearchable
|
|
29
|
+
// (#1337).
|
|
25
30
|
const SOURCE_EXTENSIONS = [
|
|
26
|
-
'.ts', '.tsx', '.js', '.jsx', '.
|
|
31
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
32
|
+
'.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.cs',
|
|
27
33
|
];
|
|
28
34
|
/**
|
|
29
35
|
* Top-level candidates for source directories. Same list `flo init` uses; the
|
|
@@ -127,7 +133,9 @@ function scanExtensions(dir, extensions, depth, maxDepth) {
|
|
|
127
133
|
scanExtensions(path.join(dir, entry.name), extensions, depth + 1, maxDepth);
|
|
128
134
|
}
|
|
129
135
|
else if (entry.isFile()) {
|
|
130
|
-
|
|
136
|
+
// Lowercase before matching: NTFS/APFS are case-insensitive, so a file
|
|
137
|
+
// committed as `Foo.MJS` yields `.MJS` and would miss an exact compare.
|
|
138
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
131
139
|
if (SOURCE_EXTENSIONS.includes(ext))
|
|
132
140
|
extensions.add(ext);
|
|
133
141
|
}
|
|
@@ -144,7 +152,7 @@ export function detectExtensions(root, srcDirs) {
|
|
|
144
152
|
catch { /* skip */ }
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
|
-
return extensions.size > 0 ? [...extensions].sort() : ['.ts', '.tsx', '.js', '.jsx'];
|
|
155
|
+
return extensions.size > 0 ? [...extensions].sort() : ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
148
156
|
}
|
|
149
157
|
export function defaultMofloYamlConfig(root) {
|
|
150
158
|
const guidanceDirs = discoverGuidanceDirs(root);
|
|
@@ -39,7 +39,7 @@ export function generateSettings(options) {
|
|
|
39
39
|
};
|
|
40
40
|
// Add claude-flow attribution for git commits and PRs
|
|
41
41
|
settings.attribution = {
|
|
42
|
-
commit: 'Co-Authored-By: moflo <noreply@
|
|
42
|
+
commit: 'Co-Authored-By: moflo <noreply@cielolimitada.com>',
|
|
43
43
|
pr: '🤖 Generated with [moflo](https://github.com/eric-cielo/moflo)',
|
|
44
44
|
};
|
|
45
45
|
// Note: Claude Code expects 'model' to be a string, not an object
|
|
@@ -339,7 +339,15 @@ function generateHooksConfig(config) {
|
|
|
339
339
|
},
|
|
340
340
|
{
|
|
341
341
|
matcher: '^mcp__moflo__memory_store$',
|
|
342
|
-
hooks: [
|
|
342
|
+
hooks: [
|
|
343
|
+
{ type: 'command', command: gateCmd('record-learnings-stored'), timeout: 2000 },
|
|
344
|
+
// #1332 — capture /verify's structured verdict so check-before-done
|
|
345
|
+
// can gate on the outcome rather than on attendance. Must route via
|
|
346
|
+
// gateHookCmd, not gateCmd: the TOOL_INPUT_* env vars this reads are
|
|
347
|
+
// built by gate-hook.mjs from the hook's stdin payload, so a direct
|
|
348
|
+
// gate.cjs invocation would see no key and no metadata.
|
|
349
|
+
{ type: 'command', command: gateHookCmd('record-verify-outcome'), timeout: 2000 },
|
|
350
|
+
],
|
|
343
351
|
},
|
|
344
352
|
{
|
|
345
353
|
// #952 — when /fl is invoked with -s/--swarm, the gate blocks Agent
|
|
@@ -20,9 +20,16 @@ import { securityTools } from './mcp-tools/security-tools.js';
|
|
|
20
20
|
import { systemTools } from './mcp-tools/system-tools.js';
|
|
21
21
|
import { neuralTools } from './mcp-tools/neural-tools.js';
|
|
22
22
|
import { performanceTools } from './mcp-tools/performance-tools.js';
|
|
23
|
-
|
|
23
|
+
// #1353 — `github-tools.ts` is gone. Its four tools made no GitHub API call;
|
|
24
|
+
// they wrote local JSON and returned `Math.random()` counts, so a `merged: true`
|
|
25
|
+
// described nothing that happened. Use the `gh` CLI or a GitHub MCP server.
|
|
24
26
|
import { coordinationTools } from './mcp-tools/coordination-tools.js';
|
|
25
27
|
import { moflodbTools } from './mcp-tools/moflodb-tools.js';
|
|
28
|
+
// #1349 — handlers for tool names shipped CLI commands were already calling.
|
|
29
|
+
import { progressTools } from './mcp-tools/progress-tools.js';
|
|
30
|
+
import { coverageTools } from './mcp-tools/coverage-tools.js';
|
|
31
|
+
import { memoryAdminTools } from './mcp-tools/memory-admin-tools.js';
|
|
32
|
+
import { analysisTools } from './mcp-tools/analysis-tools.js';
|
|
26
33
|
import { errorDetail } from './shared/utils/error-detail.js';
|
|
27
34
|
/**
|
|
28
35
|
* MCP Tool Registry
|
|
@@ -49,9 +56,12 @@ registerTools([
|
|
|
49
56
|
...systemTools,
|
|
50
57
|
...neuralTools,
|
|
51
58
|
...performanceTools,
|
|
52
|
-
...githubTools,
|
|
53
59
|
...coordinationTools,
|
|
54
60
|
...moflodbTools,
|
|
61
|
+
...progressTools,
|
|
62
|
+
...coverageTools,
|
|
63
|
+
...memoryAdminTools,
|
|
64
|
+
...analysisTools,
|
|
55
65
|
]);
|
|
56
66
|
/**
|
|
57
67
|
* MCP Client Error
|
|
@@ -25,6 +25,7 @@ import { fileURLToPath } from 'url';
|
|
|
25
25
|
import { dirname } from 'path';
|
|
26
26
|
import { errorDetail } from './shared/utils/error-detail.js';
|
|
27
27
|
import { findProjectRoot } from './services/project-root.js';
|
|
28
|
+
import { getMofloVersion } from './services/moflo-version.js';
|
|
28
29
|
import { readMofloEnv } from './services/env-compat.js';
|
|
29
30
|
// ESM-compatible __dirname
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -269,28 +270,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
269
270
|
// Import the tool registry
|
|
270
271
|
const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
|
|
271
272
|
// Read version dynamically from root moflo package.json
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const { readFileSync } = await import('fs');
|
|
275
|
-
const { dirname: _d, join: _j } = await import('path');
|
|
276
|
-
const { fileURLToPath: _f } = await import('url');
|
|
277
|
-
let _dir = _d(_f(import.meta.url));
|
|
278
|
-
for (;;) {
|
|
279
|
-
try {
|
|
280
|
-
const _pkg = JSON.parse(readFileSync(_j(_dir, 'package.json'), 'utf8'));
|
|
281
|
-
if (_pkg.name === 'moflo' && _pkg.version) {
|
|
282
|
-
VERSION = _pkg.version;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch { }
|
|
287
|
-
const _p = _d(_dir);
|
|
288
|
-
if (_p === _dir)
|
|
289
|
-
break;
|
|
290
|
-
_dir = _p;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch { }
|
|
273
|
+
const VERSION = getMofloVersion();
|
|
294
274
|
const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
295
275
|
// Log to stderr to not corrupt stdout
|
|
296
276
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Starting in stdio mode`);
|
|
@@ -445,7 +425,9 @@ export class MCPServerManager extends EventEmitter {
|
|
|
445
425
|
id: message.id,
|
|
446
426
|
result: {
|
|
447
427
|
protocolVersion: '2024-11-05',
|
|
448
|
-
|
|
428
|
+
// The `initialize` reply is where a client learns the server
|
|
429
|
+
// version; this reported 3.0.0 from a 4.x package (#1325).
|
|
430
|
+
serverInfo: { name: 'moflo', version: getMofloVersion() },
|
|
449
431
|
capabilities: {
|
|
450
432
|
tools: { listChanged: true },
|
|
451
433
|
resources: { subscribe: true, listChanged: true },
|