moflo 4.9.10 → 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 (31) 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/skills/connector-builder/SKILL.md +1 -1
  17. package/.claude/skills/guidance/SKILL.md +158 -0
  18. package/.claude/skills/publish/SKILL.md +16 -0
  19. package/.claude/skills/spell-builder/SKILL.md +2 -2
  20. package/.claude/skills/spell-builder/architecture.md +1 -1
  21. package/.claude/skills/spell-schedule/SKILL.md +167 -0
  22. package/bin/session-start-launcher.mjs +20 -7
  23. package/dist/src/cli/commands/doctor.js +30 -0
  24. package/dist/src/cli/index.js +18 -0
  25. package/dist/src/cli/init/moflo-init.js +14 -1
  26. package/dist/src/cli/init/settings-generator.js +18 -3
  27. package/dist/src/cli/services/daemon-readiness.js +12 -0
  28. package/dist/src/cli/services/hook-wiring.js +54 -1
  29. package/dist/src/cli/services/process-registry.js +58 -0
  30. package/dist/src/cli/version.js +1 -1
  31. package/package.json +2 -2
@@ -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
@@ -838,8 +838,11 @@ try {
838
838
  settingsChanges.push('added statusLine');
839
839
  }
840
840
 
841
- // 3a-iv. Repair missing required hook wirings (same logic as doctor --fix
842
- // 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.
843
846
  try {
844
847
  const hwPaths = [
845
848
  resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/hook-wiring.js'),
@@ -847,11 +850,21 @@ try {
847
850
  ];
848
851
  const hwPath = hwPaths.find(p => existsSync(p));
849
852
  if (hwPath) {
850
- const { repairHookWiring } = await import(`file://${hwPath.replace(/\\/g, '/')}`);
851
- const { repaired } = repairHookWiring(settings);
852
- if (repaired.length > 0) {
853
- dirty = true;
854
- 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
+ }
855
868
  }
856
869
  }
857
870
  } catch (err) {
@@ -1114,12 +1114,37 @@ function killTrackedProcesses() {
1114
1114
  catch { /* ok */ }
1115
1115
  return killed;
1116
1116
  }
1117
+ // Read the set of moflo background PIDs registered with the shared
1118
+ // ProcessManager (.moflo/background-pids.json). These are legitimate tracked
1119
+ // background tasks (sequential indexer chain, daemon, MCP servers spawned by
1120
+ // session-start) — they are detached:true by design so their parents have
1121
+ // already exited, but they are NOT orphans. Without this allow-set,
1122
+ // findZombieProcesses() flags every running indexer step as a zombie.
1123
+ function readTrackedBackgroundPids() {
1124
+ const result = new Set();
1125
+ const registryFile = join(process.cwd(), '.moflo', 'background-pids.json');
1126
+ try {
1127
+ if (!existsSync(registryFile))
1128
+ return result;
1129
+ const entries = JSON.parse(readFileSync(registryFile, 'utf-8'));
1130
+ if (!Array.isArray(entries))
1131
+ return result;
1132
+ for (const entry of entries) {
1133
+ if (entry && typeof entry.pid === 'number' && entry.pid > 0) {
1134
+ result.add(entry.pid);
1135
+ }
1136
+ }
1137
+ }
1138
+ catch { /* malformed or unreadable — treat as empty */ }
1139
+ return result;
1140
+ }
1117
1141
  // Find and optionally kill orphaned moflo/claude-flow node processes.
1118
1142
  // A process is only "orphaned" if its parent is no longer alive — meaning
1119
1143
  // nothing will clean it up. MCP servers spawned by a live Claude Code session
1120
1144
  // have a live parent (claude.exe) and must not be flagged.
1121
1145
  async function findZombieProcesses(kill = false) {
1122
1146
  const legitimatePid = getDaemonLockHolder(process.cwd());
1147
+ const trackedPids = readTrackedBackgroundPids();
1123
1148
  const currentPid = process.pid;
1124
1149
  const parentPid = process.ppid;
1125
1150
  const found = [];
@@ -1161,6 +1186,11 @@ async function findZombieProcesses(kill = false) {
1161
1186
  for (const { pid, ppid } of candidates) {
1162
1187
  if (pid === currentPid || pid === parentPid || pid === legitimatePid)
1163
1188
  continue;
1189
+ // Tracked background tasks (indexer chain, etc.) are detached:true so their
1190
+ // parent is dead by design. The ProcessManager registry tells us they are
1191
+ // legitimate, not orphaned.
1192
+ if (trackedPids.has(pid))
1193
+ continue;
1164
1194
  if (isProcessAlive(ppid))
1165
1195
  continue;
1166
1196
  // Defense-in-depth: detached daemons have dead parents by design.
@@ -11,6 +11,7 @@ import { suggestCommand } from './suggest.js';
11
11
  import { runStartupUpdateCheck } from './update/index.js';
12
12
  import { loadMofloConfig } from './config/moflo-config.js';
13
13
  import { getDaemonLockHolder } from './services/daemon-lock.js';
14
+ import { registerBackgroundPid } from './services/process-registry.js';
14
15
  import { VERSION } from './version.js';
15
16
  export { VERSION };
16
17
  const LONG_RUNNING_COMMANDS = ['mcp', 'daemon'];
@@ -461,6 +462,23 @@ export class CLI {
461
462
  env: { ...process.env, CLAUDE_FLOW_DAEMON: '1' },
462
463
  });
463
464
  child.unref();
465
+ // Register the spawned daemon PID with the shared ProcessManager so that
466
+ // pm.killAll() (called by the session-end hook) can reap it, AND doctor's
467
+ // zombie scan recognises it as a tracked process rather than an orphan.
468
+ // Without this, every consumer's first `flo doctor` after a CLI command
469
+ // sees the auto-started daemon as a "zombie" because its parent (this
470
+ // CLI process) has already exited. SYNCHRONOUS — must complete before any
471
+ // concurrent doctor scan runs the registry read.
472
+ if (child.pid) {
473
+ try {
474
+ registerBackgroundPid(projectRoot, child.pid, 'daemon', spawnArgs.slice(1).join(' '));
475
+ }
476
+ catch {
477
+ // Registration is non-essential — daemon still works, just not visible
478
+ // to PM-aware tooling. Stay silent to keep maybeAutoStartDaemon a
479
+ // fire-and-forget helper.
480
+ }
481
+ }
464
482
  }
465
483
  /**
466
484
  * Load configuration file
@@ -498,8 +498,13 @@ function generateHooks(root, force, answers) {
498
498
  "hooks": [{ "type": "command", "command": gateHook('record-skill-run'), "timeout": 2000 }]
499
499
  },
500
500
  {
501
+ // Use gateHook (not gate) so the wrapper forwards Claude Code's session_id as
502
+ // HOOK_SESSION_ID — record-memory-searched needs this to mark the per-actor map
503
+ // (memorySearchedBy[sid]) that check-before-read consults under #838's per-actor gating.
504
+ // Without it, the legacy boolean is set but the per-actor map stays empty, and the gate
505
+ // blocks every Read forever within the turn (issue #879).
501
506
  "matcher": "mcp__moflo__memory_",
502
- "hooks": [{ "type": "command", "command": gate('record-memory-searched'), "timeout": 3000 }]
507
+ "hooks": [{ "type": "command", "command": gateHook('record-memory-searched'), "timeout": 3000 }]
503
508
  },
504
509
  {
505
510
  "matcher": "^mcp__moflo__memory_store$",
@@ -511,6 +516,14 @@ function generateHooks(root, force, answers) {
511
516
  "hooks": [
512
517
  { "type": "command", "command": `node "$CLAUDE_PROJECT_DIR/.claude/helpers/prompt-hook.mjs"`, "timeout": 3000 }
513
518
  ]
519
+ },
520
+ {
521
+ // prompt-reminder is REQUIRED to reset memorySearched/memorySearchedBy on each
522
+ // new prompt and reclassify memoryRequired. Without it, gate state leaks across
523
+ // prompts. Separate hook entry so a prompt-hook.mjs exception doesn't skip the reset.
524
+ "hooks": [
525
+ { "type": "command", "command": gateHook('prompt-reminder'), "timeout": 3000 }
526
+ ]
514
527
  }
515
528
  ],
516
529
  "SubagentStart": [
@@ -270,9 +270,14 @@ function generateHooksConfig(config) {
270
270
  hooks: [{ type: 'command', command: gateHookCmd('record-skill-run'), timeout: 2000 }],
271
271
  },
272
272
  {
273
- // Simplified matcher — anchored regex with parens doesn't match MCP tool names reliably
273
+ // Simplified matcher — anchored regex with parens doesn't match MCP tool names reliably.
274
+ // Use gateHookCmd (not gateCmd) so the wrapper forwards Claude Code's session_id as
275
+ // HOOK_SESSION_ID — record-memory-searched needs this to mark the per-actor map
276
+ // (memorySearchedBy[sid]) that check-before-read consults under #838's per-actor gating.
277
+ // Without it, the legacy boolean is set but the per-actor map stays empty, and the gate
278
+ // blocks every Read forever within the turn (issue #879).
274
279
  matcher: 'mcp__moflo__memory_',
275
- hooks: [{ type: 'command', command: gateCmd('record-memory-searched'), timeout: 3000 }],
280
+ hooks: [{ type: 'command', command: gateHookCmd('record-memory-searched'), timeout: 3000 }],
276
281
  },
277
282
  {
278
283
  matcher: '^TaskUpdate$',
@@ -284,7 +289,12 @@ function generateHooksConfig(config) {
284
289
  },
285
290
  ];
286
291
  }
287
- // UserPromptSubmit — gate reminders + intelligent task routing
292
+ // UserPromptSubmit — gate reminders + intelligent task routing.
293
+ // The prompt-reminder hook is REQUIRED — it resets memorySearched / memorySearchedBy
294
+ // and reclassifies memoryRequired from the new prompt. Without it, gate state leaks
295
+ // across prompts: a previous turn's "satisfied" state survives, OR a previous turn's
296
+ // "armed" state never clears. Two separate hook entries (not chained) so an exception
297
+ // in prompt-hook.mjs doesn't skip the reset.
288
298
  if (config.userPromptSubmit) {
289
299
  hooks.UserPromptSubmit = [
290
300
  {
@@ -292,6 +302,11 @@ function generateHooksConfig(config) {
292
302
  { type: 'command', command: promptHookCmd(), timeout: 3000 },
293
303
  ],
294
304
  },
305
+ {
306
+ hooks: [
307
+ { type: 'command', command: gateHookCmd('prompt-reminder'), timeout: 3000 },
308
+ ],
309
+ },
295
310
  ];
296
311
  }
297
312
  // SubagentStart — inject directive for subagents to read guidance protocol
@@ -12,6 +12,7 @@ import { spawn } from 'child_process';
12
12
  import { getDaemonLockHolder } from './daemon-lock.js';
13
13
  import { isDaemonInstalled, installDaemonService } from './daemon-service.js';
14
14
  import { locateMofloCliBin } from './moflo-require.js';
15
+ import { registerBackgroundPid } from './process-registry.js';
15
16
  /**
16
17
  * Ensure the daemon is ready for scheduled spell execution.
17
18
  *
@@ -115,6 +116,17 @@ async function defaultStartDaemon(projectRoot) {
115
116
  env: daemonEnv,
116
117
  });
117
118
  child.unref();
119
+ // Register the spawned daemon PID with the shared ProcessManager (parity
120
+ // with src/cli/index.ts maybeAutoStartDaemon). Without this, doctor's
121
+ // zombie scan flags this detached process as orphaned because its parent
122
+ // (this CLI invocation) exits as soon as ensureDaemonForScheduling
123
+ // resolves.
124
+ if (child.pid) {
125
+ try {
126
+ registerBackgroundPid(projectRoot, child.pid, 'daemon', spawnArgs.slice(1).join(' '));
127
+ }
128
+ catch { /* registration is non-essential */ }
129
+ }
118
130
  // Poll for daemon lock acquisition (up to 2s, checking every 200ms)
