moflo 4.9.9 → 4.9.11

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 (40) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +201 -0
  2. package/.claude/guidance/shipped/moflo-core-guidance.md +30 -391
  3. package/.claude/guidance/shipped/moflo-cross-platform.md +20 -1
  4. package/.claude/guidance/shipped/moflo-guidance-rules.md +144 -0
  5. package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -0
  6. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +33 -6
  7. package/.claude/guidance/shipped/moflo-session-start.md +154 -0
  8. package/.claude/guidance/shipped/moflo-settings-injection.md +124 -0
  9. package/.claude/guidance/shipped/moflo-source-hygiene.md +1 -1
  10. package/.claude/guidance/shipped/moflo-spell-custom-steps.md +126 -0
  11. package/.claude/guidance/shipped/moflo-spell-engine.md +4 -101
  12. package/.claude/guidance/shipped/moflo-subagents.md +10 -0
  13. package/.claude/guidance/shipped/moflo-task-icons.md +9 -0
  14. package/.claude/guidance/shipped/moflo-user-facing-language.md +8 -0
  15. package/.claude/guidance/shipped/moflo-yaml-reference.md +191 -0
  16. package/.claude/helpers/prompt-hook.mjs +16 -2
  17. package/.claude/skills/connector-builder/SKILL.md +1 -1
  18. package/.claude/skills/guidance/SKILL.md +158 -0
  19. package/.claude/skills/publish/SKILL.md +16 -0
  20. package/.claude/skills/simplify/SKILL.md +82 -0
  21. package/.claude/skills/spell-builder/SKILL.md +2 -2
  22. package/.claude/skills/spell-builder/architecture.md +1 -1
  23. package/.claude/skills/spell-schedule/SKILL.md +167 -0
  24. package/bin/generate-code-map.mjs +4 -5
  25. package/bin/hooks.mjs +4 -14
  26. package/bin/index-all.mjs +2 -10
  27. package/bin/index-guidance.mjs +5 -7
  28. package/bin/index-patterns.mjs +7 -9
  29. package/bin/index-tests.mjs +4 -5
  30. package/bin/lib/resolve-bin.mjs +62 -0
  31. package/bin/session-start-launcher.mjs +32 -24
  32. package/dist/src/cli/commands/doctor.js +30 -0
  33. package/dist/src/cli/index.js +18 -0
  34. package/dist/src/cli/init/moflo-init.js +14 -1
  35. package/dist/src/cli/init/settings-generator.js +18 -3
  36. package/dist/src/cli/services/daemon-readiness.js +12 -0
  37. package/dist/src/cli/services/hook-wiring.js +54 -1
  38. package/dist/src/cli/services/process-registry.js +58 -0
  39. package/dist/src/cli/version.js +1 -1
  40. package/package.json +2 -2
@@ -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.
@@ -356,7 +356,7 @@ Write the updated YAML back to the original file (or a new path if requested).
356
356
 
357
357
  **Runtime source:** `src/cli/spells/commands/` — each step is a TypeScript file registered in `index.ts`.
358
358
 
359
- **Adding a new step:** Create a directory under `steps/<name>/` with a `README.md`. Follow `.claude/guidance/internal/guidance-rules.md` and use existing step READMEs as templates. The step command source goes in `src/cli/spells/commands/` and is registered in `index.ts`. No changes to this SKILL.md needed.
359
+ **Adding a new step:** Create a directory under `steps/<name>/` with a `README.md`. Follow `.claude/guidance/shipped/moflo-guidance-rules.md` and use existing step READMEs as templates. The step command source goes in `src/cli/spells/commands/` and is registered in `index.ts`. No changes to this SKILL.md needed.
360
360
 
361
361
  ### Connectors
362
362
 
@@ -371,7 +371,7 @@ Write the updated YAML back to the original file (or a new path if requested).
371
371
 
372
372
  **Runtime source:** `src/cli/spells/connectors/` — each connector is a TypeScript file registered in `index.ts`.
373
373
 
