moflo 4.9.8 → 4.9.10

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.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from 'child_process';
3
+ import { readFileSync } from 'fs';
3
4
  import { resolve } from 'path';
4
5
 
5
6
  // Read stdin JSON from Claude Code
@@ -70,6 +71,19 @@ if (TEST_HINTS.test(lower)) {
70
71
  }
71
72
  }
72
73
 
73
- var parts = [output.trim(), nsHint].filter(Boolean);
74
- if (parts.length) process.stdout.write(parts.join('\n') + '\n');
74
+ // #867 surface post-install restart notice. File is written by
75
+ // scripts/post-install-notice.mjs and cleared by the SessionStart launcher
76
+ // once the running moflo matches the file's version, so the file's
77
+ // existence at prompt-time is itself the "still needs restart" signal.
78
+ var restartNotice = '';
79
+ try {
80
+ var pendingPath = resolve(projectDir, '.moflo', 'restart-pending.json');
81
+ var pending = JSON.parse(readFileSync(pendingPath, 'utf-8'));
82
+ if (pending && typeof pending.message === 'string' && pending.message.length > 0) {
83
+ restartNotice = pending.message;
84
+ }
85
+ } catch (e) { /* ENOENT or malformed — silent fast-path */ }
86
+
87
+ var parts = [restartNotice, output.trim(), nsHint].filter(Boolean);
88
+ if (parts.length) process.stdout.write(parts.join('\n\n') + '\n');
75
89
  process.exit(0);
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: simplify
3
+ description: Review changed code for reuse, quality, and efficiency, then fix any issues found. Sizes review effort to the diff — trivial edits get a self-review, substantial edits get parallel agents.
4
+ ---
5
+
6
+ # /simplify — Adaptive Code Review
7
+
8
+ Review changed code for reuse opportunities, quality issues, and efficiency improvements. **Effort scales with diff size** — a 5-line comment trim doesn't get the same treatment as a 500-line refactor.
9
+
10
+ ## Phase 1: Identify changes
11
+
12
+ Run `git diff HEAD` (working tree) and `git diff main...HEAD` (committed) to get the full set of changes since the branch diverged. If on `main` with uncommitted changes, just `git diff HEAD`.
13
+
14
+ Treat the union of staged + unstaged + committed-since-base as the diff to review.
15
+
16
+ ## Phase 2: Classify the diff
17
+
18
+ Pick the **smallest tier** the diff genuinely fits. When in doubt, escalate.
19
+
20
+ ### TRIVIAL — self-review, no agent spawn
21
+ ALL of these must hold:
22
+ - ≤10 net LOC changed (insertions + deletions, excluding pure whitespace)
23
+ - Single file
24
+ - No logic changes — only comments, formatting, renames of local vars, JSDoc, or string-literal edits
25
+ - No new imports, no new exports, no new function/class declarations
26
+ - No removed safety checks, error handlers, or guards
27
+
28
+ Examples that qualify: trimming a comment, fixing a typo in a log message, renaming a private helper, reformatting a single block.
29
+ Examples that DON'T qualify: changing an `if` condition, reordering function args, deleting a try/catch.
30
+
31
+ ### SMALL — single agent, all three categories
32
+ ALL of these must hold:
33
+ - ≤50 net LOC changed
34
+ - ≤2 files
35
+ - No structural changes (no new modules, no API additions/removals, no contract changes)
36
+
37
+ Examples that qualify: extracting a constant, inlining a one-liner, swapping a `for` for a `forEach`, adding one early-return.
38
+
39
+ ### NORMAL — three parallel agents (the original flow)
40
+ Anything that doesn't fit TRIVIAL or SMALL. Includes any diff that:
41
+ - Spans 3+ files
42
+ - Adds/removes/renames a public API
43
+ - Changes control flow in a non-trivial way
44
+ - Introduces or removes a dependency
45
+ - Touches `bin/`, hooks, MCP tool handlers, or anything called out in `CLAUDE.md` as critical surface
46
+
47
+ When CLAUDE.md flags a file as critical surface (SessionStart, launcher, hooks, MCP coordinator wiring, swarm/hive-mind), **always escalate to NORMAL** regardless of LOC count. Risk-weighted, not size-weighted.
48
+
49
+ ## Phase 3: Run the appropriate review
50
+
51
+ ### TRIVIAL: self-review
52
+ Run the same three category checks (reuse / quality / efficiency) yourself, in one pass, against the diff. Most TRIVIAL diffs will be clean — the goal is to confirm, not to fan out. If you find an issue, fix it; otherwise stamp clean. Total budget: ~30 seconds, no Agent calls.
53
+
54
+ ### SMALL: one agent
55
+ Launch a SINGLE Agent with subagent_type `reviewer` covering all three categories in one prompt. Pass the diff inline. Budget: ~1 minute.
56
+
57
+ ```
58
+ Agent — subagent_type: "reviewer", prompt: "Review this diff for reuse, quality, and efficiency. <diff inline>. Flag specific issues with file:line; skip generic advice. Under 200 words."
59
+ ```
60
+
61
+ ### NORMAL: three parallel agents (original flow)
62
+ Launch three agents in a single message — Reuse, Quality, Efficiency — passing the full diff to each. Use the original flow's category checklists.
63
+
64
+ **Reuse**: existing helpers/utilities that should be used instead; duplicated patterns; new functions that re-implement something already in the codebase.
65
+
66
+ **Quality**: redundant state, parameter sprawl, copy-paste with variation, leaky abstractions, stringly-typed code, nested conditionals 3+ levels, unnecessary comments (WHAT-explanations, task references).
67
+
68
+ **Efficiency**: unnecessary work, missed concurrency, hot-path bloat, recurring no-op updates, TOCTOU existence checks, unbounded structures, over-broad reads.
69
+
70
+ ## Phase 4: Fix or skip
71
+
72
+ Aggregate findings. Fix each one directly. False positives or not-worth-fixing — note and skip without arguing. If TRIVIAL self-review found nothing, just confirm clean and exit.
73
+
74
+ If fixes were made, re-run tests to confirm nothing broke. If tests fail after a fix, revert it.
75
+
76
+ ## Phase 5: Stamp the gate
77
+
78
+ Whatever tier ran, the gate (`check-before-pr`) registers /simplify as having executed. The skill is satisfied.
79
+
80
+ ## Briefly summarize
81
+
82
+ End with one or two sentences: which tier, what was fixed (or "clean — no changes"). No headers, no bullets unless needed.
@@ -32,6 +32,7 @@ import { execSync, execFileSync, spawn } from 'child_process';
32
32
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
33
33
  import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