119
131
  for (let i = 0; i < 10; i++) {
120
132
  await new Promise(r => setTimeout(r, 200));
@@ -38,7 +38,10 @@ export const HOOK_ENTRY_MAP = {
38
38
  'check-dangerous-command': { event: 'PreToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-dangerous-command', timeout: 2000 } },
39
39
  'check-before-pr': { event: 'PreToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-pr', timeout: 2000 } },
40
40
  'record-task-created': { event: 'PostToolUse', matcher: '^TaskCreate$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-task-created', timeout: 2000 } },
41
- 'record-memory-searched': { event: 'PostToolUse', matcher: 'mcp__moflo__memory_', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-memory-searched', timeout: 3000 } },
41
+ // record-memory-searched MUST go through gate-hook.mjs (not gate.cjs directly)
42
+ // — the wrapper forwards Claude Code's session_id as HOOK_SESSION_ID, which
43
+ // markMemorySearched needs to stamp the per-actor map (#879).
44
+ 'record-memory-searched': { event: 'PostToolUse', matcher: 'mcp__moflo__memory_', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-memory-searched', timeout: 3000 } },
42
45
  'check-task-transition': { event: 'PostToolUse', matcher: '^TaskUpdate$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" check-task-transition', timeout: 2000 } },
43
46
  'record-learnings-stored': { event: 'PostToolUse', matcher: '^mcp__moflo__memory_store$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-learnings-stored', timeout: 2000 } },