374
- **Adding a new connector:** Create a directory under `connectors/<name>/` with a `README.md`. Follow `.claude/guidance/internal/guidance-rules.md` and use existing connector READMEs as templates. The connector source goes in `src/cli/spells/connectors/` and is registered in `index.ts`. No changes to this SKILL.md needed.
374
+ **Adding a new connector:** Create a directory under `connectors/<name>/` with a `README.md`. Follow `.claude/guidance/shipped/moflo-guidance-rules.md` and use existing connector READMEs as templates. The connector source goes in `src/cli/spells/connectors/` and is registered in `index.ts`. No changes to this SKILL.md needed.
375
375
 
376
376
  **When to create a new connector vs composing existing ones:** See [architecture.md](architecture.md) for the decision tree.
377
377
 
@@ -153,7 +153,7 @@ steps:
153
153
 
154
154
  ## Documentation Rules for New Components
155
155
 
156
- **Every new step, connector, or spell MUST include a README.md.** Apply the rules in `.claude/guidance/internal/guidance-rules.md` automatically — do not wait for the user to ask. Use existing READMEs in `steps/` and `connectors/` as templates.
156
+ **Every new step, connector, or spell MUST include a README.md.** Apply the rules in `.claude/guidance/shipped/moflo-guidance-rules.md` automatically — do not wait for the user to ask. Use existing READMEs in `steps/` and `connectors/` as templates.
157
157
 
158
158
  **Where to put the README:**
159
159
  - Steps: `.claude/skills/spell-builder/steps/<name>/README.md`
