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
@@ -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.9';
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.9",
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.8",
84
+ "moflo": "^4.9.10",
85
85
  "tsx": "^4.21.0",
86
86
  "typescript": "^5.9.3",
87
87
  "vitest": "^4.0.0"