moflo 4.9.10 → 4.9.12

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 (37) hide show
  1. package/.claude/commands/simplify.md +78 -30
  2. package/.claude/guidance/shipped/moflo-cli-reference.md +201 -0
  3. package/.claude/guidance/shipped/moflo-core-guidance.md +30 -391
  4. package/.claude/guidance/shipped/moflo-cross-platform.md +20 -1
  5. package/.claude/guidance/shipped/moflo-guidance-rules.md +144 -0
  6. package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -0
  7. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +33 -6
  8. package/.claude/guidance/shipped/moflo-session-start.md +154 -0
  9. package/.claude/guidance/shipped/moflo-settings-injection.md +124 -0
  10. package/.claude/guidance/shipped/moflo-source-hygiene.md +1 -1
  11. package/.claude/guidance/shipped/moflo-spell-custom-steps.md +126 -0
  12. package/.claude/guidance/shipped/moflo-spell-engine.md +4 -101
  13. package/.claude/guidance/shipped/moflo-subagents.md +10 -0
  14. package/.claude/guidance/shipped/moflo-task-icons.md +9 -0
  15. package/.claude/guidance/shipped/moflo-user-facing-language.md +8 -0
  16. package/.claude/guidance/shipped/moflo-yaml-reference.md +191 -0
  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 +90 -21
  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/session-start-launcher.mjs +164 -11
  25. package/dist/src/cli/commands/doctor-checks-deep.js +62 -0
  26. package/dist/src/cli/commands/doctor.js +34 -1
  27. package/dist/src/cli/config/moflo-config.js +14 -3
  28. package/dist/src/cli/index.js +18 -0
  29. package/dist/src/cli/init/moflo-init.js +19 -4
  30. package/dist/src/cli/init/settings-generator.js +18 -3
  31. package/dist/src/cli/services/daemon-readiness.js +12 -0
  32. package/dist/src/cli/services/hook-block-hash.js +320 -0
  33. package/dist/src/cli/services/hook-wiring.js +54 -1
  34. package/dist/src/cli/services/index.js +2 -0
  35. package/dist/src/cli/services/process-registry.js +58 -0
  36. package/dist/src/cli/version.js +1 -1
  37. package/package.json +2 -2
@@ -5,7 +5,7 @@ description: Review changed code for reuse, quality, and efficiency, then fix an
5
5
 
6
6
  # /simplify — Adaptive Code Review
7
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.
8
+ Review changed code for reuse opportunities, quality issues, and efficiency improvements. **Effort scales with diff size and reuses prior context** — a 5-line comment trim doesn't get the same treatment as a 500-line refactor, and a re-run after fixing pass-1 findings doesn't re-pay for a fresh fan-out.
9
9
 
10
10
  ## Phase 1: Identify changes
11
11
 
@@ -13,9 +13,11 @@ Run `git diff HEAD` (working tree) and `git diff main...HEAD` (committed) to get
13
13
 
14
14
  Treat the union of staged + unstaged + committed-since-base as the diff to review.
15
15
 
16
+ Also note: was `/simplify` already run on this branch in this session? If yes, you're in a **validation pass** (Phase 2.5 below) — most of the heavy lifting is done.
17
+
16
18
  ## Phase 2: Classify the diff
17
19
 
18
- Pick the **smallest tier** the diff genuinely fits. When in doubt, escalate.
20
+ Pick the **smallest tier** the diff genuinely fits. When in doubt, escalate one step (not two).
19
21
 
20
22
  ### TRIVIAL — self-review, no agent spawn
21
23
  ALL of these must hold:
@@ -28,38 +30,103 @@ ALL of these must hold:
28
30
  Examples that qualify: trimming a comment, fixing a typo in a log message, renaming a private helper, reformatting a single block.
29
31
  Examples that DON'T qualify: changing an `if` condition, reordering function args, deleting a try/catch.
30
32
 
31
- ### SMALL — single agent, all three categories
33
+ ### SMALL — single agent, all three categories (DEFAULT for most diffs)
32
34
  ALL of these must hold:
33
- - ≤50 net LOC changed
35
+ - ≤200 net LOC changed
34
36
  - ≤2 files
35
37
  - No structural changes (no new modules, no API additions/removals, no contract changes)