34
34
  import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
35
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
35
36
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
36
37
 
37
38
 
@@ -914,11 +915,9 @@ async function main() {
914
915
  }
915
916
 
916
917
  async function runEmbeddings() {
917
- const embedCandidates = [
918
- resolve(dirname(fileURLToPath(import.meta.url)), 'build-embeddings.mjs'),
919
- resolve(projectRoot, '.claude/scripts/build-embeddings.mjs'),
920
- ];
921
- const embedScript = embedCandidates.find(p => existsSync(p));
918
+ const embedScript = resolveMofloBin(
919
+ projectRoot, 'flo-embeddings', 'build-embeddings.mjs', { includeDevFallback: true },
920
+ );
922
921
  if (!embedScript) return;
923
922
 
924
923
  log('Generating embeddings for code-map...');
package/bin/hooks.mjs CHANGED
@@ -25,6 +25,7 @@ import { resolve, dirname } from 'path';
25
25
  import { fileURLToPath } from 'url';
26
26
  import { createProcessManager } from './lib/process-manager.mjs';
27
27
  import { shouldDaemonAutoStart } from './lib/daemon-config.mjs';
28
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
28
29
 
29
30
  const __filename = fileURLToPath(import.meta.url);
30
31
  const __dirname = dirname(__filename);
@@ -289,7 +290,20 @@ async function main() {
289
290
  // chain and producing the sqlite_autoindex corruption that #743 had
290
291
  // to repair on subsequent sessions. The launcher's foreground §3e
291
292
  // migration is the canonical migration site. See #744.
292
- spawnWindowless('node', [resolve(__dirname, 'index-all.mjs')], 'sequential indexing chain');
293
+ // Prefer the npm-bin copy of index-all.mjs over __dirname resolution
294
+ // (#866). When this hooks.mjs is loaded from `.claude/scripts/` (e.g.
295
+ // an older launcher spawned us before the launcher's bin-anchor fix
296
+ // landed), `resolve(__dirname, 'index-all.mjs')` would point at the
297
+ // synced mirror — which the launcher's section 3 may still be in the
298
+ // middle of overwriting during an upgrade. resolveBinOrLocal's
299
+ // bin-first ordering guarantees the spawned chain matches the
300
+ // installed package even when the mirror is mid-sync.
301
+ const indexAllScript = resolveBinOrLocal('flo-index-all', 'index-all.mjs');
302
+ if (indexAllScript) {
303
+ spawnWindowless('node', [indexAllScript], 'sequential indexing chain');
304
+ } else {
305
+ log('warn', 'index-all.mjs not found (checked npm bin + .claude/scripts/)');
306
+ }
293
307
  // Neural patterns now loaded by moflo core routing — no external patching.
294
308
  break;
295
309
  }
@@ -436,21 +450,10 @@ function spawnWindowless(cmd, args, description) {
436
450
  return result;
437
451
  }
438
452
 
439
- // Resolve a moflo npm bin script, falling back to local .claude/scripts/ copy
453
+ // Resolve a moflo bin script via the shared helper (bin/lib/resolve-bin.mjs).
454
+ // Bin-first ordering — see #866 / #869.
440
455
  function resolveBinOrLocal(binName, localScript) {
441
- // 1. npm bin from moflo package (always up to date with published version)
442
- const mofloScript = resolve(projectRoot, 'node_modules/moflo/bin', localScript);
443
- if (existsSync(mofloScript)) return mofloScript;
444
-
445
- // 2. npm bin from .bin (symlinked by npm install)
446
- const npmBin = resolve(projectRoot, 'node_modules/.bin', binName);
447
- if (existsSync(npmBin)) return npmBin;
448
-
449
- // 3. Local .claude/scripts/ copy (may be stale but better than nothing)
450
- const localPath = resolve(projectRoot, '.claude/scripts', localScript);
451
- if (existsSync(localPath)) return localPath;
452
-
453
- return null;
456
+ return resolveMofloBin(projectRoot, binName, localScript);
454
457
  }
455
458
 
456
459
  // Run the guidance indexer in background (non-blocking - used for session start and file changes)
package/bin/index-all.mjs CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  saveStepFingerprint,
26
26
  cleanupLegacyFingerprint,
27
27
  } from './lib/index-fingerprint.mjs';
