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.
Files changed (98) hide show
  1. package/.claude/agents/core/coder.md +2 -3
  2. package/.claude/agents/core/researcher.md +3 -4
  3. package/.claude/agents/core/reviewer.md +6 -8
  4. package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
  5. package/.claude/helpers/gate-hook.mjs +67 -2
  6. package/.claude/helpers/gate.cjs +173 -12
  7. package/.claude/helpers/hook-handler.cjs +0 -0
  8. package/.claude/helpers/prompt-hook.mjs +0 -0
  9. package/.claude/skills/connector-builder/SKILL.md +5 -4
  10. package/.claude/skills/fl/phases.md +1 -1
  11. package/.claude/skills/fl/sdd.md +1 -1
  12. package/.claude/skills/spell-builder/SKILL.md +5 -4
  13. package/.claude/skills/spell-builder/architecture.md +4 -4
  14. package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
  15. package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
  16. package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
  17. package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
  18. package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
  19. package/.claude/skills/verify/SKILL.md +24 -3
  20. package/bin/gate-hook.mjs +67 -2
  21. package/bin/gate.cjs +173 -12
  22. package/bin/generate-code-map.mjs +70 -17
  23. package/bin/index-all.mjs +38 -0
  24. package/bin/index-guidance.mjs +8 -41
  25. package/bin/index-patterns.mjs +2 -2
  26. package/bin/index-tests.mjs +6 -4
  27. package/bin/lib/guidance-config.mjs +119 -0
  28. package/bin/lib/index-fingerprint.mjs +0 -0
  29. package/dist/src/cli/commands/agent.js +1 -89
  30. package/dist/src/cli/commands/analyze.js +1 -1
  31. package/dist/src/cli/commands/benchmark.js +11 -8
  32. package/dist/src/cli/commands/claims.js +2 -2
  33. package/dist/src/cli/commands/completions.js +1 -1
  34. package/dist/src/cli/commands/config.js +256 -96
  35. package/dist/src/cli/commands/deployment.js +6 -6
  36. package/dist/src/cli/commands/diagnose.js +1 -1
  37. package/dist/src/cli/commands/doctor-checks-config.js +18 -22
  38. package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
  39. package/dist/src/cli/commands/doctor-fixes.js +36 -11
  40. package/dist/src/cli/commands/doctor.js +1 -1
  41. package/dist/src/cli/commands/embeddings.js +64 -23
  42. package/dist/src/cli/commands/github.js +1 -1
  43. package/dist/src/cli/commands/guidance.js +10 -10
  44. package/dist/src/cli/commands/hive-mind.js +8 -70
  45. package/dist/src/cli/commands/hooks.js +32 -219
  46. package/dist/src/cli/commands/mcp.js +5 -2
  47. package/dist/src/cli/commands/memory.js +59 -46
  48. package/dist/src/cli/commands/neural.js +21 -21
  49. package/dist/src/cli/commands/performance.js +14 -7
  50. package/dist/src/cli/commands/plugins.js +2 -2
  51. package/dist/src/cli/commands/providers.js +2 -2
  52. package/dist/src/cli/commands/retire.js +1 -1
  53. package/dist/src/cli/commands/route.js +1 -1
  54. package/dist/src/cli/commands/security.js +4 -4
  55. package/dist/src/cli/commands/status.js +18 -13
  56. package/dist/src/cli/commands/task.js +8 -0
  57. package/dist/src/cli/config/cli-config-store.js +326 -0
  58. package/dist/src/cli/hooks/workers/index.js +15 -9
  59. package/dist/src/cli/index.js +43 -33
  60. package/dist/src/cli/init/claudemd-generator.js +3 -3
  61. package/dist/src/cli/init/helpers-generator.js +203 -20
  62. package/dist/src/cli/init/moflo-yaml-template.js +11 -3
  63. package/dist/src/cli/init/settings-generator.js +10 -2
  64. package/dist/src/cli/mcp-client.js +12 -2
  65. package/dist/src/cli/mcp-server.js +5 -23
  66. package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
  67. package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
  68. package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
  69. package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
  70. package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
  71. package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
  72. package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
  73. package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
  74. package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
  75. package/dist/src/cli/mcp-tools/security-tools.js +1 -1
  76. package/dist/src/cli/mcp-tools/session-tools.js +116 -0
  77. package/dist/src/cli/mcp-tools/synthetic.js +74 -0
  78. package/dist/src/cli/mcp-tools/system-tools.js +126 -78
  79. package/dist/src/cli/mcp-tools/task-tools.js +101 -1
  80. package/dist/src/cli/movector/coverage-router.js +48 -6
  81. package/dist/src/cli/movector/diff-classifier.js +8 -6
  82. package/dist/src/cli/movector/flash-attention.js +1 -1
  83. package/dist/src/cli/movector/vector-db.js +1 -1
  84. package/dist/src/cli/parser.js +81 -12
  85. package/dist/src/cli/services/daemon-lock.js +5 -2
  86. package/dist/src/cli/services/hook-block-hash.js +3 -1
  87. package/dist/src/cli/services/moflo-version.js +62 -0
  88. package/dist/src/cli/services/movector-training.js +1 -1
  89. package/dist/src/cli/services/spell-gate.js +6 -4
  90. package/dist/src/cli/services/worker-daemon.js +37 -7
  91. package/dist/src/cli/shared/utils/load-average.js +59 -0
  92. package/dist/src/cli/spells/commands/agent-command.js +55 -25
  93. package/dist/src/cli/suggest.js +1 -1
  94. package/dist/src/cli/swarm/swarm-persistence.js +112 -0
  95. package/dist/src/cli/swarm/unified-coordinator.js +118 -3
  96. package/dist/src/cli/version.js +1 -1
  97. package/package.json +5 -3
  98. package/dist/src/cli/mcp-tools/github-tools.js +0 -318
