moflo 4.10.26 → 4.10.28
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/.claude/skills/publish/SKILL.md +7 -1
- 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-config.js +52 -0
- package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
- package/dist/src/cli/commands/doctor-registry.js +5 -1
- package/dist/src/cli/init/moflo-init.js +95 -161
- package/dist/src/cli/services/headless-worker-executor.js +28 -3
- 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 |
|
|
@@ -229,9 +229,15 @@ Confirm the published version matches what we just built.
|
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
231
|
npm install moflo@<new-version> --save-dev
|
|
232
|
+
npm install --package-lock-only --force
|
|
233
|
+
npm ci --dry-run --legacy-peer-deps
|
|
232
234
|
```
|
|
233
235
|
|
|
234
|
-
|
|
236
|
+
The first command updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
|
|
237
|
+
|
|
238
|
+
The second command (`--package-lock-only --force`) backfills cross-platform optional-dependency entries that npm omits from the lockfile when `npm install` runs on a single host platform. Without it, transitive optional deps like the nested `@rolldown/binding-wasm32-wasi/node_modules/@emnapi/*` entries get dropped when publishing from Windows, and the resulting lockfile fails `npm ci` on Ubuntu/macOS CI legs. This produced the green-`release-smoke` → red-`ci.yml` divergence in run `26487580745` (moflo@4.10.27 install commit).
|
|
239
|
+
|
|
240
|
+
The final `npm ci --dry-run --legacy-peer-deps` proves the resulting lockfile is in sync with `package.json` before we commit it. If this exits non-zero, stop and investigate — do not commit a broken lockfile.
|
|
235
241
|
|
|
236
242
|
### Step 13: Final Commit
|
|
237
243
|
|
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
|
|
@@ -673,6 +673,58 @@ export async function checkMofloYamlCompliance(cwd = process.cwd()) {
|
|
|
673
673
|
fix: 'Restart Claude Code (yaml-upgrader auto-appends) or `npx moflo init --force`',
|
|
674
674
|
};
|
|
675
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* #1229 — standing tripwire for a silently-disabled memory_first gate.
|
|
678
|
+
*
|
|
679
|
+
* `gates.memory_first` is the only enforcement of the memory-search-first
|
|
680
|
+
* protocol. When it is `false` in moflo.yaml the protocol is off repo-wide
|
|
681
|
+
* with no per-prompt signal — and because the disabled gate is itself what
|
|
682
|
+
* surfaces the stored "never disable memory_first" learning, the situation
|
|
683
|
+
* self-conceals. Historically a daemon-spawned headless analysis worker could
|
|
684
|
+
* write this value unprompted to unblock itself (the `optimize` worker editing
|
|
685
|
+
* `moflo.yaml` to `memory_first: false # Temporarily disabled for performance
|
|
686
|
+
* analysis`); that root cause is fixed by making those workers read-only, but
|
|
687
|
+
* this check is the loud, standing detector so the state never goes unnoticed
|
|
688
|
+
* again — whatever writes it (worker, agent, or a deliberate consumer edit).
|
|
689
|
+
*
|
|
690
|
+
* Warn rather than fail: disabling the gate is a legitimate (if rare) choice;
|
|
691
|
+
* the point is that it can never be silent. Matches only an *active* (un-
|
|
692
|
+
* commented) `memory_first: false` line and is EOL-agnostic so it behaves
|
|
693
|
+
* identically on CRLF (Windows) and LF (POSIX) checkouts.
|
|
694
|
+
*
|
|
695
|
+
* Exported with a cwd param so tests can target a temp root without touching
|
|
696
|
+
* process.cwd().
|
|
697
|
+
*/
|
|
698
|
+
export async function checkMemoryFirstGate(cwd = process.cwd()) {
|
|
699
|
+
const yamlPath = join(cwd, 'moflo.yaml');
|
|
700
|
+
// Absence is covered by checkMofloYamlCompliance; with no file the gate
|
|
701
|
+
// defaults to enabled, so there is nothing to warn about here.
|
|
702
|
+
if (!existsSync(yamlPath)) {
|
|
703
|
+
return { name: 'Memory-First Gate', status: 'pass', message: 'Enabled (default — no moflo.yaml override)' };
|
|
704
|
+
}
|
|
705
|
+
let content;
|
|
706
|
+
try {
|
|
707
|
+
content = readFileSync(yamlPath, 'utf-8');
|
|
708
|
+
}
|
|
709
|
+
catch (e) {
|
|
710
|
+
return { name: 'Memory-First Gate', status: 'warn', message: `Unable to read moflo.yaml: ${errorDetail(e)}` };
|
|
711
|
+
}
|
|
712
|
+
// Active value only: a line whose first non-whitespace token is
|
|
713
|
+
// `memory_first: false`. A commented `# memory_first: false` is ignored
|
|
714
|
+
// because the leading `#` defeats the `^\s*memory_first` anchor.
|
|
715
|
+
const disabled = content
|
|
716
|
+
.split(/\r?\n/)
|
|
717
|
+
.some((line) => /^\s*memory_first:\s*false\b/i.test(line));
|
|
718
|
+
if (disabled) {
|
|
719
|
+
return {
|
|
720
|
+
name: 'Memory-First Gate',
|
|
721
|
+
status: 'warn',
|
|
722
|
+
message: 'DISABLED in moflo.yaml — memory-search-first protocol is off repo-wide and silent per-prompt',
|
|
723
|
+
fix: 'Restore `gates.memory_first: true` (e.g. `git checkout -- moflo.yaml`) unless you disabled it deliberately',
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return { name: 'Memory-First Gate', status: 'pass', message: 'Enabled' };
|
|
727
|
+
}
|
|
676
728
|
/**
|
|
677
729
|
* #981 / #987 — surfaces the single-writer-architecture safety net.
|
|
678
730
|
*
|
|
@@ -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
|
// ============================================================================
|
|
@@ -12,7 +12,7 @@ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
|
|
|
12
12
|
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
13
13
|
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
14
14
|
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
15
|
-
import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
|
|
15
|
+
import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMemoryFirstGate, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
|
|
16
16
|
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
17
17
|
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
18
18
|
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
@@ -34,6 +34,8 @@ export const allChecks = [
|
|
|
34
34
|
checkGitRepo,
|
|
35
35
|
checkConfigFile,
|
|
36
36
|
checkMofloYamlCompliance,
|
|
37
|
+
// #1229 — loud tripwire for a silently-disabled memory_first gate.
|
|
38
|
+
checkMemoryFirstGate,
|
|
37
39
|
checkStatusLine,
|
|
38
40
|
checkDaemonStatus,
|
|
39
41
|
checkDaemonVersionSkew,
|
|
@@ -97,6 +99,8 @@ export const componentMap = {
|
|
|
97
99
|
'config': checkConfigFile,
|
|
98
100
|
'yaml': checkMofloYamlCompliance,
|
|
99
101
|
'moflo-yaml': checkMofloYamlCompliance,
|
|
102
|
+
'memory-first': checkMemoryFirstGate,
|
|
103
|
+
'memory-gate': checkMemoryFirstGate,
|
|
100
104
|
'statusline': checkStatusLine,
|
|
101
105
|
'status-line': checkStatusLine,
|
|
102
106
|
'daemon': checkDaemonStatus,
|
|
@@ -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)
|
|
@@ -46,6 +46,28 @@ const MODEL_IDS = {
|
|
|
46
46
|
opus: 'claude-opus-4-6',
|
|
47
47
|
haiku: 'claude-haiku-4-5-20251001',
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Tool allowlist for every headless worker. These workers are READ-ONLY by
|
|
51
|
+
* design: they analyse the codebase and produce a report. Moflo persists their
|
|
52
|
+
* stdout to `.moflo/reports/<type>.<ext>` itself (see `executeInternal`), so
|
|
53
|
+
* they never need write capability to deliver their output.
|
|
54
|
+
*
|
|
55
|
+
* Restricting tools here is a GATE-INTEGRITY guarantee, not a tidy-up. The
|
|
56
|
+
* spawn previously ran `claude --print <prompt>` with no tool restriction, so
|
|
57
|
+
* a daemon-spawned worker inherited the consumer's permissions and could edit
|
|
58
|
+
* any file in their tree. The `optimize` ("Performance optimization") worker
|
|
59
|
+
* is `enabled: true` by default and runs unattended; given write access and a
|
|
60
|
+
* "analyze this codebase for performance optimizations" prompt, it reasons its
|
|
61
|
+
* way into editing `moflo.yaml` to `memory_first: false`
|
|
62
|
+
* (`# Temporarily disabled for performance analysis`) to unblock its own
|
|
63
|
+
* non-memory-first tool calls against the gate — silently disabling the
|
|
64
|
+
* memory-first protocol repo-wide and dirtying the working tree every run.
|
|
65
|
+
* Worse, the gate it disables is what surfaces the "never disable memory_first"
|
|
66
|
+
* learning, so the loop self-conceals. A read-only allowlist makes that edit
|
|
67
|
+
* physically impossible while leaving full analysis capability (Read/Glob/Grep)
|
|
68
|
+
* intact. See issue #1229.
|
|
69
|
+
*/
|
|
70
|
+
const HEADLESS_WORKER_ALLOWED_TOOLS = 'Read,Glob,Grep';
|
|
49
71
|
/**
|
|
50
72
|
* Default headless worker configurations based on ADR-020 (the
|
|
51
73
|
* `audit`/`document`/`predict` entries from the original ADR were dropped
|
|
@@ -811,7 +833,7 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
811
833
|
buildPrompt(template, context, reportPath) {
|
|
812
834
|
const ioInstructions = `## Output
|
|
813
835
|
|
|
814
|
-
Save the full report to \`${reportPath}\`
|
|
836
|
+
Save the full report to \`${reportPath}\` by emitting it as your response — moflo writes your output to that path after you finish, so that location is authoritative. You are a READ-ONLY analysis worker and have no write tools: do not attempt to create or modify any file in the project, and in particular never edit \`moflo.yaml\`, anything under \`.claude/\`, or any gate/config (the memory-first gate stays on — comply with it, never disable it). Include any test skeletons or code samples inline in the report as fenced code blocks.`;
|
|
815
837
|
if (!context) {
|
|
816
838
|
return `${template}
|
|
817
839
|
|
|
@@ -845,8 +867,11 @@ ${ioInstructions}`;
|
|
|
845
867
|
};
|
|
846
868
|
// Set model
|
|
847
869
|
env.ANTHROPIC_MODEL = MODEL_IDS[options.model];
|
|
848
|
-
// Spawn claude CLI process
|
|
849
|
-
|
|
870
|
+
// Spawn claude CLI process. Keep `--print` first and the prompt second
|
|
871
|
+
// (callers/tests rely on argv[0]/argv[1]); the read-only allowlist is
|
|
872
|
+
// appended so these analysis workers cannot mutate the consumer's tree
|
|
873
|
+
// (e.g. disable a gate in moflo.yaml). See HEADLESS_WORKER_ALLOWED_TOOLS.
|
|
874
|
+
const child = spawn('claude', ['--print', prompt, '--allowedTools', HEADLESS_WORKER_ALLOWED_TOOLS], {
|
|
850
875
|
cwd: this.projectRoot,
|
|
851
876
|
env,
|
|
852
877
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -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.28",
|
|
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.27",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|