28
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
28
29
 
29
30
  // Cap fastembed/ONNX thread count when spawning the heavy steps. Without
30
31
  // this, ONNX defaults to one thread per CPU core (22+ on a modern dev box),
@@ -61,16 +62,7 @@ function log(msg) {
61
62
  }
62
63
 
63
64
  function resolveBin(binName, localScript) {
64
- const mofloScript = resolve(projectRoot, 'node_modules/moflo/bin', localScript);
65
- if (existsSync(mofloScript)) return mofloScript;
66
- const npmBin = resolve(projectRoot, 'node_modules/.bin', binName);
67
- if (existsSync(npmBin)) return npmBin;
68
- const localPath = resolve(projectRoot, '.claude/scripts', localScript);
69
- if (existsSync(localPath)) return localPath;
70
- // Also check bin/ directory (for development use)
71
- const binPath = resolve(projectRoot, 'bin', localScript);
72
- if (existsSync(binPath)) return binPath;
73
- return null;
65
+ return resolveMofloBin(projectRoot, binName, localScript, { includeDevFallback: true });
74
66
  }
75
67
 
76
68
  function getLocalCliPath() {
@@ -27,6 +27,7 @@ import { resolve, relative, dirname, basename, extname } from 'path';
27
27
  import { fileURLToPath } from 'url';
28
28
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
29
29
  import { memoryDbPath } from './lib/moflo-paths.mjs';
30
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
30
31
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
31
32
 
32
33
 
@@ -873,14 +874,11 @@ if (!skipEmbeddings && needsEmbeddings) {
873
874
 
874
875
  const { spawn } = await import('child_process');
875
876
 
876
- // Look for build-embeddings script in multiple locations:
877
- // 1. Shipped with moflo (node_modules/moflo/bin/)
878
- // 2. Project-local (.claude/scripts/)
879
- const mofloScript = resolve(__dirname, 'build-embeddings.mjs');
880
- const projectLocalScript = resolve(projectRoot, '.claude/scripts/build-embeddings.mjs');
881
- const embeddingScript = existsSync(mofloScript) ? mofloScript : projectLocalScript;
877
+ const embeddingScript = resolveMofloBin(
878
+ projectRoot, 'flo-embeddings', 'build-embeddings.mjs', { includeDevFallback: true },
879
+ );
882
880
 
883
- if (existsSync(embeddingScript)) {
881
+ if (embeddingScript) {
884
882
  const embeddingArgs = ['--namespace', NAMESPACE];
885
883
 
886
884
  // Create log file for background process output
@@ -28,10 +28,10 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
28
28
  import { resolve, dirname, relative, basename, extname } from 'path';
29
29
  import { fileURLToPath } from 'url';
30
30
  import { spawn } from 'child_process';
31
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
31
32
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
32
33
  import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
33
34
  import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
34
- const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
35
35
 
36
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
37
 
@@ -75,6 +75,9 @@ function ensureDbDir() {
75
75
 
76
76
  async function getDb() {
77
77
  ensureDbDir();
78
+ // Lazy: hash-cache-match and no-source-files early-exits in main() never
79
+ // reach this, and the sql.js wasm cold-load is ~400ms otherwise wasted.
80
+ const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
78
81
  const SQL = await initSqlJs();
79
82
  let db;
80
83
  if (existsSync(DB_PATH)) {
@@ -341,14 +344,9 @@ async function main() {
341
344
 
342
345
  // Trigger embedding generation in background
343
346
  try {
344
- // Check __dirname first (works in both dev bin/ and consumer .claude/scripts/),
345
- // then fall back to node_modules/moflo/bin/ for consumer projects
346
- const candidates = [
347
- resolve(__dirname, 'build-embeddings.mjs'),
348
- resolve(projectRoot, 'node_modules/moflo/bin/build-embeddings.mjs'),
349
- resolve(projectRoot, '.claude/scripts/build-embeddings.mjs'),
350
- ];
351
- const embeddingScript = candidates.find(p => existsSync(p));
347
+ const embeddingScript = resolveMofloBin(
348
+ projectRoot, 'flo-embeddings', 'build-embeddings.mjs', { includeDevFallback: true },
349
+ );
352
350
  if (embeddingScript) {
353
351
  const child = spawn('node', [embeddingScript, '--namespace', NAMESPACE], {
354
352
  cwd: projectRoot,
@@ -28,6 +28,7 @@ import { fileURLToPath } from 'url';
28
28
  import { execSync, execFileSync, spawn } from 'child_process';
29
29
  import { mofloResolveURL } from './lib/moflo-resolve.mjs';
30
30
  import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
31
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
31
32
  import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
32
33
  const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
33
34
 
@@ -687,11 +688,9 @@ async function main() {
687
688
  }
688
689
 
689
690
  async function runEmbeddings() {
690
- const embedCandidates = [
691
- resolve(dirname(fileURLToPath(import.meta.url)), 'build-embeddings.mjs'),
692
- resolve(projectRoot, '.claude/scripts/build-embeddings.mjs'),
693
- ];
694
- const embedScript = embedCandidates.find(p => existsSync(p));
691
+ const embedScript = resolveMofloBin(
692
+ projectRoot, 'flo-embeddings', 'build-embeddings.mjs', { includeDevFallback: true },
693
+ );
695
694
  if (!embedScript) return;
696
695
 
697
696
  log('Generating embeddings for tests...');
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared moflo bin-script path resolver.
3
+ *
4
+ * Resolves a moflo bin script in a consumer project, preferring the
5
+ * npm-installed copy over the .claude/scripts/ mirror so the spawned process
6
+ * matches the installed package version.
7
+ *
8
+ * Resolution order (intentional — see #866):
9
+ * 1. <projectRoot>/node_modules/moflo/bin/<localScript> — published, atomic
10
+ * 2. <projectRoot>/node_modules/.bin/<binName> — npm-managed alias (only if binName given)
11
+ * 3. <projectRoot>/.claude/scripts/<localScript> — derived sync, can lag a session
12
+ * 4. <projectRoot>/bin/<localScript> — DEV ONLY, opt-in via includeDevFallback
13
+ *
14
+ * Why bin-first matters: the .claude/scripts/ mirror is updated by a sync
15
+ * step that races the launcher's section-3 file copies during the very
16
+ * upgrade session, so a still-stale mirror can spawn pre-upgrade argv into
17
+ * a post-upgrade chain (#866). The npm package copy is updated atomically
18
+ * by `npm install moflo`, so spawning from there guarantees the running
19
+ * script matches the installed package.
20
+ *
21
+ * Cross-platform: paths are joined via `node:path.resolve`, which normalises
22
+ * separators on Windows + POSIX. Callers compute `projectRoot` themselves
23
+ * (typically via findProjectRoot()) so this helper has no implicit cwd.
24
+ */
25
+
26
+ import { resolve } from 'node:path';
27
+ import { existsSync } from 'node:fs';
28
+
29
+ /**
30
+ * @param {string} projectRoot Consumer's project root (caller-computed).
31
+ * @param {string|null} binName npm bin alias (e.g. 'flo-index'), or null/undefined when no alias exists.
32
+ * @param {string} localScript Script filename (e.g. 'index-guidance.mjs').
33
+ * @param {{ includeDevFallback?: boolean }} [opts]
34
+ * includeDevFallback: also probe `<projectRoot>/bin/<localScript>` for source-tree dev.
35
+ * @returns {string|null} Resolved absolute path, or null when no candidate exists.
36
+ */
37
+ export function resolveMofloBin(projectRoot, binName, localScript, opts = {}) {
38
+ for (const candidate of mofloBinCandidates(projectRoot, binName, localScript, opts)) {
39
+ if (existsSync(candidate)) return candidate;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Pure-function variant — returns the candidate list in resolution order
46
+ * without touching the filesystem. Used by the source-invariant test that
47
+ * pins the bin-first ordering, and by callers that want to log what was
48
+ * probed when nothing resolved.
49
+ */
50
+ export function mofloBinCandidates(projectRoot, binName, localScript, opts = {}) {
51
+ const candidates = [
52
+ resolve(projectRoot, 'node_modules', 'moflo', 'bin', localScript),
53
+ ];
54
+ if (binName) {
55
+ candidates.push(resolve(projectRoot, 'node_modules', '.bin', binName));
56
+ }
57
+ candidates.push(resolve(projectRoot, '.claude', 'scripts', localScript));
58
+ if (opts.includeDevFallback) {
59
+ candidates.push(resolve(projectRoot, 'bin', localScript));
60
+ }
61
+ return candidates;
62
+ }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from 'child_process';
3
+ import { readFileSync } from 'fs';
3
4
  import { resolve } from 'path';
4
5
 
5
6
  // Read stdin JSON from Claude Code
@@ -70,6 +71,19 @@ if (TEST_HINTS.test(lower)) {
70
71
  }
71
72
  }
72
73
 
73
- var parts = [output.trim(), nsHint].filter(Boolean);
74
- if (parts.length) process.stdout.write(parts.join('\n') + '\n');
74
+ // #867 surface post-install restart notice. File is written by
75
+ // scripts/post-install-notice.mjs and cleared by the SessionStart launcher
76
+ // once the running moflo matches the file's version, so the file's
77
+ // existence at prompt-time is itself the "still needs restart" signal.
78
+ var restartNotice = '';
79
+ try {
80
+ var pendingPath = resolve(projectDir, '.moflo', 'restart-pending.json');
81
+ var pending = JSON.parse(readFileSync(pendingPath, 'utf-8'));
82
+ if (pending && typeof pending.message === 'string' && pending.message.length > 0) {
83
+ restartNotice = pending.message;
84
+ }
85
+ } catch (e) { /* ENOENT or malformed — silent fast-path */ }
86
+
87
+ var parts = [restartNotice, output.trim(), nsHint].filter(Boolean);
88
+ if (parts.length) process.stdout.write(parts.join('\n\n') + '\n');
75
89
  process.exit(0);
@@ -13,6 +13,7 @@ import { resolve, dirname, join } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { mofloDir } from './lib/moflo-paths.mjs';
15
15
  import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
16
+ import { resolveMofloBin } from './lib/resolve-bin.mjs';
16
17
 
17
18
  // Headless skip (#860). The daemon's headless workers spawn `claude --print`
18
19
  // with CLAUDE_CODE_HEADLESS=true (see src/cli/services/headless-worker-
@@ -217,6 +218,23 @@ try {
217
218
  // own errors if the DB is still broken.
218
219
  }
219
220
 
221
+ // ── 0d. Clear post-install restart notice when version is current (#867) ───
222
+ // scripts/post-install-notice.mjs drops `.moflo/restart-pending.json` on every
223
+ // `npm install moflo`. The UserPromptSubmit hook surfaces it on every prompt
224
+ // until cleared, so this session only sees the message between install and
225
+ // the FIRST restart that actually picks up the new bits.
226
+ try {
227
+ const pendingPath = join(mofloDir(projectRoot), 'restart-pending.json');
228
+ const pkgPath = resolve(projectRoot, 'node_modules/moflo/package.json');
229
+ const pending = JSON.parse(readFileSync(pendingPath, 'utf-8'));
230
+ const installedVersion = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
231
+ if (pending && typeof pending.version === 'string' && pending.version === installedVersion) {
232
+ unlinkSync(pendingPath);
233
+ try { unlinkSync(join(mofloDir(projectRoot), 'last-install-banner.json')); } catch { /* tracker may not exist */ }
234
+ emitMutation('cleared post-install restart notice', `${installedVersion} now running`);
235
+ }
236
+ } catch { /* file missing or malformed — silent fast-path */ }
237
+
220
238
  // ── 1. Helper: fire-and-forget a background process ─────────────────────────
221
239
  function fireAndForget(cmd, args, label) {
222
240
  try {
@@ -1106,12 +1124,17 @@ if (mutationCount > 0) {
1106
1124
  }
1107
1125
 
1108
1126
  // ── 4. Spawn background tasks ───────────────────────────────────────────────
1109
- const localCli = resolve(projectRoot, 'node_modules/moflo/bin/cli.js');
1110
- const hasLocalCli = existsSync(localCli);
1111
1127
 
1112
- // hooks.mjs session-start (daemon, indexer, pretrain, HNSW, neural patterns)
1113
- const hooksScript = resolve(projectRoot, '.claude/scripts/hooks.mjs');
1114
- if (existsSync(hooksScript)) {
1128
+ // hooks.mjs session-start (daemon, indexer, pretrain, HNSW, neural patterns).
1129
+ // Bin-first ordering via resolveMofloBin — prefers the npm-package copy over
1130
+ // the `.claude/scripts/` mirror (#866). The mirror is a derived sync that
1131
+ // races the launcher's section-3 file copies during the very upgrade session;
1132
+ // spawning the still-stale mirror produces an orphan running pre-upgrade argv
1133
+ // (e.g. `rebuild-index --force` after #859 had already dropped it). The pkg
1134
+ // copy is updated atomically by `npm install moflo`, so spawning from there
1135
+ // guarantees the running hook code matches the installed package.
1136
+ const hooksScript = resolveMofloBin(projectRoot, null, 'hooks.mjs');
1137
+ if (hooksScript) {
1115
1138
  fireAndForget('node', [hooksScript, 'session-start'], 'hooks session-start');
1116
1139
  }
1117
1140
 
@@ -1126,10 +1149,8 @@ if (existsSync(hooksScript)) {
1126
1149
  // Run synchronously (capture stdout) so each completed migration surfaces
1127
1150
  // through emitMutation — Claude's session-start hook captures launcher
1128
1151
  // stdout and that's the only channel that reaches the user.
1129
- const runMigrationsPkg = resolve(projectRoot, 'node_modules/moflo/bin/run-migrations.mjs');
1130
- const runMigrationsMirror = resolve(projectRoot, '.claude/scripts/run-migrations.mjs');
1131
- const runMigrations = existsSync(runMigrationsPkg) ? runMigrationsPkg : runMigrationsMirror;
1132
- if (existsSync(runMigrations)) {
1152
+ const runMigrations = resolveMofloBin(projectRoot, null, 'run-migrations.mjs');
1153
+ if (runMigrations) {
1133
1154
  runMigrationsAndAnnounce(runMigrations);
1134
1155
  }
1135
1156
 
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.8';
5
+ export const VERSION = '4.9.10';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.8",
3
+ "version": "4.9.10",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -81,7 +81,7 @@
81
81
  "@typescript-eslint/eslint-plugin": "^7.18.0",
82
82
  "@typescript-eslint/parser": "^7.18.0",
83
83
  "eslint": "^8.0.0",
84
- "moflo": "^4.9.7",
84
+ "moflo": "^4.9.9",
85
85
  "tsx": "^4.21.0",
86
86
  "typescript": "^5.9.3",
87
87
  "vitest": "^4.0.0"