moflo 4.10.26 → 4.10.27
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-yaml-reference.md +3 -2
- package/LICENSE +21 -21
- package/README.md +8 -0
- package/bin/build-embeddings.mjs +0 -0
- package/bin/gate-hook.mjs +0 -0
- package/bin/gate.cjs +0 -0
- package/bin/generate-code-map.mjs +0 -0
- package/bin/hook-handler.cjs +0 -0
- package/bin/hooks.mjs +0 -0
- package/bin/index-all.mjs +0 -0
- package/bin/index-guidance.mjs +0 -0
- package/bin/index-patterns.mjs +0 -0
- package/bin/index-tests.mjs +0 -0
- package/bin/lib/moflo-resolve.mjs +0 -0
- package/bin/lib/process-manager.mjs +0 -0
- package/bin/lib/registry-cleanup.cjs +0 -0
- package/bin/npx-repair.js +0 -0
- package/bin/npx-safe-launch.js +0 -0
- package/bin/prompt-hook.mjs +0 -0
- package/bin/semantic-search.mjs +0 -0
- package/bin/session-start-launcher.mjs +16 -2
- package/bin/setup-project.mjs +0 -0
- package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
- package/dist/src/cli/init/moflo-init.js +95 -161
- package/dist/src/cli/spells/commands/composite-command.js +13 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -117,7 +117,7 @@ auto_update:
|
|
|
117
117
|
enabled: true # Master toggle for version-change auto-sync
|
|
118
118
|
scripts: true # Sync .claude/scripts/ from moflo bin/
|
|
119
119
|
helpers: true # Sync .claude/helpers/ from moflo source
|
|
120
|
-
hook_block_drift:
|
|
120
|
+
hook_block_drift: regenerate # warn | regenerate | off (default: regenerate since #1227)
|
|
121
121
|
claudemd_injection_drift: regenerate # warn | regenerate | off
|
|
122
122
|
```
|
|
123
123
|
|
|
@@ -150,7 +150,8 @@ If your `moflo.yaml` predates the `sandbox:` or `auto_update:` blocks, they are
|
|
|
150
150
|
| `sandbox.tier: full` | Require OS sandbox; throw at runtime if the platform tool is unavailable |
|
|
151
151
|
| `sandbox.tier: denylist-only` | Keep Layer 1 denylist only; skip OS isolation even when enabled |
|
|
152
152
|
| `auto_update.enabled: false` | Disable all on-session auto-sync (scripts, helpers, drift checks) |
|
|
153
|
-
| `auto_update.hook_block_drift: regenerate` | Auto-repair drift in `.claude/settings.json` hook block on session start (#881) |
|
|
153
|
+
| `auto_update.hook_block_drift: regenerate` | Auto-repair drift in `.claude/settings.json` hook block on session start (#881, default since #1227 — basename guard from #1180 keeps user-owned hooks safe). |
|
|
154
|
+
| `auto_update.hook_block_drift: warn` | Print drift notice but leave settings.json unchanged. Opt-out from auto-regen. |
|
|
154
155
|
| `auto_update.hook_block_drift: off` | Skip hook-block drift detection entirely |
|
|
155
156
|
| `auto_update.claudemd_injection_drift: regenerate` | Auto-refresh the MoFlo block in `CLAUDE.md` when it drifts from the current generator (#1142, default) |
|
|
156
157
|
| `auto_update.claudemd_injection_drift: warn` | Print a drift notice on session start but leave `CLAUDE.md` unchanged |
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024-2026 ruvnet
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 ruvnet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -120,6 +120,14 @@ To force a clean re-initialization over an existing setup:
|
|
|
120
120
|
flo init --force
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
#### First-run heads-up: brief CPU spike during initial indexing
|
|
124
|
+
|
|
125
|
+
The **very first time** MoFlo runs in your project — typically right after `flo init` and the next session start — it builds the initial guidance, code map, and test indexes from scratch and generates 384-dim embeddings for every chunk. On a medium-sized project this takes **one to a few minutes** and you may see elevated CPU during that window. It runs in the background, so you can start working immediately; you just might hear your fans for a bit.
|
|
126
|
+
|
|
127
|
+
After that first pass, indexing is **incremental and lazy** — every subsequent session start only re-processes files that actually changed since the last run, which typically finishes in under a second with no perceptible CPU activity. The big one-time cost is a deliberate trade: pay it once, then every future session opens with your project already searchable by meaning.
|
|
128
|
+
|
|
129
|
+
> **Tip:** Let that initial indexing finish before running **`/healer`** (or `flo healer`). Healer's embeddings and semantic-quality checks expect the indexes to exist — if you run it mid-build it may flag warnings that resolve themselves the moment indexing completes.
|
|
130
|
+
|
|
123
131
|
### 2. Review your guidance and code settings
|
|
124
132
|
|
|
125
133
|
Open `moflo.yaml` to see what init detected. The two key sections:
|
package/bin/build-embeddings.mjs
CHANGED
|
File without changes
|
package/bin/gate-hook.mjs
CHANGED
|
File without changes
|
package/bin/gate.cjs
CHANGED
|
File without changes
|
|
File without changes
|
package/bin/hook-handler.cjs
CHANGED
|
File without changes
|
package/bin/hooks.mjs
CHANGED
|
File without changes
|
package/bin/index-all.mjs
CHANGED
|
File without changes
|
package/bin/index-guidance.mjs
CHANGED
|
File without changes
|
package/bin/index-patterns.mjs
CHANGED
|
File without changes
|
package/bin/index-tests.mjs
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/bin/npx-repair.js
CHANGED
|
File without changes
|
package/bin/npx-safe-launch.js
CHANGED
|
File without changes
|
package/bin/prompt-hook.mjs
CHANGED
|
File without changes
|
package/bin/semantic-search.mjs
CHANGED
|
File without changes
|
|
@@ -812,7 +812,18 @@ let autoUpdateConfig = {
|
|
|
812
812
|
enabled: true,
|
|
813
813
|
scripts: true,
|
|
814
814
|
helpers: true,
|
|
815
|
-
|
|
815
|
+
// #1227 — default flipped from 'warn' → 'regenerate'. After #1180 added the
|
|
816
|
+
// basename guard in applyWholesaleRegeneration, the wholesale path stopped
|
|
817
|
+
// clobbering user-owned entries (commands not pointing at moflo helpers are
|
|
818
|
+
// grafted back into the fresh tree). The only thing left to "lose" is stale
|
|
819
|
+
// moflo entries from prior versions — exactly what consumers WANT migrated.
|
|
820
|
+
// The 'warn' default was a holdover from the pre-#1180 era when the wipe
|
|
821
|
+
// wasn't safe; that rationale no longer applies, and the launcher silently
|
|
822
|
+
// leaving legacy shapes in place was the root of #1227's surprise deletions
|
|
823
|
+
// (`flo init`'s wholesale-wipe path tripped where the launcher's safe wipe
|
|
824
|
+
// hadn't run because of the warn default). Consumers who explicitly want
|
|
825
|
+
// warn-only can still set `auto_update.hook_block_drift: warn` in moflo.yaml.
|
|
826
|
+
hookBlockDrift: 'regenerate',
|
|
816
827
|
// #1142 — CLAUDE.md injection drift refresh mode (warn | regenerate | off,
|
|
817
828
|
// default regenerate). Defaults to regenerate because the consumer cannot
|
|
818
829
|
// refresh CLAUDE.md on their own — there is no other auto-refresh path.
|
|
@@ -826,7 +837,10 @@ try {
|
|
|
826
837
|
const enabledMatch = yamlContent.match(/auto_update:\s*\n\s+enabled:\s*(true|false)/);
|
|
827
838
|
const scriptsMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+scripts:\s*(true|false)/);
|
|
828
839
|
const helpersMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+helpers:\s*(true|false)/);
|
|
829
|
-
// #881: hook-block drift detector (warn | regenerate | off; default
|
|
840
|
+
// #881: hook-block drift detector (warn | regenerate | off; default
|
|
841
|
+
// regenerate post-#1227 — the basename guard from #1180 made wholesale
|
|
842
|
+
// regen safe for user-owned hooks, so the prior 'warn' default just left
|
|
843
|
+
// legacy shapes in place for `flo init` to wipe non-safely.)
|
|
830
844
|
const driftMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+hook_block_drift:\s*(warn|regenerate|off)/);
|
|
831
845
|
// #1142: CLAUDE.md injection drift detector (warn | regenerate | off; default regenerate)
|
|
832
846
|
const claudemdMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+claudemd_injection_drift:\s*(warn|regenerate|off)/);
|
package/bin/setup-project.mjs
CHANGED
|
File without changes
|
|
@@ -667,7 +667,7 @@ export async function checkHookBlockDrift() {
|
|
|
667
667
|
name: 'Hook Block Drift',
|
|
668
668
|
status: 'warn',
|
|
669
669
|
message: parts.join(', '),
|
|
670
|
-
fix: '
|
|
670
|
+
fix: 'session-start auto-regenerates by default (#1227); next Claude Code start should heal this. If it persists, ensure `auto_update.hook_block_drift` is not set to `warn`/`off` in moflo.yaml, or set `moflo.hooks.locked: true` to suppress.',
|
|
671
671
|
};
|
|
672
672
|
}
|
|
673
673
|
// ============================================================================
|
|
@@ -19,6 +19,9 @@ import { generateClaudeMd as generateMofloSection } from './claudemd-generator.j
|
|
|
19
19
|
import { applyInjectionReplacement } from '../services/claudemd-injection.js';
|
|
20
20
|
import { loadShippedScripts } from './shipped-scripts.js';
|
|
21
21
|
import { DEFAULT_INIT_OPTIONS } from './types.js';
|
|
22
|
+
import { generateSettings } from './settings-generator.js';
|
|
23
|
+
import { applyWholesaleRegeneration, computeHookBlockDrift, isHookBlockLocked, } from '../services/hook-block-hash.js';
|
|
24
|
+
import { rewriteIncorrectHookWiring } from '../services/hook-wiring.js';
|
|
22
25
|
export { discoverTestDirs };
|
|
23
26
|
// ============================================================================
|
|
24
27
|
// Init
|
|
@@ -180,176 +183,107 @@ function generateConfig(root, force, answers) {
|
|
|
180
183
|
// ============================================================================
|
|
181
184
|
// Step 2: .claude/settings.json hooks
|
|
182
185
|
// ============================================================================
|
|
183
|
-
|
|
186
|
+
// #1227 — `flo init` was the surgical patcher that silently nuked user-owned
|
|
187
|
+
// hooks (project-analysis-gate.cjs, e2e-gate.cjs) AND moflo entries in legacy
|
|
188
|
+
// slots (swarm_init/hive-mind_init in PreToolUse, auto-memory-hook in SessionEnd).
|
|
189
|
+
// The old generateHooks had three structural defects:
|
|
190
|
+
// (1) Its "already configured" guard scanned for the literal substring
|
|
191
|
+
// `'flo gate'` / `'moflo gate'` — modern moflo commands are
|
|
192
|
+
// `node ".../helpers/gate.cjs ..."` so the substring NEVER matched and
|
|
193
|
+
// the guard always fell through to the wipe path.
|
|
194
|
+
// (2) `existing.hooks = hooks` was a wholesale overwrite — the comment claimed
|
|
195
|
+
// "preserve existing non-MoFlo hooks" but the code did the opposite.
|
|
196
|
+
// (3) Its inlined canonical block had drifted from settings-generator.ts /
|
|
197
|
+
// hook-block-hash.ts (no swarm_init, no hive-mind_init, no Stop
|
|
198
|
+
// auto-memory-hook, SessionStart launcher timeout 3000 not 5000).
|
|
199
|
+
//
|
|
200
|
+
// New shape: one canonical source. For missing settings.json, write
|
|
201
|
+
// generateSettings(DEFAULT_INIT_OPTIONS). For existing, run the same wholesale
|
|
202
|
+
// regen the session-start launcher uses — applyWholesaleRegeneration preserves
|
|
203
|
+
// user-owned entries via the #1180 basename guard AND relocates moflo entries
|
|
204
|
+
// from legacy slots (SessionEnd → Stop, PreToolUse swarm_init → PostToolUse,
|
|
205
|
+
// etc.). rewriteIncorrectHookWiring runs first so command-string rewrites
|
|
206
|
+
// (#879 / #931) are healed before the structural pass hashes the block.
|
|
207
|
+
function generateHooks(root, force, _answers) {
|
|
184
208
|
const settingsPath = path.join(root, '.claude', 'settings.json');
|
|
185
209
|
const settingsDir = path.dirname(settingsPath);
|
|
186
210
|
if (!fs.existsSync(settingsDir)) {
|
|
187
211
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
188
212
|
}
|
|
189
|
-
|
|
190
|
-
if (fs.existsSync(settingsPath)) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
// No settings.json yet — write the canonical default and return.
|
|
214
|
+
if (!fs.existsSync(settingsPath)) {
|
|
215
|
+
const fresh = generateSettings({ ...DEFAULT_INIT_OPTIONS, targetDir: root, force: true });
|
|
216
|
+
fs.writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), 'utf-8');
|
|
217
|
+
return { name: '.claude/settings.json', status: 'created', detail: 'canonical hooks block written' };
|
|
218
|
+
}
|
|
219
|
+
let existing;
|
|
220
|
+
try {
|
|
221
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Corrupt — rewrite from canonical (force-overwrites; user can revert).
|
|
225
|
+
const fresh = generateSettings({ ...DEFAULT_INIT_OPTIONS, targetDir: root, force: true });
|
|
226
|
+
fs.writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), 'utf-8');
|
|
227
|
+
return { name: '.claude/settings.json', status: 'updated', detail: 'rewrote unparseable settings.json from canonical' };
|
|
228
|
+
}
|
|
229
|
+
// Respect the explicit opt-out — same sentinel the launcher honours.
|
|
230
|
+
if (isHookBlockLocked(existing) && !force) {
|
|
231
|
+
return { name: '.claude/settings.json', status: 'skipped', detail: 'moflo.hooks.locked=true (explicit opt-out)' };
|
|
232
|
+
}
|
|
233
|
+
// Pass 1: in-place command + matcher rewrites (#879, #929, #931, #1171,
|
|
234
|
+
// auto-meditate rebrand). These never delete anything; they fix commands
|
|
235
|
+
// that exist but point at the wrong helper/subcommand.
|
|
236
|
+
const { rewrites } = rewriteIncorrectHookWiring(existing);
|
|
237
|
+
const rewroteCommands = rewrites.reduce((n, r) => n + r.count, 0);
|
|
238
|
+
// Pass 2: structural wholesale regen. Preserves user-owned entries via the
|
|
239
|
+
// #1180 basename guard (any command not pointing at a moflo-shipped helper
|
|
240
|
+
// is grafted back in); relocates moflo entries from legacy event/matcher
|
|
241
|
+
// slots to the current canonical shape.
|
|
242
|
+
const report = computeHookBlockDrift((existing.hooks ?? {}));
|
|
243
|
+
let added = 0;
|
|
244
|
+
let removed = 0;
|
|
245
|
+
let preserved = 0;
|
|
246
|
+
if (report.drifted) {
|
|
247
|
+
const extraCount = report.extra.length;
|
|
248
|
+
const result = applyWholesaleRegeneration(existing, report);
|
|
249
|
+
added = result.added;
|
|
250
|
+
removed = result.removed;
|
|
251
|
+
// applyWholesaleRegeneration computes `removed = extra - customisations`,
|
|
252
|
+
// so `preserved` is the complement — the number of user-owned entries
|
|
253
|
+
// grafted back into the fresh tree.
|
|
254
|
+
preserved = extraCount - removed;
|
|
255
|
+
}
|
|
256
|
+
// Ensure statusLine + permissions/env/attribution scaffold is present —
|
|
257
|
+
// mirrors the existing moflo-init.ts UX but no longer overwrites user
|
|
258
|
+
// values that are already set.
|
|
259
|
+
const canonical = generateSettings({ ...DEFAULT_INIT_OPTIONS, targetDir: root, force: true });
|
|
260
|
+
const scaffoldKeys = ['statusLine', 'permissions', 'env', 'attribution'];
|
|
261
|
+
const scaffoldAdded = [];
|
|
262
|
+
for (const key of scaffoldKeys) {
|
|
263
|
+
if (existing[key] == null && canonical[key] != null) {
|
|
264
|
+
existing[key] = canonical[key];
|
|
265
|
+
scaffoldAdded.push(key);
|
|
200
266
|
}
|
|
201
267
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const gateHook = (sub) => `node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" ${sub}`;
|
|
206
|
-
const gate = (sub) => `node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" ${sub}`;
|
|
207
|
-
const handler = (sub) => `node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" ${sub}`;
|
|
208
|
-
const hooks = {
|
|
209
|
-
"PreToolUse": [
|
|
210
|
-
{
|
|
211
|
-
"matcher": "^(Write|Edit|MultiEdit)$",
|
|
212
|
-
"hooks": [{ "type": "command", "command": handler('post-edit'), "timeout": 5000 }]
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
"matcher": "^(Glob|Grep)$",
|
|
216
|
-
"hooks": [{ "type": "command", "command": gateHook('check-before-scan'), "timeout": 3000 }]
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
"matcher": "^Read$",
|
|
220
|
-
"hooks": [{ "type": "command", "command": gateHook('check-before-read'), "timeout": 3000 }]
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
// #1171 — widened to cover the dedicated `PowerShell` tool.
|
|
224
|
-
"matcher": "^(Bash|PowerShell)$",
|
|
225
|
-
"hooks": [
|
|
226
|
-
{ "type": "command", "command": gateHook('check-dangerous-command'), "timeout": 2000 },
|
|
227
|
-
{ "type": "command", "command": gateHook('check-before-pr'), "timeout": 2000 }
|
|
228
|
-
]
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
// #931 — Advisory only; never blocks. TaskCreate REMINDER and the
|
|
232
|
-
// namespace hint moved here from UserPromptSubmit so they emit only
|
|
233
|
-
// when Claude is about to spawn an Agent — saves ~90 tokens × every
|
|
234
|
-
// prompt × every consumer. Routed via gate-hook.mjs so Claude Code's
|
|
235
|
-
// session_id is forwarded as HOOK_SESSION_ID, enabling per-actor
|
|
236
|
-
// single-shot emission (mirror of #879's record-memory-searched fix).
|
|
237
|
-
"matcher": "^Agent$",
|
|
238
|
-
"hooks": [{ "type": "command", "command": gateHook('check-before-agent'), "timeout": 2000 }]
|
|
239
|
-
}
|
|
240
|
-
],
|
|
241
|
-
"PostToolUse": [
|
|
242
|
-
{
|
|
243
|
-
"matcher": "^(Write|Edit|MultiEdit)$",
|
|
244
|
-
"hooks": [
|
|
245
|
-
{ "type": "command", "command": handler('post-edit'), "timeout": 5000 },
|
|
246
|
-
{ "type": "command", "command": gateHook('reset-edit-gates'), "timeout": 2000 }
|
|
247
|
-
]
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
"matcher": "^Agent$",
|
|
251
|
-
"hooks": [{ "type": "command", "command": handler('post-task'), "timeout": 5000 }]
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
"matcher": "^TaskCreate$",
|
|
255
|
-
"hooks": [{ "type": "command", "command": gate('record-task-created'), "timeout": 2000 }]
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
// #1171 — widened to cover the dedicated `PowerShell` tool.
|
|
259
|
-
"matcher": "^(Bash|PowerShell)$",
|
|
260
|
-
"hooks": [
|
|
261
|
-
{ "type": "command", "command": gateHook('check-bash-memory'), "timeout": 2000 },
|
|
262
|
-
{ "type": "command", "command": gateHook('record-test-run'), "timeout": 2000 }
|
|
263
|
-
]
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
"matcher": "^Skill$",
|
|
267
|
-
"hooks": [{ "type": "command", "command": gateHook('record-skill-run'), "timeout": 2000 }]
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
// Anchored alternation — Claude Code anchors hook matchers (`^…$` semantics),
|
|
271
|
-
// so a bare `mcp__moflo__memory_` never matches any real MCP tool name and the
|
|
272
|
-
// hook silently no-ops (#929 regression). The explicit suffix list keeps the
|
|
273
|
-
// matcher narrow while catching every memory_* tool we ship.
|
|
274
|
-
// Use gateHook (not gate) 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).
|
|
279
|
-
"matcher": "^mcp__moflo__memory_(search|retrieve|list|stats|store)$",
|
|
280
|
-
"hooks": [{ "type": "command", "command": gateHook('record-memory-searched'), "timeout": 3000 }]
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
"matcher": "^mcp__moflo__memory_store$",
|
|
284
|
-
"hooks": [{ "type": "command", "command": gate('record-learnings-stored'), "timeout": 2000 }]
|
|
285
|
-
}
|
|
286
|
-
],
|
|
287
|
-
"UserPromptSubmit": [
|
|
288
|
-
{
|
|
289
|
-
"hooks": [
|
|
290
|
-
{ "type": "command", "command": `node "$CLAUDE_PROJECT_DIR/.claude/helpers/prompt-hook.mjs"`, "timeout": 3000 }
|
|
291
|
-
]
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
// prompt-state-reset is REQUIRED to reset memorySearched/memorySearchedBy on
|
|
295
|
-
// each new prompt and reclassify memoryRequired. Without it, gate state leaks
|
|
296
|
-
// across prompts. Separate hook entry so a prompt-hook.mjs exception doesn't
|
|
297
|
-
// skip the reset. Idempotent state reset only — no emission, no
|
|
298
|
-
// interactionCount increment (#931 dedupe).
|
|
299
|
-
"hooks": [
|
|
300
|
-
{ "type": "command", "command": gateHook('prompt-state-reset'), "timeout": 3000 }
|
|
301
|
-
]
|
|
302
|
-
}
|
|
303
|
-
],
|
|
304
|
-
"SubagentStart": [
|
|
305
|
-
{
|
|
306
|
-
"hooks": [{
|
|
307
|
-
"type": "command",
|
|
308
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/subagent-start.cjs\"",
|
|
309
|
-
"timeout": 2000
|
|
310
|
-
}]
|
|
311
|
-
}
|
|
312
|
-
],
|
|
313
|
-
"SessionStart": [
|
|
314
|
-
{
|
|
315
|
-
"hooks": [
|
|
316
|
-
{
|
|
317
|
-
"type": "command",
|
|
318
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/scripts/session-start-launcher.mjs\"",
|
|
319
|
-
"timeout": 3000
|
|
320
|
-
}
|
|
321
|
-
]
|
|
322
|
-
}
|
|
323
|
-
],
|
|
324
|
-
"Stop": [
|
|
325
|
-
{
|
|
326
|
-
"hooks": [{ "type": "command", "command": handler('session-end'), "timeout": 5000 }]
|
|
327
|
-
}
|
|
328
|
-
],
|
|
329
|
-
"PreCompact": [
|
|
330
|
-
{
|
|
331
|
-
"hooks": [{ "type": "command", "command": gate('compact-guidance'), "timeout": 3000 }]
|
|
332
|
-
}
|
|
333
|
-
],
|
|
334
|
-
"Notification": [
|
|
335
|
-
{
|
|
336
|
-
"hooks": [{ "type": "command", "command": handler('notification'), "timeout": 3000 }]
|
|
337
|
-
}
|
|
338
|
-
]
|
|
339
|
-
};
|
|
340
|
-
// Merge: preserve existing non-MoFlo hooks, add MoFlo hooks
|
|
341
|
-
existing.hooks = hooks;
|
|
342
|
-
// Ensure statusLine is always present (required for dashboard display).
|
|
343
|
-
// The executor.ts / settings-generator.ts code path adds this, but
|
|
344
|
-
// moflo-init.ts uses its own generateHooks() which was missing it.
|
|
345
|
-
if (!existing.statusLine) {
|
|
346
|
-
existing.statusLine = {
|
|
347
|
-
type: 'command',
|
|
348
|
-
command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/statusline.cjs"',
|
|
349
|
-
};
|
|
268
|
+
const dirty = rewroteCommands > 0 || added > 0 || removed > 0 || scaffoldAdded.length > 0;
|
|
269
|
+
if (!dirty) {
|
|
270
|
+
return { name: '.claude/settings.json', status: 'skipped', detail: 'already at canonical reference' };
|
|
350
271
|
}
|
|
351
272
|
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
352
|
-
|
|
273
|
+
// Surface deletions, preserved customisations, and rewrites so nothing is
|
|
274
|
+
// silent — direct response to #1227's "no notice was printed" complaint.
|
|
275
|
+
const parts = [];
|
|
276
|
+
if (added > 0)
|
|
277
|
+
parts.push(`+${added} canonical`);
|
|
278
|
+
if (removed > 0)
|
|
279
|
+
parts.push(`-${removed} stale moflo`);
|
|
280
|
+
if (preserved > 0)
|
|
281
|
+
parts.push(`✓${preserved} preserved`);
|
|
282
|
+
if (rewroteCommands > 0)
|
|
283
|
+
parts.push(`↻${rewroteCommands} rewrites`);
|
|
284
|
+
if (scaffoldAdded.length > 0)
|
|
285
|
+
parts.push(`+scaffold (${scaffoldAdded.join(',')})`);
|
|
286
|
+
return { name: '.claude/settings.json', status: 'updated', detail: parts.join(', ') };
|
|
353
287
|
}
|
|
354
288
|
// ============================================================================
|
|
355
289
|
// Step 3: .claude/skills/flo/ skill (with /fl alias)
|
|
@@ -217,13 +217,24 @@ function interpolateCommandString(command, config) {
|
|
|
217
217
|
}
|
|
218
218
|
/**
|
|
219
219
|
* Run a shell command and capture output.
|
|
220
|
-
*
|
|
220
|
+
*
|
|
221
|
+
* On POSIX we deliberately use `/bin/sh` (NOT `process.env.SHELL`). The
|
|
222
|
+
* interpolation pipeline uses POSIX single-quote escaping (`shellEscapeValue`),
|
|
223
|
+
* which is safe under sh/bash but breaks under zsh: zsh's stricter globbing
|
|
224
|
+
* treats unmatched `[...]` patterns as a hard error ("zsh:1: no matches found"),
|
|
225
|
+
* whereas POSIX sh and bash leave unmatched globs literal. Honouring the user's
|
|
226
|
+
* interactive `$SHELL` made CI (Ubuntu, /bin/bash) pass while macOS/Linux dev
|
|
227
|
+
* machines on zsh silently failed every composite step containing bracket
|
|
228
|
+
* characters in interpolated values.
|
|
229
|
+
*
|
|
230
|
+
* On Windows we use ComSpec (cmd.exe) — POSIX shell quoting is meaningless
|
|
231
|
+
* there; the rest of the pipeline already special-cases Windows shell-out.
|
|
221
232
|
*/
|
|
222
233
|
function runShellCommand(command, context) {
|
|
223
234
|
const timeout = 30000;
|
|
224
235
|
const shell = process.platform === 'win32'
|
|
225
236
|
? (process.env.ComSpec || 'cmd.exe')
|
|
226
|
-
:
|
|
237
|
+
: '/bin/sh';
|
|
227
238
|
return new Promise((resolve) => {
|
|
228
239
|
const child = exec(command, { timeout, shell }, (error, stdout, stderr) => {
|
|
229
240
|
context.abortSignal?.removeEventListener('abort', onAbort);
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.27",
|
|
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",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
96
96
|
"@typescript-eslint/parser": "^7.18.0",
|
|
97
97
|
"eslint": "^8.0.0",
|
|
98
|
-
"moflo": "^4.10.
|
|
98
|
+
"moflo": "^4.10.26",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|