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.
- package/.claude/commands/simplify.md +78 -30
- 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/simplify/SKILL.md +90 -21
- 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 +164 -11
- package/dist/src/cli/commands/doctor-checks-deep.js +62 -0
- package/dist/src/cli/commands/doctor.js +34 -1
- package/dist/src/cli/config/moflo-config.js +14 -3
- package/dist/src/cli/index.js +18 -0
- package/dist/src/cli/init/moflo-init.js +19 -4
- 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-block-hash.js +320 -0
- package/dist/src/cli/services/hook-wiring.js +54 -1
- package/dist/src/cli/services/index.js +2 -0
- package/dist/src/cli/services/process-registry.js +58 -0
- package/dist/src/cli/version.js +1 -1
- 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
|
-
- ≤
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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
|
|
842
|
-
//
|
|
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
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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/
|