44
47
  'check-bash-memory': { event: 'PostToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory', timeout: 2000 } },
@@ -84,4 +87,54 @@ export function repairHookWiring(settings) {
84
87
  settings.hooks = hooks;
85
88
  return { settings, repaired };
86
89
  }
90
+ export const HOOK_REWRITE_RULES = [
91
+ // Issue #879 — record-memory-searched MUST use gate-hook.mjs so Claude Code's
92
+ // session_id is forwarded as HOOK_SESSION_ID. Without it, the per-actor map
93
+ // stays empty and the gate blocks every Read forever within a turn.
94
+ {
95
+ name: '#879: record-memory-searched → gate-hook.mjs',
96
+ from: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-memory-searched',
97
+ to: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-memory-searched',
98
+ },
99
+ // Symmetry hardening — same fix shape for check-bash-memory. The shipped
100
+ // settings.json already wires this through gate-hook.mjs in current versions,
101
+ // but a stale consumer settings.json may have it wrong.
102
+ {
103
+ name: '#879: check-bash-memory → gate-hook.mjs',
104
+ from: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" check-bash-memory',
105
+ to: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory',
106
+ },
107
+ ];
108
+ /**
109
+ * Apply HOOK_REWRITE_RULES to every hook command in `settings.hooks.*`.
110
+ * Idempotent: a hook already at the `to` form won't match `from`.
111
+ *
112
+ * @returns The (potentially mutated) settings and a list of rewrites that fired.
113
+ */
114
+ export function rewriteIncorrectHookWiring(settings) {
115
+ const hooks = (settings.hooks ?? {});
116
+ const rewrites = [];
117
+ for (const rule of HOOK_REWRITE_RULES) {
118
+ let count = 0;
119
+ for (const eventName of Object.keys(hooks)) {
120
+ const eventArray = hooks[eventName];
121
+ if (!Array.isArray(eventArray))
122
+ continue;
123
+ for (const block of eventArray) {
124
+ const blockHooks = block.hooks;
125
+ if (!Array.isArray(blockHooks))
126
+ continue;
127
+ for (const h of blockHooks) {
128
+ if (typeof h.command === 'string' && h.command.includes(rule.from)) {
129
+ h.command = h.command.split(rule.from).join(rule.to);
130
+ count++;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ if (count > 0)
136
+ rewrites.push({ name: rule.name, count });
137
+ }
138
+ return { settings, rewrites };
139
+ }
87
140
  //# sourceMappingURL=hook-wiring.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared write side of the moflo background-process registry.
3
+ *
4
+ * Mirrors `bin/lib/process-manager.mjs`'s atomic write logic so TS spawn
5
+ * sites (auto-start daemon in src/cli/index.ts and daemon-readiness.ts) can
6
+ * register their PIDs alongside the ones written by bin/hooks.mjs's
7
+ * spawnWindowless helper. Without registry parity, doctor's zombie scan
8
+ * (src/cli/commands/doctor.ts) flags every TS-spawned background process as
9
+ * an orphan, because they are detached:true so their immediate parent dies.
10
+ *
11
+ * The .mjs module remains the canonical reader/writer with full read+killAll
12
+ * semantics; this TS helper only covers the registration we need from compiled
13
+ * paths. Both write to the same JSON file (`<projectRoot>/.moflo/background-pids.json`),
14
+ * so a process registered here is reapable via pm.killAll() at session-end.
15
+ */
16
+ import { join } from 'path';
17
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
18
+ const REGISTRY_FILENAME = 'background-pids.json';
19
+ function registryPath(projectRoot) {
20
+ return join(projectRoot, '.moflo', REGISTRY_FILENAME);
21
+ }
22
+ function readRegistry(projectRoot) {
23
+ const path = registryPath(projectRoot);
24
+ if (!existsSync(path))
25
+ return [];
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
28
+ return Array.isArray(parsed) ? parsed : [];
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ function writeRegistry(projectRoot, entries) {
35
+ const path = registryPath(projectRoot);
36
+ mkdirSync(join(projectRoot, '.moflo'), { recursive: true });
37
+ const tmp = `${path}.tmp.${process.pid}`;
38
+ writeFileSync(tmp, JSON.stringify(entries, null, 2));
39
+ renameSync(tmp, path);
40
+ }
41
+ /**
42
+ * Register a spawned background PID under the given label. Replaces any
43
+ * pre-existing entry with the same label so a stale dead-PID row doesn't
44
+ * accumulate when a daemon crashes and is restarted.
45
+ */
46
+ export function registerBackgroundPid(projectRoot, pid, label, cmd) {
47
+ if (!Number.isInteger(pid) || pid <= 0)
48
+ return;
49
+ const fresh = readRegistry(projectRoot).filter(e => e && e.label !== label);
50
+ fresh.push({
51
+ pid,
52
+ label,
53
+ cmd: cmd.slice(0, 200),
54
+ startedAt: new Date().toISOString(),
55
+ });
56
+ writeRegistry(projectRoot, fresh);
57
+ }
58
+ //# sourceMappingURL=process-registry.js.map
@@ -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.10';
5
+ export const VERSION = '4.9.11';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.10",
3
+ "version": "4.9.11",
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.9",
84
+ "moflo": "^4.9.10",
85
85
  "tsx": "^4.21.0",
86
86
  "typescript": "^5.9.3",
87
87
  "vitest": "^4.0.0"