36
38
 
37
- Examples that qualify: extracting a constant, inlining a one-liner, swapping a `for` for a `forEach`, adding one early-return.
39
+ This is the default tier for **most real diffs**, including changes to critical surface (launcher, hooks, MCP wiring). Critical surface raises the *care* of the agent prompt (sharper checklist, blast-radius framing), not the *number* of agents.
40
+
41
+ Examples that qualify: extracting a constant, inlining a one-liner, swapping a `for` for a `forEach`, adding one early-return, refactoring a single function within a file, adding a cache fast-path inside an existing block.
38
42
 
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
43
+ ### NORMAL — three parallel agents
44
+ Reserved for **genuinely cross-cutting** changes. ANY of these triggers NORMAL:
45
+ - 3+ files changed
46
+ - >200 net LOC changed
42
47
  - Adds/removes/renames a public API
43
- - Changes control flow in a non-trivial way
44
48
  - Introduces or removes a dependency
45
- - Touches `bin/`, hooks, MCP tool handlers, or anything called out in `CLAUDE.md` as critical surface
49
+ - Cross-cutting refactor (touches the same pattern in multiple modules)
50
+
51
+ Three agents exist to cover orthogonal axes (Reuse / Quality / Efficiency) when the change is broad enough that one agent's tool-call budget can't survey it all. For single-file edits, one focused agent always covers all three axes — three is duplication, not coverage.
52
+
53
+ ## Phase 2.5: Validation pass (re-run after fixes)
54
+
55
+ If `/simplify` already ran on this branch in this session AND the only edits since are fixes driven by the prior pass's findings, default to **self-review tier** regardless of LOC count. The fan-out already happened; the fix is small relative to the diff that was already reviewed.
56
+
57
+ Escalate one tier (self-review → SMALL agent) only if the fix introduced any of:
58
+ - A new file
59
+ - A new exported symbol
60
+ - A new dependency or import from a previously-untouched module
61
+ - A change to control flow not covered in the original findings
62
+
63
+ Do **not** escalate to NORMAL on a validation pass. If the fix is so structural that NORMAL is warranted, treat it as a fresh diff and start over from Phase 1.
64
+
65
+ ## Phase 2.7: Route the model (before any Agent spawn)
66
+
67
+ For every tier that spawns an Agent (SMALL / NORMAL — TRIVIAL self-review skips this), call the moflo router to pick the cheapest model that fits the task **before** invoking Agent:
68
+
69
+ ```
70
+ mcp__moflo__hooks_model-route — {
71
+ task: "<diff summary — see wording rules below>",
72
+ preferCost: true
73
+ }
74
+ ```
75
+
76
+ ### Wording the task description
77
+
78
+ The router's complexity score is keyword-sensitive. Words like `refactor`, `architect`, `audit`, `system`, `redesign`, `migrate` flip a high-complexity flag and force opus *even when scoring suggests sonnet*. For `/simplify` you are **always doing code review**, never genuine architecture, so frame the task accordingly:
79
+
80
+ - ✅ Good: `"Review 110-line single-file change in bin/session-start-launcher.mjs for reuse, quality, efficiency."`
81
+ - ❌ Bad: `"Review refactor that adds mtime-cache fast-path and architects new caching layer."`
46
82
 
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.
83
+ Drop the trigger words. State LOC count, file count, and "review for reuse, quality, efficiency". That's enough signal.
84
+
85
+ ### Applying the result
86
+
87
+ The router returns `{ model: 'haiku' | 'sonnet' | 'opus', complexity, reasoning, alternatives, ... }`.
88
+
89
+ **Hard rule for `/simplify`: opus is never correct.** Code review does not require Opus-tier reasoning even on critical surface. If the router returns `opus`:
90
+
91
+ 1. Look at `alternatives` — if `sonnet` scores higher than the selected model's confidence, downgrade to sonnet.
92
+ 2. Otherwise, downgrade to sonnet anyway (treat opus as "router was uncertain — pick the safer middle").
93
+
94
+ Pass the final model verbatim to the Agent's `model` parameter (Agent accepts `'haiku' | 'sonnet' | 'opus'`). On router failure (MCP call errors), default to `'sonnet'`.
95
+
96
+ In practice: comment trims and pure formatting → haiku; everything else for `/simplify` → sonnet.
97
+
98
+ ### Feed back the outcome
99
+
100
+ After the agent completes, record the outcome so the router learns:
101
+
102
+ ```
103
+ mcp__moflo__hooks_model-outcome — { task: "<same wording as route call>", model: "<chosen>", outcome: "success" | "failure" | "escalated" }
104
+ ```
105
+
106
+ `escalated` = the agent missed something a higher-tier pass would have caught. That signal teaches the router to bias similar tasks upward next time. Don't fake `escalated` to retroactively justify opus — only record it when a *real* miss happened.
48
107
 