@@ -0,0 +1,167 @@
1
+ ---
2
+ name: spell-schedule
3
+ description: |
4
+ Schedule a moflo spell to run on the local machine via the moflo daemon (cron, interval, or one-time).
5
+ Use when the user wants to schedule, automate, or recurringly run one of THEIR spells locally —
6
+ e.g. "schedule the oap spell every hour", "run my audit spell every weekday at 9am", "fire X once tomorrow morning".
7
+ This is the LOCAL daemon path. For remote Anthropic-cloud agents, use /schedule instead.
8
+ arguments: "[spell-name-or-alias]"
9
+ ---
10
+
11
+ # /spell-schedule — Schedule a Local Spell
12
+
13
+ This skill walks the user through scheduling a moflo spell on the **local** moflo daemon.
14
+ Schedules live in moflo's memory store and are evaluated once per minute by the daemon's poll loop.
15
+ Execution goes through the same engine path as `flo spell cast`.
16
+
17
+ > Not the same as `/schedule`. `/schedule` creates **remote** Anthropic-cloud routines; this skill drives the **local** daemon scheduler.
18
+
19
+ **Arguments:** `$ARGUMENTS` (optional spell name/alias to pre-select)
20
+
21
+ ## When to use
22
+
23
+ The user says any of:
24
+ - "schedule the X spell"
25
+ - "run X every <interval>"
26
+ - "fire X once at <time>"
27
+ - "set up a recurring run for X"
28
+ - "I want X to run every morning"
29
+
30
+ If the user wants a **cloud** agent (mentions "remote", "GitHub Actions", "Anthropic cloud", or specifies a repo to clone), redirect them to `/schedule`.
31
+
32
+ ## Workflow
33
+
34
+ ### Step 1 — Verify the daemon is running
35
+
36
+ ```bash
37
+ npx flo doctor 2>&1 | grep -i daemon
38
+ ```
39
+
40
+ If the daemon is not running, prompt the user:
41
+ - "The moflo daemon isn't running. Schedules only fire while the daemon is up. Start it now?"
42
+ - If yes: `npx flo daemon start` (or instruct them to enable OS autostart for survival across reboots).
43
+ - If they decline, warn the user that the schedule will be created but won't fire until the daemon is started.
44
+
45
+ ### Step 2 — Identify the target spell
46
+
47
+ If `$ARGUMENTS` was provided, use it as the spell name/alias. Otherwise, list spells and let the user pick:
48
+
49
+ ```bash
50
+ npx flo spell list 2>&1
51
+ ```
52
+
53
+ The output is a markdown table with columns: name, alias, description, source. Both `name` and `alias` are valid for `flo spell schedule create -n <value>` — prefer the full name to avoid alias conflicts.
54
+
55
+ If the user-named spell is not in the list, stop and ask. Do NOT silently create a schedule for a missing spell — it will be auto-disabled on first fire.
56
+
57
+ ### Step 3 — Pick the cadence
58
+
59
+ Use AskUserQuestion to offer four options:
60
+
61
+ | Option | When to suggest | CLI form |
62
+ |--------|-----------------|----------|
63
+ | **Cron** | Specific time of day, day of week, or month boundary | `--cron "<5-field cron>"` (UTC, 5 fields: minute hour day-of-month month day-of-week) |
64
+ | **Interval** | "Every N seconds/minutes/hours/days" with no specific clock anchor | `--interval <N><s\|m\|h\|d>` (e.g., `30m`, `6h`, `1d`) |
65
+ | **One-time** | "Run once at..." or "remind me to..." | `--at <ISO 8601 datetime>` |
66
+ | **Embedded in spell** | The schedule should travel with the spell definition (registered every daemon start) | Edit the spell YAML to add a `schedule:` block; no CLI |
67
+
68
+ #### Timezone conversion (CRITICAL)
69
+
70
+ Cron expressions and `--at` timestamps are **always UTC**. The user almost always means their local time.
71
+
72
+ 1. **Look up the user's timezone** — derive from system. On Windows, `[System.TimeZoneInfo]::Local.Id` or read the auto-memory `currentDate` block. **Never** guess.
73
+ 2. **Convert to UTC** explicitly using PowerShell (cross-platform-safe):
74
+ ```powershell
75
+ [System.TimeZoneInfo]::ConvertTimeToUtc((Get-Date "9:00am"), [System.TimeZoneInfo]::Local)
76
+ ```
77
+ 3. **Echo back the conversion**: "9am America/Guatemala = 15:00 UTC, so the cron would be `0 15 * * 1-5`. Confirm?"
78
+ 4. **Re-check current time before any `--at`** — long conversations drift. Run `date -u +%Y-%m-%dT%H:%M:%SZ` (or PowerShell equivalent) before computing the absolute timestamp. If the resolved time is in the past, ask for clarification — do not silently roll forward.
79
+
80
+ #### Constraints
81
+
82
+ - Minimum poll interval is 1 minute (the daemon polls once per `pollIntervalMs`, default 60000). Sub-minute schedules are rejected.
83
+ - Interval units: `s`, `m`, `h`, `d` ONLY. `--interval 1w` is rejected at load time.
84
+ - `--at` must be a valid ISO 8601 datetime in the future.
85
+ - Exactly one of `--cron`, `--interval`, `--at` per schedule.
86
+
87
+ ### Step 4 — Confirm and create
88
+
89
+ Show the full plan to the user before creating:
90
+
91
+ ```
92
+ Spell: outlook-attachment-processor (alias: oap)
93
+ Cadence: every weekday at 9am America/Guatemala (15:00 UTC)
94
+ Cron: 0 15 * * 1-5
95
+ Daemon: running ✓
96
+ ```
97
+
98
+ After user confirms, run:
99
+
100
+ ```bash
101
+ npx flo spell schedule create -n <spell-name> --cron "<cron>" 2>&1
102
+ # or --interval <value>
103
+ # or --at <iso-datetime>
104
+ ```
105
+
106
+ Capture the schedule ID from output and surface it to the user along with the next computed run time.
107
+
108
+ ### Step 5 — Verify the wiring
109
+
110
+ Tail the schedule executions for the first fire so the user can confirm the daemon actually picks it up:
111
+
112
+ ```bash
113
+ npx flo spell schedule list 2>&1
114
+ ```
115
+
116
+ If the user wants to wait for the first fire (interval ≤ 5m), poll `flo spell schedule list` or the daemon dashboard. Otherwise, summarize and exit:
117
+
118
+ ```
119
+ Scheduled: <schedule-id>
120
+ Next run: <ISO datetime UTC> (<local-equivalent>)
121
+ Cancel: npx flo spell schedule cancel <schedule-id>
122
+ ```
123
+
124
+ ## Sub-actions (when not creating)
125
+
126
+ If the user asks to **list** schedules:
127
+ ```bash
128
+ npx flo spell schedule list 2>&1
129
+ ```
130
+
131
+ If the user asks to **cancel** a schedule:
132
+ 1. Run `flo spell schedule list` and let them pick.
133
+ 2. `npx flo spell schedule cancel <schedule-id>`.
134
+ 3. Confirm the entry is gone from the list.
135
+
136
+ If the user asks to **run now** without altering the cadence:
137
+ - Use the dashboard's "Run now" button if available, or the daemon's `runScheduleNow` API.
138
+ - The CLI does not currently expose this — surface that limitation if asked, and offer `flo spell cast -n <name>` as a manual alternative.
139
+
140
+ ## Important — gotchas
141
+
142
+ - **Daemon prerequisite**: schedules only fire while the daemon is running. Tell the user this explicitly. For survival across reboots, `flo daemon install` registers the OS-level autostart service.
143
+ - **Catch-up window** (default 1h, `scheduler.catchUpWindowMs` in `moflo.yaml`): if the daemon was offline when a run was due, runs within the window still fire on the next poll. Older missed runs are skipped with a `schedule:skipped` event.
144
+ - **maxConcurrent** (default 2): caps the number of scheduled spells running concurrently. Same-schedule overlap is never allowed.
145
+ - **No update CLI yet**: `flo spell schedule` exposes create/list/cancel only. To change a cadence, cancel + recreate.
146
+ - **Spell-required sandboxing** (#878): when that ships, scheduled runs honor it just like manual casts — a missing sandbox skips the run with a `schedule:skipped` event.
147
+
148
+ ## Output
149
+
150
+ End the session with a single-block summary:
151
+
152
+ ```
153
+ Schedule Created
154
+ ────────────────
155
+ Spell: <name>
156
+ Cadence: <human-readable>
157
+ Cron/At: <UTC expression>
158
+ ID: <schedule-id>
159
+ Next run: <UTC + local>
160
+ Cancel: npx flo spell schedule cancel <id>
161
+ Daemon: running | needs-start
162
+ ```
163
+
164
+ ## Reference
165
+
166
+ - Full daemon scheduler docs: https://github.com/eric-cielo/moflo/blob/main/docs/SPELLS.md#scheduling
167
+ - Tracking issue: https://github.com/eric-cielo/moflo/issues/877
@@ -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);
@@ -449,21 +450,10 @@ function spawnWindowless(cmd, args, description) {
449
450
  return result;
450
451
  }
