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.
- package/.claude/guidance/shipped/moflo-cli-reference.md +201 -0
- package/.claude/guidance/shipped/moflo-core-guidance.md +30 -391
- package/.claude/guidance/shipped/moflo-cross-platform.md +20 -1
- package/.claude/guidance/shipped/moflo-guidance-rules.md +144 -0
- package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -0
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +33 -6
- package/.claude/guidance/shipped/moflo-session-start.md +154 -0
- package/.claude/guidance/shipped/moflo-settings-injection.md +124 -0
- package/.claude/guidance/shipped/moflo-source-hygiene.md +1 -1
- package/.claude/guidance/shipped/moflo-spell-custom-steps.md +126 -0
- package/.claude/guidance/shipped/moflo-spell-engine.md +4 -101
- package/.claude/guidance/shipped/moflo-subagents.md +10 -0
- package/.claude/guidance/shipped/moflo-task-icons.md +9 -0
- package/.claude/guidance/shipped/moflo-user-facing-language.md +8 -0
- package/.claude/guidance/shipped/moflo-yaml-reference.md +191 -0
- package/.claude/skills/connector-builder/SKILL.md +1 -1
- package/.claude/skills/guidance/SKILL.md +158 -0
- package/.claude/skills/publish/SKILL.md +16 -0
- package/.claude/skills/spell-builder/SKILL.md +2 -2
- package/.claude/skills/spell-builder/architecture.md +1 -1
- package/.claude/skills/spell-schedule/SKILL.md +167 -0
- package/bin/session-start-launcher.mjs +20 -7
- package/dist/src/cli/commands/doctor.js +30 -0
- package/dist/src/cli/index.js +18 -0
- package/dist/src/cli/init/moflo-init.js +14 -1
- package/dist/src/cli/init/settings-generator.js +18 -3
- package/dist/src/cli/services/daemon-readiness.js +12 -0
- package/dist/src/cli/services/hook-wiring.js +54 -1
- package/dist/src/cli/services/process-registry.js +58 -0
- package/dist/src/cli/version.js +1 -1
- 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
|
|
842
|
-
//
|
|
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
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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.
|
package/dist/src/cli/index.js
CHANGED
|
@@ -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":
|
|
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:
|
|
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
|
-
|
|
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
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.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.
|
|
84
|
+
"moflo": "^4.9.10",
|
|
85
85
|
"tsx": "^4.21.0",
|
|
86
86
|
"typescript": "^5.9.3",
|
|
87
87
|
"vitest": "^4.0.0"
|