49
108
  ## Phase 3: Run the appropriate review
50
109
 
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.
110
+ ### TRIVIAL / Validation: self-review
111
+ Run the same three category checks (reuse / quality / efficiency) yourself, in one pass, against the diff. Most TRIVIAL and validation 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. No router call needed.
53
112
 
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.
113
+ ### SMALL: one agent (model from router)
114
+ Launch a SINGLE Agent with subagent_type `reviewer`, passing the model returned by Phase 2.7's router call. Cap the agent's tool budget by being explicit:
56
115
 
57
116
  ```
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."
117
+ Agent — {
118
+ subagent_type: "reviewer",
119
+ model: "<from router, typically 'sonnet'>",
120
+ prompt: "Review this diff for reuse, quality, and efficiency. <diff inline>. Flag specific issues as file:line + 1-line description. Max 5 file reads. Under 200 words. Skip cosmetic style. Don't suggest cross-cutting refactors of code outside this diff."
121
+ }
59
122
  ```
60
123
 
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.
124
+ For critical-surface files, prepend a 1-line risk note to the prompt (e.g., "This is `bin/session-start-launcher.mjs` — runs in every consumer's session-start hot path; cross-platform + blast-radius matter."). One careful agent, not three.
125
+
126
+ Budget: ~1 minute.
127
+
128
+ ### NORMAL: three parallel agents (model from router, applied to all)
129
+ Launch three agents in a single message — Reuse, Quality, Efficiency — passing the full diff and the same routed `model` to each. Each agent gets the same tool-budget cap as SMALL.
63
130
 
64
131
  **Reuse**: existing helpers/utilities that should be used instead; duplicated patterns; new functions that re-implement something already in the codebase.
65
132
 
@@ -69,14 +136,16 @@ Launch three agents in a single message — Reuse, Quality, Efficiency — passi
69
136
 
70
137
  ## Phase 4: Fix or skip
71
138
 
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.
139
+ Aggregate findings. Fix each one directly. False positives or not-worth-fixing — note and skip without arguing. If self-review found nothing, just confirm clean and exit.
73
140
 
74
141
  If fixes were made, re-run tests to confirm nothing broke. If tests fail after a fix, revert it.
75
142
 
143
+ After fixes: the next `/simplify` invocation is a **validation pass** (Phase 2.5). Do not re-fan-out unless the fix added genuinely new concerns — bundle related fixes into one batch so a single validation pass covers them.
144
+
76
145
  ## Phase 5: Stamp the gate
77
146
 
78
- Whatever tier ran, the gate (`check-before-pr`) registers /simplify as having executed. The skill is satisfied.
147
+ Whatever tier ran, the gate (`check-before-pr`) registers /simplify as having executed. The skill is satisfied. Self-review counts.
79
148
 
80
149
  ## Briefly summarize
81
150
 
82
- End with one or two sentences: which tier, what was fixed (or "clean — no changes"). No headers, no bullets unless needed.
151
+ End with one or two sentences: which tier ran, 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
@@ -10,7 +10,7 @@
10
10
  import { spawn, execFileSync } from 'child_process';
11
11
  import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, readdirSync, mkdirSync, statSync } from 'fs';
12
12
  import { resolve, dirname, join } from 'path';
13
- import { fileURLToPath } from 'url';
13
+ import { fileURLToPath, pathToFileURL } from 'url';
14
14
  import { mofloDir } from './lib/moflo-paths.mjs';
15
15
  import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
16
16
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
@@ -218,11 +218,17 @@ try {
218
218
  // own errors if the DB is still broken.
219
219
  }
220
220
 