451
452
 
452
- // 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.
453
455
  function resolveBinOrLocal(binName, localScript) {
454
- // 1. npm bin from moflo package (always up to date with published version)
455
- const mofloScript = resolve(projectRoot, 'node_modules/moflo/bin', localScript);
456
- if (existsSync(mofloScript)) return mofloScript;
457
-
458
- // 2. npm bin from .bin (symlinked by npm install)
459
- const npmBin = resolve(projectRoot, 'node_modules/.bin', binName);
460
- if (existsSync(npmBin)) return npmBin;
461
-
462
- // 3. Local .claude/scripts/ copy (may be stale but better than nothing)
463
- const localPath = resolve(projectRoot, '.claude/scripts', localScript);
464
- if (existsSync(localPath)) return localPath;
465
-
466
- return null;
456
+ return resolveMofloBin(projectRoot, binName, localScript);
467
457
  }
468
458
 
469
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
+ }
@@ -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-
@@ -837,8 +838,11 @@ try {
837
838
  settingsChanges.push('added statusLine');
838
839
  }
839
840
 
840
- // 3a-iv. Repair missing required hook wirings (same logic as doctor --fix
841
- // and moflo upgradeshared via hook-wiring.js to stay DRY)
841
+ // 3a-iv. Repair missing required hook wirings AND rewrite known-bad
842
+ // wirings from older moflo versions (#879 record-memory-searched
843
+ // wired to gate.cjs directly skips session_id forwarding and deadlocks
844
+ // the per-actor gate). Both passes share hook-wiring.js so doctor --fix,
845
+ // upgrade-merge, and the launcher stay DRY.
842
846
  try {
843
847
  const hwPaths = [
844
848
  resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/hook-wiring.js'),
@@ -846,11 +850,21 @@ try {
846
850
  ];
847
851
  const hwPath = hwPaths.find(p => existsSync(p));
848
852
  if (hwPath) {
849
- const { repairHookWiring } = await import(`file://${hwPath.replace(/\\/g, '/')}`);
850
- const { repaired } = repairHookWiring(settings);
851
- if (repaired.length > 0) {
852
- dirty = true;
853
- settingsChanges.push(`repaired ${plural(repaired.length, 'hook wiring')}`);
853
+ const mod = await import(`file://${hwPath.replace(/\\/g, '/')}`);
854
+ if (typeof mod.rewriteIncorrectHookWiring === 'function') {
855
+ const { rewrites } = mod.rewriteIncorrectHookWiring(settings);
856
+ if (rewrites.length > 0) {
857
+ dirty = true;
858
+ const total = rewrites.reduce((n, r) => n + r.count, 0);
859
+ settingsChanges.push(`rewrote ${plural(total, 'stale hook wiring')}`);
860
+ }
861
+ }
862
+ if (typeof mod.repairHookWiring === 'function') {
863
+ const { repaired } = mod.repairHookWiring(settings);
864
+ if (repaired.length > 0) {
865
+ dirty = true;
866
+ settingsChanges.push(`repaired ${plural(repaired.length, 'hook wiring')}`);
867
+ }
854
868
  }
855
869
  }
856
870
  } catch (err) {
@@ -1125,19 +1139,15 @@ if (mutationCount > 0) {
1125
1139
  // ── 4. Spawn background tasks ───────────────────────────────────────────────
1126
1140
 
1127
1141
  // hooks.mjs session-start (daemon, indexer, pretrain, HNSW, neural patterns).
1128
- // Prefer the npm-bin copy over the `.claude/scripts/` mirror (#866). The mirror
1129
- // is a derived sync that races the launcher's section-3 file copies during the
1130
- // very upgrade session spawning the still-stale `.claude/scripts/hooks.mjs`
1131
- // then chaining `__dirname/index-all.mjs` produces an orphan running pre-
1132
- // upgrade argv (e.g. `rebuild-index --force` after #859 had already dropped
1133
- // it). The bin/ copy is updated atomically by `npm install moflo` (single-
1134
- // step), so spawning from there guarantees the running hook code matches the
1135
- // installed package. Falls back to the mirror only when the package copy is
1136
- // unresolvable (development / symlinked installs).
1137
- const hooksPkg = resolve(projectRoot, 'node_modules/moflo/bin/hooks.mjs');
1138
- const hooksMirror = resolve(projectRoot, '.claude/scripts/hooks.mjs');
1139
- const hooksScript = existsSync(hooksPkg) ? hooksPkg : hooksMirror;
1140
- if (existsSync(hooksScript)) {
1142
+ // Bin-first ordering via resolveMofloBin — prefers the npm-package copy over
1143
+ // the `.claude/scripts/` mirror (#866). The mirror is a derived sync that
1144
+ // races the launcher's section-3 file copies during the very upgrade session;
1145
+ // spawning the still-stale mirror produces an orphan running pre-upgrade argv
1146
+ // (e.g. `rebuild-index --force` after #859 had already dropped it). The pkg
1147
+ // copy is updated atomically by `npm install moflo`, so spawning from there
1148
+ // guarantees the running hook code matches the installed package.
1149
+ const hooksScript = resolveMofloBin(projectRoot, null, 'hooks.mjs');
1150
+ if (hooksScript) {
1141
1151
  fireAndForget('node', [hooksScript, 'session-start'], 'hooks session-start');
1142
1152
  }
1143
1153
 
@@ -1152,10 +1162,8 @@ if (existsSync(hooksScript)) {
1152
1162
  // Run synchronously (capture stdout) so each completed migration surfaces
1153
1163
  // through emitMutation — Claude's session-start hook captures launcher
1154
1164
  // stdout and that's the only channel that reaches the user.
1155
- const runMigrationsPkg = resolve(projectRoot, 'node_modules/moflo/bin/run-migrations.mjs');
1156
- const runMigrationsMirror = resolve(projectRoot, '.claude/scripts/run-migrations.mjs');
1157
- const runMigrations = existsSync(runMigrationsPkg) ? runMigrationsPkg : runMigrationsMirror;
1158
- if (existsSync(runMigrations)) {
1165
+ const runMigrations = resolveMofloBin(projectRoot, null, 'run-migrations.mjs');
1166
+ if (runMigrations) {
1159
1167
  runMigrationsAndAnnounce(runMigrations);
1160
1168
  }
1161
1169