@@ -2,7 +2,7 @@
2
2
  * V3 CLI Main Entry Point
3
3
  * MoFlo V4 CLI
4
4
  *
5
- * Created with ❤️ by motailz.com
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 global flags
109
- if (flags.version || flags.V) {
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 motailz.com'));
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
- ### Auto-enforced gates
38
+ ### Gates
39
39
 
40
- - **TaskCreate-first**: Call \`TaskCreate\` before spawning the Agent tool
41
- - **Task Icons**: \`TaskCreate\` entries MUST use ICON+[Role] format — see \`.claude/guidance/moflo-task-icons.md\`
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
- var EDIT_RESET_SKIP_PATH_RE = /(?:^|[\\\\\\/])\\.github[\\\\\\/](?:workflows|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)(?:[\\\\\\/.]|$)/i;
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
- // Advisory only — agent spawning is never blocked.
560
- // Memory-first enforcement happens at the scan/read gate layer.
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. Task tool is blocked until then.\\n');
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 (!s.testsRun) {
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
- if (!s.verifyRun) { s.verifyRun = true; writeState(s); }
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): no gate reset.
740
- if (fp && EDIT_RESET_SKIP_BOTH_RE.test(fp)) break;
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
- var resetVerify = s.verifyRun;
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
- if (s.verifyRun) break;
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
- process.stderr.write(' - the change has not been verified since the last code edit (run /verify)\\n');
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 environment variables.
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 { execSync } from 'child_process';
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 has its own session_id.
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
- if (typeof hookContext.tool_input[key] === 'string') {
951
- env['TOOL_INPUT_' + key] = hookContext.tool_input[key];
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 = execSync('node "' + gateScript + '" ' + command, {
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', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.cs',
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
- const ext = path.extname(entry.name);
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@motailz.com>',
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: [{ type: 'command', command: gateCmd('record-learnings-stored'), timeout: 2000 }],
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
- import { githubTools } from './mcp-tools/github-tools.js';
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
- let VERSION = '4.6.3';
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
- serverInfo: { name: 'moflo', version: '3.0.0' },
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 },