221
- // ── 0d. Clear post-install restart notice when version is current (#867) ───
221
+ // ── 0d. Silently clear post-install restart notice when version is current (#867, #887)
222
222
  // scripts/post-install-notice.mjs drops `.moflo/restart-pending.json` on every
223
223
  // `npm install moflo`. The UserPromptSubmit hook surfaces it on every prompt
224
224
  // until cleared, so this session only sees the message between install and
225
225
  // the FIRST restart that actually picks up the new bits.
226
+ //
227
+ // Cleanup is silent (#887): the user already saw + acted on the restart prompt
228
+ // — surfacing a "cleared notice" line on the very next session reads like an
229
+ // error in additionalContext and inflates mutationCount, which would also fire
230
+ // the closing "starting background tasks" framing. Both are noise on a
231
+ // successful post-restart session.
226
232
  try {
227
233
  const pendingPath = join(mofloDir(projectRoot), 'restart-pending.json');
228
234
  const pkgPath = resolve(projectRoot, 'node_modules/moflo/package.json');
@@ -231,7 +237,6 @@ try {
231
237
  if (pending && typeof pending.version === 'string' && pending.version === installedVersion) {
232
238
  unlinkSync(pendingPath);
233
239
  try { unlinkSync(join(mofloDir(projectRoot), 'last-install-banner.json')); } catch { /* tracker may not exist */ }
234
- emitMutation('cleared post-install restart notice', `${installedVersion} now running`);
235
240
  }
236
241
  } catch { /* file missing or malformed — silent fast-path */ }
237
242
 
@@ -304,7 +309,7 @@ try {
304
309
  // Controlled by `auto_update.enabled` in moflo.yaml (default: true).
305
310
  // When moflo is upgraded (npm install), scripts and helpers may be stale.
306
311
  // Detect version change and sync from source before running hooks.
307
- let autoUpdateConfig = { enabled: true, scripts: true, helpers: true };
312
+ let autoUpdateConfig = { enabled: true, scripts: true, helpers: true, hookBlockDrift: 'warn' };
308
313
  try {
309
314
  const mofloYaml = resolve(projectRoot, 'moflo.yaml');
310
315
  if (existsSync(mofloYaml)) {
@@ -313,9 +318,12 @@ try {
313
318
  const enabledMatch = yamlContent.match(/auto_update:\s*\n\s+enabled:\s*(true|false)/);
314
319
  const scriptsMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+scripts:\s*(true|false)/);
315
320
  const helpersMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+helpers:\s*(true|false)/);
321
+ // #881: hook-block drift detector (warn | regenerate | off; default warn)
322
+ const driftMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+hook_block_drift:\s*(warn|regenerate|off)/);
316
323
  if (enabledMatch) autoUpdateConfig.enabled = enabledMatch[1] === 'true';
317
324
  if (scriptsMatch) autoUpdateConfig.scripts = scriptsMatch[1] === 'true';
318
325
  if (helpersMatch) autoUpdateConfig.helpers = helpersMatch[1] === 'true';
326
+ if (driftMatch) autoUpdateConfig.hookBlockDrift = driftMatch[1];
319
327
  }
320
328
  } catch (err) {
321
329
  // Defaults (all true) keep the upgrade flow alive but the user should
@@ -838,8 +846,11 @@ try {
838
846
  settingsChanges.push('added statusLine');
839
847
  }
840
848
 
841
- // 3a-iv. Repair missing required hook wirings (same logic as doctor --fix
842
- // and moflo upgradeshared via hook-wiring.js to stay DRY)
849
+ // 3a-iv. Repair missing required hook wirings AND rewrite known-bad
850
+ // wirings from older moflo versions (#879 record-memory-searched
851
+ // wired to gate.cjs directly skips session_id forwarding and deadlocks
852
+ // the per-actor gate). Both passes share hook-wiring.js so doctor --fix,
853
+ // upgrade-merge, and the launcher stay DRY.
843
854
  try {
844
855
  const hwPaths = [
845
856
  resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/hook-wiring.js'),
@@ -847,11 +858,21 @@ try {
847
858
  ];
848
859
  const hwPath = hwPaths.find(p => existsSync(p));
849
860
  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')}`);
861
+ const mod = await import(`file://${hwPath.replace(/\\/g, '/')}`);
862
+ if (typeof mod.rewriteIncorrectHookWiring === 'function') {
863
+ const { rewrites } = mod.rewriteIncorrectHookWiring(settings);
864
+ if (rewrites.length > 0) {
865
+ dirty = true;
866
+ const total = rewrites.reduce((n, r) => n + r.count, 0);
867
+ settingsChanges.push(`rewrote ${plural(total, 'stale hook wiring')}`);
868
+ }
869
+ }
870
+ if (typeof mod.repairHookWiring === 'function') {
871
+ const { repaired } = mod.repairHookWiring(settings);
872
+ if (repaired.length > 0) {
873
+ dirty = true;
874
+ settingsChanges.push(`repaired ${plural(repaired.length, 'hook wiring')}`);
875
+ }
855
876
  }
856
877
  }
857
878
  } catch (err) {
@@ -868,6 +889,138 @@ try {
868
889
  emitWarning(`settings.json migration failed (${errMessage(err)})`);
869
890
  }
870
891
 
892
+ // ── 3a-vi. Hook-block drift detection (#881) ───────────────────────────────
893
+ // Hash the consumer's settings.json hook block against the reference block
894
+ // `generateHooksConfig()` would produce for this moflo version. Catches
895
+ // drift the per-bug `repairHookWiring` / `rewriteIncorrectHookWiring` rules
896
+ // don't cover (future hook events, partial migrations, hand-edited commands).
897
+ // Runs every session under `auto_update.enabled`, not only on version change.
898
+ //
899
+ // Modes (`auto_update.hook_block_drift` in moflo.yaml):
900
+ // warn — print a one-line summary + diff to stdout (default)
901
+ // regenerate — additively add missing hooks; falls back to warn when the
902
+ // consumer has extra (custom) hooks, to avoid clobbering
903
+ // off — skip entirely
904
+ //
905
+ // Also respects a `claudeFlow.hooks.locked: true` sentinel in settings.json
906
+ // — if set, the user has explicitly opted out of drift surfacing.
907
+ // Fast-path: `.moflo/hook-drift-cache.json` records the last clean run. If
908
+ // settings.json + the dist module both still match the cached mtimes and the
909
+ // cached check was clean (consumerHash === referenceHash), skip readFile +
910
+ // JSON.parse + dynamic import entirely. This block runs every session; the
911
+ // cache makes it ~free in the steady state.
912
+ //
913
+ // Returns the values to persist on the slow path, or null when skipped
914
+ // (cache hit, no settings.json, no dist module, locked, etc.). Pulled out
915
+ // to keep the guard chain flat — the original inline form was 9 levels deep.
916
+ async function runHookBlockDriftCheck() {
917
+ const settingsPath = resolve(projectRoot, '.claude', 'settings.json');
918
+ let settingsStat;
919
+ try { settingsStat = statSync(settingsPath); } catch { return null; }
920
+
921
+ // statSync each candidate doubles as existence check + provides the mtime
922
+ // we need for the cache key, avoiding the existsSync→import TOCTOU pattern.
923
+ const hbhCandidates = [
924
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/hook-block-hash.js'),
925
+ resolve(projectRoot, 'dist/src/cli/services/hook-block-hash.js'),
926
+ ];
927
+ let hbhPath = null;
928
+ let hbhStat = null;
929
+ for (const p of hbhCandidates) {
930
+ try { hbhStat = statSync(p); hbhPath = p; break; } catch { /* try next */ }
931
+ }
932
+ if (!hbhPath) return null;
933
+
934
+ // Fast-path requires consumerHash === referenceHash (a previously *clean*
935
+ // run). A drifted-but-cached state still needs to re-emit the warning each
936
+ // session, so we always re-do the work in that case.
937
+ const cachePath = join(mofloDir(projectRoot), 'hook-drift-cache.json');
938
+ let cached = null;
939
+ try { cached = JSON.parse(readFileSync(cachePath, 'utf-8')); } catch { /* missing or corrupt */ }
940
+ if (
941
+ cached &&
942
+ cached.settingsMtimeMs === settingsStat.mtimeMs &&
943
+ cached.moduleMtimeMs === hbhStat.mtimeMs &&
944
+ cached.consumerHash === cached.referenceHash
945
+ ) return null;
946
+
947
+ // Try-catch around the dynamic import handles the file disappearing
948
+ // between statSync and import (TOCTOU); module-load errors fall through.
949
+ let mod = null;
950
+ try { mod = await import(pathToFileURL(hbhPath).href); } catch { /* TOCTOU or load error — skip */ return null; }
951
+ if (typeof mod.computeHookBlockDrift !== 'function') return null;
952
+
953
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
954
+ if (typeof mod.isHookBlockLocked === 'function' && mod.isHookBlockLocked(settings)) return null;
955
+
956
+ const report = mod.computeHookBlockDrift(settings.hooks || {});
957
+ let regenerated = false;
958
+
959
+ if (report.drifted) {
960
+ const wantRegenerate = autoUpdateConfig.hookBlockDrift === 'regenerate';
961
+ const safeToRegenerate = wantRegenerate && report.extra.length === 0;
962
+ if (safeToRegenerate && typeof mod.applyAdditiveRegeneration === 'function') {
963
+ const { added } = mod.applyAdditiveRegeneration(settings, report);
964
+ if (added > 0) {
965
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
966
+ regenerated = true;
967
+ emitMutation(
968
+ 'regenerated hook block',
969
+ `added ${plural(added, 'missing hook entry')} (drift ${report.consumerHash} → ${report.referenceHash})`,
970
+ );
971
+ }
972
+ } else {
973
+ const parts = [];
974
+ if (report.missing.length > 0) parts.push(plural(report.missing.length, 'missing entry'));
975
+ if (report.extra.length > 0) parts.push(`${plural(report.extra.length, 'custom hook')} preserved`);
976
+ const reason = parts.join(', ') || 'reordered';
977
+ // stdout (not stderr) so Claude sees this in `additionalContext` and
978
+ // surfaces it to the user — not a mutation since we didn't change anything.
979
+ try {
980
+ process.stdout.write(
981
+ `moflo: hook block drift (${reason}); run \`flo doctor hook-drift\` or set auto_update.hook_block_drift: regenerate in moflo.yaml\n`,
982
+ );
983
+ } catch { /* broken stdout — non-fatal */ }
984
+ }
985
+ }
986
+
987
+ // Regeneration mutated settings.json — re-stat for the fresh mtime so next
988
+ // session's fast-path matches; otherwise reuse the stat we already have.
989
+ let finalSettingsMtime = settingsStat.mtimeMs;
990
+ if (regenerated) {
991
+ try { finalSettingsMtime = statSync(settingsPath).mtimeMs; } catch { /* keep prior */ }
992
+ }
993
+ // After successful regeneration consumerHash matches referenceHash by construction.
994
+ const finalConsumerHash = regenerated ? report.referenceHash : report.consumerHash;
995
+
996
+ return {
997
+ cachePath,
998
+ settingsMtimeMs: finalSettingsMtime,
999
+ moduleMtimeMs: hbhStat.mtimeMs,
1000
+ consumerHash: finalConsumerHash,
1001
+ referenceHash: report.referenceHash,
1002
+ };
1003
+ }
1004
+
1005
+ try {
1006
+ if (autoUpdateConfig.enabled && autoUpdateConfig.hookBlockDrift !== 'off') {
1007
+ const result = await runHookBlockDriftCheck();
1008
+ if (result) {
1009
+ try {
1010
+ mkdirSync(mofloDir(projectRoot), { recursive: true });
1011
+ writeFileSync(result.cachePath, JSON.stringify({
1012
+ settingsMtimeMs: result.settingsMtimeMs,
1013
+ moduleMtimeMs: result.moduleMtimeMs,
1014
+ consumerHash: result.consumerHash,
1015
+ referenceHash: result.referenceHash,
1016
+ }));
1017
+ } catch { /* cache is opportunistic — non-fatal */ }
1018
+ }
1019
+ }
1020
+ } catch (err) {
1021
+ emitWarning(`hook-block drift check skipped (${errMessage(err)})`);
1022
+ }
1023
+
871
1024
  // ── 3b. Ensure shipped guidance files exist (even without version change) ──
872
1025
  // Subagents need these files on disk for direct reads without memory search.
873
1026
  // Also prunes top-level mirrors whose source no longer exists in shipped/