moflo 4.10.15 → 4.10.17
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/README.md +13 -0
- package/dist/src/cli/services/hook-block-hash.js +131 -10
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,19 @@ Or — just ask Claude to install MoFlo into your project and initialize it!
|
|
|
23
23
|
|
|
24
24
|
To verify everything is running, ask Claude to run `flo healer` with full diagnostics after restarting. If anything fails, ask Claude to fix it with `flo healer --fix`. (`flo doctor` is still accepted as an alias.)
|
|
25
25
|
|
|
26
|
+
## Next Step: Consult the Eldar
|
|
27
|
+
|
|
28
|
+
After installing moflo, the single highest-leverage thing you can do for the best experience is run **`/eldar`** inside a Claude Code session.
|
|
29
|
+
|
|
30
|
+
Where `flo healer` verifies that *moflo itself* is wired up correctly, **`/eldar` audits how Claude is set up to actually use your project** — guidance docs, CLAUDE.md, memory namespaces, hook/MCP wiring, model routing, and whether every technology in your stack (TypeScript, Python, Rust, Go, etc.) has matching guidance for Claude to lean on. The stack → guidance cross-reference alone is often the difference between *"Claude feels lost in this codebase"* and *"Claude knows this codebase"*.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/eldar # Read-only audit; categorized findings, severity-ranked
|
|
34
|
+
/eldar --fix # Interactive triage — pick what to fix and the Eldar walk you through it
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run it on day one in any new project, any time Claude feels off, or as a periodic health check. Outside of the core install, this is the most impactful thing moflo offers — full details in the **`/eldar`** section [further down](#eldar--consult-the-eldar-project-setup-audit--wizard).
|
|
38
|
+
|
|
26
39
|
## Opinionated Defaults
|
|
27
40
|
|
|
28
41
|
MoFlo makes deliberate choices so you don't have to:
|
|
@@ -262,14 +262,26 @@ export function computeHookBlockDrift(consumerHooks, referenceHooks) {
|
|
|
262
262
|
// Settings.json helpers — shared between launcher + doctor
|
|
263
263
|
// ────────────────────────────────────────────────────────────────────────────
|
|
264
264
|
/**
|
|
265
|
-
* True when the user has set `
|
|
266
|
-
*
|
|
265
|
+
* True when the user has set `moflo.hooks.locked: true` (canonical) or
|
|
266
|
+
* `claudeFlow.hooks.locked: true` (legacy alias) in their settings.json —
|
|
267
|
+
* a sentinel that suppresses drift surfacing entirely.
|
|
268
|
+
*
|
|
269
|
+
* The `claudeFlow.*` settings tree is a pre-rebrand legacy name that survives
|
|
270
|
+
* in writers + readers across the codebase. Renaming the whole tree is a
|
|
271
|
+
* separate effort; this one reader accepts `moflo.hooks.locked` ahead of the
|
|
272
|
+
* legacy key so the #1180 escape hatch is documented under the canonical
|
|
273
|
+
* brand from day one and consumers never have to migrate the key after we
|
|
274
|
+
* tell them to set it.
|
|
267
275
|
*/
|
|
268
276
|
export function isHookBlockLocked(settings) {
|
|
269
277
|
const root = settings;
|
|
278
|
+
const moflo = root?.moflo;
|
|
279
|
+
const mofloHooks = moflo?.hooks;
|
|
280
|
+
if (mofloHooks?.locked === true)
|
|
281
|
+
return true;
|
|
270
282
|
const cf = root?.claudeFlow;
|
|
271
|
-
const
|
|
272
|
-
return
|
|
283
|
+
const cfHooks = cf?.hooks;
|
|
284
|
+
return cfHooks?.locked === true;
|
|
273
285
|
}
|
|
274
286
|
/**
|
|
275
287
|
* Additively repair drift: for every entry in `report.missing`, locate the
|
|
@@ -309,26 +321,135 @@ export function applyAdditiveRegeneration(settings, report) {
|
|
|
309
321
|
settings.hooks = hooks;
|
|
310
322
|
return { settings, added, removed: 0 };
|
|
311
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Set of helper-script basenames that moflo ships under `.claude/helpers/` and
|
|
326
|
+
* `.claude/scripts/`. Built once from `getReferenceHookBlock()` so it always
|
|
327
|
+
* tracks whatever the current reference block actually emits. Used by the
|
|
328
|
+
* wholesale-regen path to tell "stale moflo entry from a removed reference
|
|
329
|
+
* shape" (drop) apart from "consumer customisation we never owned" (preserve).
|
|
330
|
+
*/
|
|
331
|
+
let MOFLO_HELPER_BASENAMES_CACHE = null;
|
|
332
|
+
function getMofloHelperBasenames() {
|
|
333
|
+
if (MOFLO_HELPER_BASENAMES_CACHE)
|
|
334
|
+
return MOFLO_HELPER_BASENAMES_CACHE;
|
|
335
|
+
const out = new Set();
|
|
336
|
+
const tree = getReferenceHookBlock();
|
|
337
|
+
for (const event of Object.keys(tree)) {
|
|
338
|
+
for (const block of tree[event]) {
|
|
339
|
+
for (const hook of block.hooks) {
|
|
340
|
+
const m = hook.command.match(/\.claude\/(?:helpers|scripts)\/([\w.\-]+)/);
|
|
341
|
+
if (m)
|
|
342
|
+
out.add(m[1]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
MOFLO_HELPER_BASENAMES_CACHE = out;
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* True when a hook command references a moflo-shipped helper basename.
|
|
351
|
+
*
|
|
352
|
+
* Design: moflo "owns" the slot for any command pointing at one of its shipped
|
|
353
|
+
* helpers, so wholesale regen is free to replace that slot with the current
|
|
354
|
+
* reference. Conversely, a command pointing at a non-moflo helper basename is
|
|
355
|
+
* a consumer-owned customisation that must survive (#1180).
|
|
356
|
+
*
|
|
357
|
+
* Trade-off — hand-edits to a moflo helper command (e.g. consumer adds
|
|
358
|
+
* `--my-flag` to `gate-hook.mjs check-dangerous-command`) are treated as
|
|
359
|
+
* moflo-owned and replaced with the current reference shape on regen. The
|
|
360
|
+
* alternative (preserve any non-exact match) would silently retain stale
|
|
361
|
+
* entries like `gate.cjs session-reset` (removed in #842), defeating the
|
|
362
|
+
* whole point of wholesale regen. Consumers who need a tweaked moflo command
|
|
363
|
+
* should either lock the hook block via `moflo.hooks.locked: true`
|
|
364
|
+
* (`claudeFlow.hooks.locked` is still honoured as a legacy alias) or route
|
|
365
|
+
* through their own helper basename.
|
|
366
|
+
*
|
|
367
|
+
* Cross-platform: regex matches forward slashes only — what moflo always
|
|
368
|
+
* emits (Claude Code expands `$CLAUDE_PROJECT_DIR` on every OS). A consumer
|
|
369
|
+
* who hand-edited `settings.json` with backslashes on Windows would fail to
|
|
370
|
+
* match here and be treated as a customisation (preserved). That's the safer
|
|
371
|
+
* default — we'd rather keep an unfamiliar entry than delete user work.
|
|
372
|
+
*/
|
|
373
|
+
function isMofloOwnedHookEntry(command) {
|
|
374
|
+
const m = command.match(/\.claude\/(?:helpers|scripts)\/([\w.\-]+)/);
|
|
375
|
+
return m ? getMofloHelperBasenames().has(m[1]) : false;
|
|
376
|
+
}
|
|
312
377
|
/**
|
|
313
378
|
* Wholesale regeneration: replace `settings.hooks` with the canonical reference
|
|
314
|
-
* block
|
|
315
|
-
* `gate.cjs session-reset` SessionStart hook removed in #842) AND
|
|
316
|
-
* entries — the additive variant only does the latter.
|
|
379
|
+
* block, then graft consumer customisations back in. Drops stale moflo entries
|
|
380
|
+
* (e.g. the `gate.cjs session-reset` SessionStart hook removed in #842) AND
|
|
381
|
+
* adds missing entries — the additive variant only does the latter.
|
|
382
|
+
*
|
|
383
|
+
* #1180 — `report.extra` carries both stale moflo entries AND consumer-owned
|
|
384
|
+
* customisations (e.g. waxstak's `project-analysis-gate.cjs`). The wholesale
|
|
385
|
+
* path used to drop both; it now distinguishes them via the helper-basename
|
|
386
|
+
* discriminator: any extra command referencing a moflo-shipped helper under
|
|
387
|
+
* `.claude/helpers/` or `.claude/scripts/` is stale moflo and gets dropped;
|
|
388
|
+
* anything else is consumer-owned and gets grafted back into the fresh tree
|
|
389
|
+
* under the same `(event, matcher)`, with its original `HookEntry`
|
|
390
|
+
* (type/timeout) snapshotted before the replace.
|
|
317
391
|
*
|
|
318
392
|
* The caller MUST check `isHookBlockLocked(settings)` first; if locked, the
|
|
319
393
|
* user has opted out and this function should not be called. Non-hooks fields
|
|
320
|
-
* on `settings` (permissions, env, claudeFlow.*, etc.) are preserved.
|
|
394
|
+
* on `settings` (permissions, env, moflo.*, claudeFlow.*, etc.) are preserved.
|
|
321
395
|
*
|
|
322
396
|
* Mutates `settings` in place; caller is responsible for writing the file.
|
|
323
397
|
*/
|
|
324
398
|
export function applyWholesaleRegeneration(settings, report) {
|
|
325
399
|
if (!report.drifted)
|
|
326
400
|
return { settings, added: 0, removed: 0 };
|
|
401
|
+
// Snapshot full `HookEntry` objects for consumer customisations BEFORE we
|
|
402
|
+
// overwrite settings.hooks — the drift report carries command strings only,
|
|
403
|
+
// not timeout/type. Walk the consumer's existing tree, match against each
|
|
404
|
+
// non-moflo `extra` on (event, matcher, command).
|
|
405
|
+
const customisations = [];
|
|
406
|
+
const consumerHooks = (settings.hooks ?? {});
|
|
407
|
+
for (const extra of report.extra) {
|
|
408
|
+
if (isMofloOwnedHookEntry(extra.command))
|
|
409
|
+
continue;
|
|
410
|
+
const evtArr = consumerHooks[extra.event];
|
|
411
|
+
if (!Array.isArray(evtArr))
|
|
412
|
+
continue;
|
|
413
|
+
for (const block of evtArr) {
|
|
414
|
+
if ((block?.matcher ?? '') !== extra.matcher)
|
|
415
|
+
continue;
|
|
416
|
+
const found = Array.isArray(block.hooks)
|
|
417
|
+
? block.hooks.find((h) => h?.command === extra.command)
|
|
418
|
+
: undefined;
|
|
419
|
+
if (found) {
|
|
420
|
+
customisations.push({ event: extra.event, matcher: extra.matcher, hook: found });
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
327
425
|
// Clone the cached reference so a later mutation of settings.hooks (by the
|
|
328
426
|
// launcher's settings.json migrations, doctor --fix, etc.) cannot corrupt
|
|
329
427
|
// the cached tree shared across `computeHookBlockDrift` calls in this process.
|
|
330
|
-
|
|
331
|
-
|
|
428
|
+
const fresh = structuredClone(getCachedReference().tree);
|
|
429
|
+
// Graft customisations into the fresh tree, slotting them under the same
|
|
430
|
+
// matcher block (created if absent).
|
|
431
|
+
for (const { event, matcher, hook } of customisations) {
|
|
432
|
+
let arr = fresh[event];
|
|
433
|
+
if (!Array.isArray(arr)) {
|
|
434
|
+
arr = [];
|
|
435
|
+
fresh[event] = arr;
|
|
436
|
+
}
|
|
437
|
+
let block = arr.find(b => (b?.matcher ?? '') === matcher);
|
|
438
|
+
if (!block) {
|
|
439
|
+
block = { hooks: [] };
|
|
440
|
+
if (matcher)
|
|
441
|
+
block.matcher = matcher;
|
|
442
|
+
arr.push(block);
|
|
443
|
+
}
|
|
444
|
+
if (!Array.isArray(block.hooks))
|
|
445
|
+
block.hooks = [];
|
|
446
|
+
if (!block.hooks.some((h) => h?.command === hook.command)) {
|
|
447
|
+
block.hooks.push(hook);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
settings.hooks = fresh;
|
|
451
|
+
const removed = report.extra.length - customisations.length;
|
|
452
|
+
return { settings, added: report.missing.length, removed };
|
|
332
453
|
}
|
|
333
454
|
/**
|
|
334
455
|
* Format a drift report for human-readable output (multi-line, no colour).
|
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.17",
|
|
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.16",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|