moflo 4.10.6 → 4.10.8
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-cli-reference.md +1 -1
- package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +19 -5
- package/.claude/skills/memory-optimization/SKILL.md +1 -1
- package/.claude/skills/memory-patterns/SKILL.md +3 -3
- package/.claude/skills/vector-search/SKILL.md +2 -2
- package/README.md +5 -5
- package/bin/lib/daemon-port.mjs +66 -0
- package/bin/session-start-launcher.mjs +189 -15
- package/bin/setup-project.mjs +38 -58
- package/dist/src/cli/commands/daemon.js +31 -10
- package/dist/src/cli/commands/doctor-checks-config.js +139 -1
- package/dist/src/cli/commands/doctor-checks-deep.js +105 -0
- package/dist/src/cli/commands/doctor-fixes.js +99 -2
- package/dist/src/cli/commands/doctor-registry.js +15 -2
- package/dist/src/cli/commands/memory.js +8 -8
- package/dist/src/cli/commands/neural.js +8 -6
- package/dist/src/cli/config/moflo-config.js +79 -3
- package/dist/src/cli/index.js +18 -19
- package/dist/src/cli/init/claudemd-generator.js +6 -2
- package/dist/src/cli/init/moflo-init.js +13 -21
- package/dist/src/cli/init/moflo-yaml-template.js +1 -1
- package/dist/src/cli/mcp-server.js +59 -10
- package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
- package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
- package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
- package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
- package/dist/src/cli/memory/daemon-write-client.js +178 -49
- package/dist/src/cli/memory/database-provider.js +58 -3
- package/dist/src/cli/memory/intelligence.js +54 -26
- package/dist/src/cli/memory/memory-initializer.js +21 -11
- package/dist/src/cli/services/claudemd-injection.js +173 -0
- package/dist/src/cli/services/daemon-dashboard.js +94 -25
- package/dist/src/cli/services/daemon-lock.js +390 -3
- package/dist/src/cli/services/daemon-port.js +217 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/config-adapter.js +0 -182
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| `init` | 4 | Project initialization with wizard, presets, skills, hooks |
|
|
14
14
|
| `agent` | 8 | Agent lifecycle (spawn, list, status, stop, metrics, pool, health, logs) |
|
|
15
15
|
| `swarm` | 6 | Multi-agent swarm coordination and orchestration |
|
|
16
|
-
| `memory` | 11 |
|
|
16
|
+
| `memory` | 11 | node:sqlite + HNSW vector search, 150x-12,500x faster |
|
|
17
17
|
| `mcp` | 9 | MCP server management and tool execution |
|
|
18
18
|
| `task` | 6 | Task creation, assignment, and lifecycle |
|
|
19
19
|
| `session` | 7 | Session state management and persistence |
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
Source files (`.claude/guidance/*.md`, `docs/**/*.md`, code, tests) flow through four stages:
|
|
10
10
|
|
|
11
11
|
1. **Index** — `bin/index-*.mjs` chunks markdown on `##` headers and walks code/tests for structural facts
|
|
12
|
-
2. **Store** — entries land in `.moflo/moflo.db` (SQLite via
|
|
12
|
+
2. **Store** — entries land in `.moflo/moflo.db` (SQLite via Node 22's built-in `node:sqlite`) with metadata + RAG links
|
|
13
13
|
3. **Embed** — `bin/build-embeddings.mjs` generates 384-dim vectors via `fastembed` (mandatory, see ADR-EMB-001)
|
|
14
14
|
4. **Search** — `mcp__moflo__memory_search` (preferred), `npx flo memory search` (fallback), or `npx flo-search` (verbose) hit an HNSW index for nearest-neighbor lookup
|
|
15
15
|
|
|
@@ -47,7 +47,7 @@ auto_index:
|
|
|
47
47
|
|
|
48
48
|
# Memory backend
|
|
49
49
|
memory:
|
|
50
|
-
backend:
|
|
50
|
+
backend: node-sqlite # node-sqlite (default) | rvf (pure-TS fallback) | json (last resort). Passed to createDatabase() as the preferred provider (#1144).
|
|
51
51
|
embedding_model: Xenova/all-MiniLM-L6-v2 # 384-dim neural embeddings
|
|
52
52
|
namespace: default # Default namespace for memory operations
|
|
53
53
|
|
|
@@ -101,9 +101,17 @@ status_line:
|
|
|
101
101
|
sandbox:
|
|
102
102
|
enabled: false # Set to true to wrap bash steps in an OS sandbox
|
|
103
103
|
tier: auto # auto | denylist-only | full
|
|
104
|
+
|
|
105
|
+
# Auto-update on session start (refreshes consumer assets when moflo upgrades)
|
|
106
|
+
auto_update:
|
|
107
|
+
enabled: true # Master toggle for version-change auto-sync
|
|
108
|
+
scripts: true # Sync .claude/scripts/ from moflo bin/
|
|
109
|
+
helpers: true # Sync .claude/helpers/ from moflo source
|
|
110
|
+
hook_block_drift: warn # warn | regenerate | off
|
|
111
|
+
claudemd_injection_drift: regenerate # warn | regenerate | off
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
If your `moflo.yaml` predates the `sandbox:`
|
|
114
|
+
If your `moflo.yaml` predates the `sandbox:` or `auto_update:` blocks, they are auto-appended on the next session start — you never need to re-run `moflo init` after a version bump.
|
|
107
115
|
|
|
108
116
|
### Key Behaviors
|
|
109
117
|
|
|
@@ -128,6 +136,12 @@ If your `moflo.yaml` predates the `sandbox:` block, it is auto-appended on the n
|
|
|
128
136
|
| `sandbox.enabled: true` | Wrap bash steps in an OS sandbox (macOS/Linux/WSL) — absolute disable when `false`, regardless of tier |
|
|
129
137
|
| `sandbox.tier: full` | Require OS sandbox; throw at runtime if the platform tool is unavailable |
|
|
130
138
|
| `sandbox.tier: denylist-only` | Keep Layer 1 denylist only; skip OS isolation even when enabled |
|
|
139
|
+
| `auto_update.enabled: false` | Disable all on-session auto-sync (scripts, helpers, drift checks) |
|
|
140
|
+
| `auto_update.hook_block_drift: regenerate` | Auto-repair drift in `.claude/settings.json` hook block on session start (#881) |
|
|
141
|
+
| `auto_update.hook_block_drift: off` | Skip hook-block drift detection entirely |
|
|
142
|
+
| `auto_update.claudemd_injection_drift: regenerate` | Auto-refresh the MoFlo block in `CLAUDE.md` when it drifts from the current generator (#1142, default) |
|
|
143
|
+
| `auto_update.claudemd_injection_drift: warn` | Print a drift notice on session start but leave `CLAUDE.md` unchanged |
|
|
144
|
+
| `auto_update.claudemd_injection_drift: off` | Skip CLAUDE.md injection drift detection entirely |
|
|
131
145
|
|
|
132
146
|
---
|
|
133
147
|
|
|
@@ -142,9 +156,9 @@ CLAUDE_FLOW_LOG_LEVEL=info # debug | info | warn | error
|
|
|
142
156
|
# MCP Server (stdio transport — no port)
|
|
143
157
|
CLAUDE_FLOW_MCP_TRANSPORT=stdio
|
|
144
158
|
|
|
145
|
-
# Memory backend
|
|
146
|
-
CLAUDE_FLOW_MEMORY_BACKEND=
|
|
147
|
-
CLAUDE_FLOW_MEMORY_TYPE=sqlite #
|
|
159
|
+
# Memory backend (legacy SystemConfig env vars — moflo.yaml `memory.backend` is the modern surface)
|
|
160
|
+
CLAUDE_FLOW_MEMORY_BACKEND=sqlite # informational only; not consumed by selectProvider
|
|
161
|
+
CLAUDE_FLOW_MEMORY_TYPE=sqlite # SystemConfig override (legacy)
|
|
148
162
|
```
|
|
149
163
|
|
|
150
164
|
Variable names retain the `CLAUDE_FLOW_` prefix for backward compatibility with consumers upgraded from claude-flow.
|
|
@@ -112,7 +112,7 @@ const stats = await mcp.memory_stats({});
|
|
|
112
112
|
- **Don't rebuild the index on every test.** Use a module-level singleton + `beforeAll`. HNSW cold-boot is ~5s.
|
|
113
113
|
- **Don't raise `ef` globally.** Raise it on the specific queries that need recall. Default is fine for 90% of calls.
|
|
114
114
|
- **Don't quantize a small corpus.** Below ~500k vectors the RAM saving doesn't justify the recall cost.
|
|
115
|
-
- **Don't measure in dev mode.**
|
|
115
|
+
- **Don't measure in dev mode.** The memory stack behaves differently under `NODE_ENV=production`; benches should match the target.
|
|
116
116
|
|
|
117
117
|
## See Also
|
|
118
118
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "memory-patterns"
|
|
3
|
-
description: "Persistent memory patterns for moflo agents — session memory, long-term knowledge, pattern learning, and cross-session context via moflo's
|
|
3
|
+
description: "Persistent memory patterns for moflo agents — session memory, long-term knowledge, pattern learning, and cross-session context via moflo's node:sqlite + HNSW vector store. Use when building stateful agents or assistants that need to remember across runs."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# MoFlo Memory Patterns
|
|
7
7
|
|
|
8
|
-
Persistent, semantically-searchable memory for moflo-enabled projects. Backed by `.
|
|
8
|
+
Persistent, semantically-searchable memory for moflo-enabled projects. Backed by `.moflo/moflo.db` (node:sqlite + HNSW vector index) and exposed through MCP tools.
|
|
9
9
|
|
|
10
10
|
## Core API
|
|
11
11
|
|
|
@@ -124,7 +124,7 @@ This is the same fan-out the `/flo` spell does — cheap (HNSW, parallel) and re
|
|
|
124
124
|
|
|
125
125
|
## Persistence & Indexing
|
|
126
126
|
|
|
127
|
-
- File: `.
|
|
127
|
+
- File: `.moflo/moflo.db` at project root (node:sqlite, Node 22+ built-in).
|
|
128
128
|
- Embeddings: built by cli's embeddings module; indexed with HNSW from `src/cli/memory/`.
|
|
129
129
|
- Cold-start cost: ~5 seconds to initialize HNSW. Tests should share a single instance (`beforeAll`, not `beforeEach`).
|
|
130
130
|
- Namespace isolation: each namespace is a logical partition, but the HNSW index spans the table. Query time scales with `limit` and `threshold`, not total row count.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "vector-search"
|
|
3
|
-
description: "Semantic vector search with moflo — RAG over your own documents, similarity matching, context-aware retrieval via HNSW (
|
|
3
|
+
description: "Semantic vector search with moflo — RAG over your own documents, similarity matching, context-aware retrieval via HNSW (node:sqlite-backed). Use when building retrieval layers for chat, search, or context-assembly."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# MoFlo Vector Search (RAG)
|
|
7
7
|
|
|
8
|
-
Semantic search over your own documents, backed by moflo's HNSW index in `.
|
|
8
|
+
Semantic search over your own documents, backed by moflo's HNSW index in `.moflo/moflo.db` (node:sqlite, Node 22+ built-in). Small enough to ship in a devDependency; fast enough for interactive retrieval at 100k–1M vectors.
|
|
9
9
|
|
|
10
10
|
## When to Use This vs `memory-patterns`
|
|
11
11
|
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ MoFlo makes deliberate choices so you don't have to:
|
|
|
30
30
|
- **Fully self-contained** — No external services, no cloud dependencies, no API keys. Everything runs locally on your machine.
|
|
31
31
|
- **Minimal dependencies** — small runtime dep set, all WASM or prebuilt binaries. No native compilation, no `node-gyp`, no platform-specific build steps.
|
|
32
32
|
- **Node.js runtime** — Targets Node.js specifically. All scripts, hooks, and tooling are JavaScript/TypeScript. No Python, no Rust binaries, no native compilation.
|
|
33
|
-
- **
|
|
33
|
+
- **node:sqlite (built-in)** — The memory database uses Node 22's built-in SQLite engine. No `better-sqlite3` native bindings to compile, no WASM round-trip, no platform-specific build steps. Works identically on Windows, macOS, and Linux.
|
|
34
34
|
- **Neural embeddings by default** — 384-dimensional embeddings using `all-MiniLM-L6-v2`. No hash fallback, no peer-optional setup, no install prompts — real semantic search works out of the box. A `postinstall` step trims the embedding runtime to your platform and strips GPU-only libraries the runtime never loads, reclaiming roughly 340 MB on Linux and 150 MB on Windows from a fresh install. Set `MOFLO_NO_PRUNE=1` to skip the trim, or `ONNXRUNTIME_NODE_INSTALL_CUDA=true` to keep CUDA GPU support.
|
|
35
35
|
- **Full learning stack wired up OOTB** — All configured and functional from `flo init`, no manual setup:
|
|
36
36
|
- **SONA** (Self-Optimizing Neural Architecture) — learns from task trajectories
|
|
@@ -537,7 +537,7 @@ flo diagnose --json # JSON output for CI/automation
|
|
|
537
537
|
| **MCP Spell Integration** | Bridge between MCP tools and spell engine functions correctly |
|
|
538
538
|
| **Hook Execution** | Hook executor is functional and can fire hooks |
|
|
539
539
|
| **Gate Health** | All gate cases, hook bindings, and state file are intact |
|
|
540
|
-
| **MofloDb Bridge** | Memory DB adapter (
|
|
540
|
+
| **MofloDb Bridge** | Memory DB adapter (node:sqlite + HNSW) is wired and routable |
|
|
541
541
|
| **Sandbox Tier** | Detects which sandbox backend is available (Docker / bwrap / sandbox-exec / none) |
|
|
542
542
|
|
|
543
543
|
**Auto-fix mode** (`flo healer --fix`) attempts to repair each failing check automatically:
|
|
@@ -645,7 +645,7 @@ These are the backend systems that hooks and commands interact with.
|
|
|
645
645
|
|
|
646
646
|
| System | What It Does | Why It Matters | Enabled OOTB |
|
|
647
647
|
|--------|-------------|----------------|:---:|
|
|
648
|
-
| **Semantic Memory** | SQLite database (
|
|
648
|
+
| **Semantic Memory** | SQLite database (Node's built-in `node:sqlite`) storing knowledge entries with 384-dim vector embeddings | Your AI assistant accumulates project knowledge across sessions instead of starting from scratch each time | Yes |
|
|
649
649
|
| **HNSW Vector Search** | Hierarchical Navigable Small World index for fast nearest-neighbor search | Searches across thousands of stored entries return in milliseconds instead of scanning linearly | Yes |
|
|
650
650
|
| **Guidance Indexing** | Chunks markdown docs into overlapping segments, embeds each with MiniLM-L6-v2 | Your project documentation becomes searchable by meaning ("how does auth work?") not just keywords | Yes |
|
|
651
651
|
| **Code Map** | Parses source files for exports, classes, functions, types | The AI can answer "where is X defined?" from the index instead of running Glob/Grep | Yes |
|
|
@@ -720,7 +720,7 @@ Routing outcomes persist across sessions. You can inspect them with `flo hooks p
|
|
|
720
720
|
|
|
721
721
|
### Memory & Knowledge Storage
|
|
722
722
|
|
|
723
|
-
MoFlo uses a SQLite database (via
|
|
723
|
+
MoFlo uses a SQLite database (via Node 22's built-in `node:sqlite` — no native deps) to store three types of knowledge:
|
|
724
724
|
|
|
725
725
|
| Namespace | What's Stored | How It Gets There |
|
|
726
726
|
|-----------|---------------|-------------------|
|
|
@@ -861,7 +861,7 @@ MoFlo started from [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) but is n
|
|
|
861
861
|
|
|
862
862
|
My use case was just one of those many scenarios: day-to-day local coding, enhancing my normal Claude Code experience on a single project. The original supported this — it was all in there — but because the project served so many different needs, I found myself configuring and tailoring things for my specific setup each time I pulled in updates. That isn't a shortcoming of the original; it's the natural trade-off of a tool designed to be that flexible and powerful.
|
|
863
863
|
|
|
864
|
-
So I started from that foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding. Over time MoFlo grew its own architecture (workspace collapse, in-tree fastembed runtime,
|
|
864
|
+
So I started from that foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding. Over time MoFlo grew its own architecture (workspace collapse, in-tree fastembed runtime, node:sqlite + HNSW memory layer, spell engine, daemon-driven scheduling) and the two projects fully diverged.
|
|
865
865
|
|
|
866
866
|
If you're exploring the full breadth of agent orchestration, go look at [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) — it's the real deal. If your needs are similar to mine — a focused, opinionated local dev setup that just works — MoFlo is for you.
|
|
867
867
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS counterpart to src/cli/services/daemon-port.ts.
|
|
3
|
+
*
|
|
4
|
+
* Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
|
|
5
|
+
* run before any TS compilation has happened. The TS file is the canonical
|
|
6
|
+
* API; this file MUST stay algorithmically identical (asserted by
|
|
7
|
+
* `tests/system/daemon-port-twin.test.ts`).
|
|
8
|
+
*
|
|
9
|
+
* See `src/cli/services/daemon-port.ts` for the full doc + history.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const PORT_RANGE_BASE = 33000;
|
|
16
|
+
export const PORT_RANGE_SIZE = 1000;
|
|
17
|
+
export const LEGACY_DEFAULT_PORT = 3117;
|
|
18
|
+
|
|
19
|
+
export function readEnvPortOverride() {
|
|
20
|
+
const raw = process.env.MOFLO_DAEMON_PORT;
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
const n = parseInt(raw, 10);
|
|
23
|
+
if (!Number.isFinite(n) || n < 1 || n > 65535) return null;
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveProjectPort(projectRoot) {
|
|
28
|
+
const envPort = readEnvPortOverride();
|
|
29
|
+
if (envPort != null) return envPort;
|
|
30
|
+
const hash = createHash('sha256').update(projectRoot).digest();
|
|
31
|
+
return PORT_RANGE_BASE + (hash.readUInt16BE(0) % PORT_RANGE_SIZE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveClientPort(projectRoot) {
|
|
35
|
+
const envPort = readEnvPortOverride();
|
|
36
|
+
if (envPort != null) return envPort;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const lockFile = join(projectRoot, '.moflo', 'daemon.lock');
|
|
40
|
+
if (existsSync(lockFile)) {
|
|
41
|
+
const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
42
|
+
const lockPort = typeof lock?.port === 'number' ? lock.port : null;
|
|
43
|
+
if (lockPort && Number.isFinite(lockPort) && lockPort > 0 && lockPort < 65536) {
|
|
44
|
+
return lockPort;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// fall through
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return resolveProjectPort(projectRoot);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function serverPortCandidates(projectRoot, maxAttempts = 10) {
|
|
55
|
+
const envPort = readEnvPortOverride();
|
|
56
|
+
if (envPort != null) return [envPort];
|
|
57
|
+
|
|
58
|
+
const base = resolveProjectPort(projectRoot);
|
|
59
|
+
const attempts = Math.min(Math.max(1, maxAttempts), PORT_RANGE_SIZE);
|
|
60
|
+
const ports = [];
|
|
61
|
+
for (let i = 0; i < attempts; i++) {
|
|
62
|
+
const candidate = PORT_RANGE_BASE + ((base - PORT_RANGE_BASE + i) % PORT_RANGE_SIZE);
|
|
63
|
+
ports.push(candidate);
|
|
64
|
+
}
|
|
65
|
+
return ports;
|
|
66
|
+
}
|
|
@@ -640,7 +640,16 @@ try {
|
|
|
640
640
|
// Controlled by `auto_update.enabled` in moflo.yaml (default: true).
|
|
641
641
|
// When moflo is upgraded (npm install), scripts and helpers may be stale.
|
|
642
642
|
// Detect version change and sync from source before running hooks.
|
|
643
|
-
let autoUpdateConfig = {
|
|
643
|
+
let autoUpdateConfig = {
|
|
644
|
+
enabled: true,
|
|
645
|
+
scripts: true,
|
|
646
|
+
helpers: true,
|
|
647
|
+
hookBlockDrift: 'warn',
|
|
648
|
+
// #1142 — CLAUDE.md injection drift refresh mode (warn | regenerate | off,
|
|
649
|
+
// default regenerate). Defaults to regenerate because the consumer cannot
|
|
650
|
+
// refresh CLAUDE.md on their own — there is no other auto-refresh path.
|
|
651
|
+
claudemdInjectionDrift: 'regenerate',
|
|
652
|
+
};
|
|
644
653
|
try {
|
|
645
654
|
const mofloYaml = resolve(projectRoot, 'moflo.yaml');
|
|
646
655
|
if (existsSync(mofloYaml)) {
|
|
@@ -651,10 +660,13 @@ try {
|
|
|
651
660
|
const helpersMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+helpers:\s*(true|false)/);
|
|
652
661
|
// #881: hook-block drift detector (warn | regenerate | off; default warn)
|
|
653
662
|
const driftMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+hook_block_drift:\s*(warn|regenerate|off)/);
|
|
663
|
+
// #1142: CLAUDE.md injection drift detector (warn | regenerate | off; default regenerate)
|
|
664
|
+
const claudemdMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+claudemd_injection_drift:\s*(warn|regenerate|off)/);
|
|
654
665
|
if (enabledMatch) autoUpdateConfig.enabled = enabledMatch[1] === 'true';
|
|
655
666
|
if (scriptsMatch) autoUpdateConfig.scripts = scriptsMatch[1] === 'true';
|
|
656
667
|
if (helpersMatch) autoUpdateConfig.helpers = helpersMatch[1] === 'true';
|
|
657
668
|
if (driftMatch) autoUpdateConfig.hookBlockDrift = driftMatch[1];
|
|
669
|
+
if (claudemdMatch) autoUpdateConfig.claudemdInjectionDrift = claudemdMatch[1];
|
|
658
670
|
}
|
|
659
671
|
} catch (err) {
|
|
660
672
|
// Defaults (all true) keep the upgrade flow alive but the user should
|
|
@@ -1414,23 +1426,185 @@ async function runHookBlockDriftCheck() {
|
|
|
1414
1426
|
};
|
|
1415
1427
|
}
|
|
1416
1428
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1429
|
+
// ── 3a-vii. CLAUDE.md injection drift detection (#1142) ──────────────────
|
|
1430
|
+
// Refresh the consumer's `<root>/CLAUDE.md` MoFlo block when it has drifted
|
|
1431
|
+
// from what `claudemd-generator.ts` currently produces. The launcher's
|
|
1432
|
+
// stages 3/3b refresh shipped guidance files on every version change, but
|
|
1433
|
+
// CLAUDE.md was only rewritten by explicit `flo init` / `flo-setup` — so
|
|
1434
|
+
// consumers carried stale injection content (with the legacy `shipped/`
|
|
1435
|
+
// guidance paths, for example) for as long as they didn't re-run init.
|
|
1436
|
+
//
|
|
1437
|
+
// Modes (`auto_update.claudemd_injection_drift` in moflo.yaml):
|
|
1438
|
+
// regenerate — replace the drifted block in place (default; the consumer
|
|
1439
|
+
// has no other path to refresh CLAUDE.md)
|
|
1440
|
+
// warn — print a one-line drift summary to stdout
|
|
1441
|
+
// off — skip detection entirely
|
|
1442
|
+
//
|
|
1443
|
+
// Fast-path: `.moflo/claudemd-injection-cache.json` records the last clean
|
|
1444
|
+
// run. If CLAUDE.md + the generator module both still match the cached
|
|
1445
|
+
// mtimes, skip the readFile + dynamic import.
|
|
1446
|
+
async function runClaudeMdInjectionDriftCheck() {
|
|
1447
|
+
const claudeMdPath = resolve(projectRoot, 'CLAUDE.md');
|
|
1448
|
+
let claudeMdStat;
|
|
1449
|
+
try { claudeMdStat = statSync(claudeMdPath); } catch { return null; }
|
|
1450
|
+
|
|
1451
|
+
// Locate the generator and the drift service. Both must be present —
|
|
1452
|
+
// generator owns the canonical content, drift service owns the marker
|
|
1453
|
+
// logic. Use bin/lib/moflo-resolve.mjs path resolution semantics
|
|
1454
|
+
// (covers consumer node_modules + dev source tree). Two candidates each.
|
|
1455
|
+
const generatorCandidates = [
|
|
1456
|
+
resolve(projectRoot, 'node_modules/moflo/dist/src/cli/init/claudemd-generator.js'),
|
|
1457
|
+
resolve(projectRoot, 'dist/src/cli/init/claudemd-generator.js'),
|
|
1458
|
+
];
|
|
1459
|
+
const driftCandidates = [
|
|
1460
|
+
resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/claudemd-injection.js'),
|
|
1461
|
+
resolve(projectRoot, 'dist/src/cli/services/claudemd-injection.js'),
|
|
1462
|
+
];
|
|
1463
|
+
let generatorPath = null, generatorStat = null;
|
|
1464
|
+
for (const p of generatorCandidates) {
|
|
1465
|
+
try { generatorStat = statSync(p); generatorPath = p; break; } catch { /* try next */ }
|
|
1466
|
+
}
|
|
1467
|
+
let driftPath = null, driftStat = null;
|
|
1468
|
+
for (const p of driftCandidates) {
|
|
1469
|
+
try { driftStat = statSync(p); driftPath = p; break; } catch { /* try next */ }
|
|
1470
|
+
}
|
|
1471
|
+
if (!generatorPath || !driftPath) return null;
|
|
1472
|
+
|
|
1473
|
+
// Use the max mtime of the two modules so any update to either invalidates
|
|
1474
|
+
// the cache. Both are co-bumped on publish so this is normally one mtime.
|
|
1475
|
+
const moduleMtimeMs = Math.max(generatorStat.mtimeMs, driftStat.mtimeMs);
|
|
1476
|
+
|
|
1477
|
+
// Cache short-circuits any (claudeMdMtime, moduleMtime, state) triple
|
|
1478
|
+
// match — not just 'in-sync'. A drifted consumer in warn mode still emits
|
|
1479
|
+
// the nudge once on first detection, then stays silent until something
|
|
1480
|
+
// actually changes (CLAUDE.md mtime bumps from a user edit, or moflo
|
|
1481
|
+
// upgrade bumps moduleMtimeMs). Without this, every session re-does the
|
|
1482
|
+
// full slow path (3 statSync + readFile + 2 dynamic imports + generator
|
|
1483
|
+
// call) for non-in-sync consumers in perpetuity.
|
|
1484
|
+
const cachePath = join(mofloDir(projectRoot), 'claudemd-injection-cache.json');
|
|
1485
|
+
let cached = null;
|
|
1486
|
+
try { cached = JSON.parse(readFileSync(cachePath, 'utf-8')); } catch { /* missing or corrupt */ }
|
|
1487
|
+
if (
|
|
1488
|
+
cached &&
|
|
1489
|
+
cached.claudeMdMtimeMs === claudeMdStat.mtimeMs &&
|
|
1490
|
+
cached.moduleMtimeMs === moduleMtimeMs &&
|
|
1491
|
+
typeof cached.state === 'string'
|
|
1492
|
+
) return null;
|
|
1493
|
+
|
|
1494
|
+
// Try-catch around the dynamic imports handles the file disappearing
|
|
1495
|
+
// between statSync and import (TOCTOU); other load errors surface as
|
|
1496
|
+
// an emitWarning so a transitive dependency failure isn't invisible
|
|
1497
|
+
// (mirrors the silent-catch lesson — see feedback_consumer_blast_radius).
|
|
1498
|
+
let genMod = null, driftMod = null;
|
|
1499
|
+
try {
|
|
1500
|
+
genMod = await import(pathToFileURL(generatorPath).href);
|
|
1501
|
+
driftMod = await import(pathToFileURL(driftPath).href);
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
emitWarning(`CLAUDE.md drift check skipped (${errMessage(err)})`);
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
if (typeof genMod.generateClaudeMd !== 'function') return null;
|
|
1507
|
+
if (typeof driftMod.computeInjectionDrift !== 'function') return null;
|
|
1508
|
+
if (typeof driftMod.applyInjectionReplacement !== 'function') return null;
|
|
1509
|
+
|
|
1510
|
+
const claudeMdContents = readFileSync(claudeMdPath, 'utf-8');
|
|
1511
|
+
const canonical = genMod.generateClaudeMd({});
|
|
1512
|
+
const report = driftMod.computeInjectionDrift(claudeMdContents, canonical);
|
|
1513
|
+
|
|
1514
|
+
let finalState = report.state;
|
|
1515
|
+
let finalMtime = claudeMdStat.mtimeMs;
|
|
1516
|
+
|
|
1517
|
+
// Treat both 'drifted' and 'legacy-marker' as repairable in regenerate mode.
|
|
1518
|
+
// 'no-marker' means the user removed the inject deliberately — don't re-add
|
|
1519
|
+
// it on every session start; that's a re-init operation, not a drift fix.
|
|
1520
|
+
// 'no-file' is unreachable here because statSync already succeeded.
|
|
1521
|
+
const repairable = report.state === 'drifted' || report.state === 'legacy-marker';
|
|
1522
|
+
if (repairable) {
|
|
1523
|
+
const wantRegenerate = autoUpdateConfig.claudemdInjectionDrift === 'regenerate';
|
|
1524
|
+
if (wantRegenerate) {
|
|
1525
|
+
const result = driftMod.applyInjectionReplacement(claudeMdContents, canonical);
|
|
1526
|
+
if (result.changed && typeof result.contents === 'string') {
|
|
1527
|
+
writeFileSync(claudeMdPath, result.contents);
|
|
1528
|
+
finalState = 'in-sync';
|
|
1529
|
+
try { finalMtime = statSync(claudeMdPath).mtimeMs; } catch { /* keep prior */ }
|
|
1530
|
+
emitMutation(
|
|
1531
|
+
'refreshed CLAUDE.md MoFlo block',
|
|
1532
|
+
`replaced ${report.state} block with current generator output`,
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
// warn mode — surface a one-line summary on stdout for Claude/user.
|
|
1421
1537
|
try {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1538
|
+
process.stdout.write(
|
|
1539
|
+
`moflo: CLAUDE.md injection ${report.state}; run \`flo doctor claudemd-drift\` or set auto_update.claudemd_injection_drift: regenerate in moflo.yaml\n`,
|
|
1540
|
+
);
|
|
1541
|
+
} catch { /* broken stdout — non-fatal */ }
|
|
1542
|
+
}
|
|
1543
|
+
} else if (report.state === 'no-marker') {
|
|
1544
|
+
// Distinct from the drift cases — surface once via warn channel so a
|
|
1545
|
+
// user who didn't run init still sees a nudge, but never auto-mutate.
|
|
1546
|
+
if (autoUpdateConfig.claudemdInjectionDrift !== 'off') {
|
|
1547
|
+
try {
|
|
1548
|
+
process.stdout.write(
|
|
1549
|
+
`moflo: CLAUDE.md has no MoFlo injection block; run \`npx flo-setup\` to add one\n`,
|
|
1550
|
+
);
|
|
1551
|
+
} catch { /* broken stdout — non-fatal */ }
|
|
1430
1552
|
}
|
|
1431
1553
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1554
|
+
|
|
1555
|
+
return {
|
|
1556
|
+
cachePath,
|
|
1557
|
+
claudeMdMtimeMs: finalMtime,
|
|
1558
|
+
moduleMtimeMs,
|
|
1559
|
+
state: finalState,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Run the two drift detectors (settings.json hook block + CLAUDE.md
|
|
1564
|
+
// injection) in parallel. Both do their own statSync → cache compare →
|
|
1565
|
+
// dynamic import dance; the work is independent and the file targets are
|
|
1566
|
+
// different, so Promise.all halves cold-path latency on every session start.
|
|
1567
|
+
// Cache-hit fast paths return null with no work — Promise.all is still
|
|
1568
|
+
// trivially correct there.
|
|
1569
|
+
{
|
|
1570
|
+
const hookEnabled = autoUpdateConfig.enabled && autoUpdateConfig.hookBlockDrift !== 'off';
|
|
1571
|
+
const claudemdEnabled = autoUpdateConfig.enabled && autoUpdateConfig.claudemdInjectionDrift !== 'off';
|
|
1572
|
+
const [hookResult, claudemdResult] = await Promise.all([
|
|
1573
|
+
hookEnabled
|
|
1574
|
+
? runHookBlockDriftCheck().catch((err) => {
|
|
1575
|
+
emitWarning(`hook-block drift check skipped (${errMessage(err)})`);
|
|
1576
|
+
return null;
|
|
1577
|
+
})
|
|
1578
|
+
: Promise.resolve(null),
|
|
1579
|
+
claudemdEnabled
|
|
1580
|
+
? runClaudeMdInjectionDriftCheck().catch((err) => {
|
|
1581
|
+
emitWarning(`CLAUDE.md injection drift check skipped (${errMessage(err)})`);
|
|
1582
|
+
return null;
|
|
1583
|
+
})
|
|
1584
|
+
: Promise.resolve(null),
|
|
1585
|
+
]);
|
|
1586
|
+
|
|
1587
|
+
if (hookResult) {
|
|
1588
|
+
try {
|
|
1589
|
+
mkdirSync(mofloDir(projectRoot), { recursive: true });
|
|
1590
|
+
writeFileSync(hookResult.cachePath, JSON.stringify({
|
|
1591
|
+
settingsMtimeMs: hookResult.settingsMtimeMs,
|
|
1592
|
+
moduleMtimeMs: hookResult.moduleMtimeMs,
|
|
1593
|
+
consumerHash: hookResult.consumerHash,
|
|
1594
|
+
referenceHash: hookResult.referenceHash,
|
|
1595
|
+
}));
|
|
1596
|
+
} catch { /* cache is opportunistic — non-fatal */ }
|
|
1597
|
+
}
|
|
1598
|
+
if (claudemdResult) {
|
|
1599
|
+
try {
|
|
1600
|
+
mkdirSync(mofloDir(projectRoot), { recursive: true });
|
|
1601
|
+
writeFileSync(claudemdResult.cachePath, JSON.stringify({
|
|
1602
|
+
claudeMdMtimeMs: claudemdResult.claudeMdMtimeMs,
|
|
1603
|
+
moduleMtimeMs: claudemdResult.moduleMtimeMs,
|
|
1604
|
+
state: claudemdResult.state,
|
|
1605
|
+
}));
|
|
1606
|
+
} catch { /* cache is opportunistic — non-fatal */ }
|
|
1607
|
+
}
|
|
1434
1608
|
}
|
|
1435
1609
|
|
|
1436
1610
|
// ── 3b. Ensure shipped guidance files exist (even without version change) ──
|
package/bin/setup-project.mjs
CHANGED
|
@@ -37,16 +37,14 @@ import { mofloInternalURL } from './lib/moflo-resolve.mjs';
|
|
|
37
37
|
// works identically from bin/ (canonical) or from .claude/scripts/ (synced copy).
|
|
38
38
|
const mofloRoot = dirname(fileURLToPath(mofloInternalURL('package.json')));
|
|
39
39
|
|
|
40
|
-
// Single source of truth: claudemd-generator.ts owns the section content
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
LEGACY_MARKER_ENDS,
|
|
49
|
-
} = await import(mofloInternalURL('dist/src/cli/init/claudemd-generator.js'));
|
|
40
|
+
// Single source of truth: claudemd-generator.ts owns the section content,
|
|
41
|
+
// claudemd-injection.ts owns the marker-replace logic. Use the shared
|
|
42
|
+
// mofloInternalURL helper so the script works identically when invoked
|
|
43
|
+
// from bin/ (canonical) or from .claude/scripts/ (synced copy).
|
|
44
|
+
const { generateClaudeMd } = await import(mofloInternalURL('dist/src/cli/init/claudemd-generator.js'));
|
|
45
|
+
const { applyInjectionReplacement, computeInjectionDrift } = await import(
|
|
46
|
+
mofloInternalURL('dist/src/cli/services/claudemd-injection.js')
|
|
47
|
+
);
|
|
50
48
|
|
|
51
49
|
const args = process.argv.slice(2);
|
|
52
50
|
const updateOnly = args.includes('--update');
|
|
@@ -150,65 +148,47 @@ function cleanupLegacyBootstrap(projectRoot) {
|
|
|
150
148
|
|
|
151
149
|
function updateClaudeMd(projectRoot) {
|
|
152
150
|
const claudeMdPath = join(projectRoot, 'CLAUDE.md');
|
|
151
|
+
const existed = existsSync(claudeMdPath);
|
|
152
|
+
const content = existed ? readFileSync(claudeMdPath, 'utf-8') : null;
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
log('📝 Creating CLAUDE.md with subagent protocol section');
|
|
160
|
-
writeFileSync(claudeMdPath, `# Project Configuration\n\n${CLAUDE_MD_SECTION}\n`, 'utf-8');
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
154
|
+
// Single source of truth for the marker-replace logic lives in
|
|
155
|
+
// src/cli/services/claudemd-injection.ts. Classify state for logging,
|
|
156
|
+
// then apply (or report) the replacement.
|
|
157
|
+
const report = computeInjectionDrift(content, CLAUDE_MD_SECTION);
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const allStarts = [MARKER_START, ...LEGACY_MARKER_STARTS];
|
|
168
|
-
const allEnds = [MARKER_END, ...LEGACY_MARKER_ENDS];
|
|
169
|
-
|
|
170
|
-
for (let i = 0; i < allStarts.length; i++) {
|
|
171
|
-
if (content.includes(allStarts[i])) {
|
|
172
|
-
const startIdx = content.indexOf(allStarts[i]);
|
|
173
|
-
const endIdx = content.indexOf(allEnds[i]);
|
|
174
|
-
|
|
175
|
-
if (endIdx > startIdx) {
|
|
176
|
-
// If current markers and content matches, we're up to date
|
|
177
|
-
if (i === 0) {
|
|
178
|
-
const existingSection = content.substring(startIdx, endIdx + allEnds[i].length);
|
|
179
|
-
if (existingSection === CLAUDE_MD_SECTION) {
|
|
180
|
-
log('✅ CLAUDE.md moflo section is current');
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Replace (current or legacy) with new section
|
|
186
|
-
if (!checkOnly) {
|
|
187
|
-
const updated = content.substring(0, startIdx) + CLAUDE_MD_SECTION + content.substring(endIdx + allEnds[i].length);
|
|
188
|
-
writeFileSync(claudeMdPath, updated, 'utf-8');
|
|
189
|
-
log(i === 0 ? '📝 Updated CLAUDE.md moflo section' : '📝 Replaced legacy CLAUDE.md section with minimal moflo injection');
|
|
190
|
-
} else {
|
|
191
|
-
log('⚠️ CLAUDE.md moflo section needs update');
|
|
192
|
-
}
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
159
|
+
if (report.state === 'in-sync') {
|
|
160
|
+
log('✅ CLAUDE.md moflo section is current');
|
|
161
|
+
return true;
|
|
196
162
|
}
|
|
197
163
|
|
|
164
|
+
// `updateOnly` is informational — refresh the bootstrap mirror file but
|
|
165
|
+
// leave CLAUDE.md alone unless an inject already exists.
|
|
198
166
|
if (updateOnly) {
|
|
199
|
-
log('⚠️ CLAUDE.md
|
|
200
|
-
|
|
167
|
+
if (!existed) { log('⚠️ No CLAUDE.md found'); return false; }
|
|
168
|
+
if (report.state === 'no-marker') {
|
|
169
|
+
log('⚠️ CLAUDE.md has no moflo section (run without --update to add)');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// Existing block (current or legacy) + drift → fall through to write.
|
|
201
173
|
}
|
|
202
174
|
|
|
203
175
|
if (checkOnly) {
|
|
204
|
-
log('⚠️ CLAUDE.md
|
|
176
|
+
if (!existed) { log('⚠️ No CLAUDE.md found'); return false; }
|
|
177
|
+
if (report.state === 'no-marker') { log('⚠️ CLAUDE.md missing subagent protocol section'); return false; }
|
|
178
|
+
log('⚠️ CLAUDE.md moflo section needs update');
|
|
205
179
|
return false;
|
|
206
180
|
}
|
|
207
181
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
writeFileSync(claudeMdPath,
|
|
211
|
-
|
|
182
|
+
const result = applyInjectionReplacement(content, CLAUDE_MD_SECTION);
|
|
183
|
+
if (!result.changed) return true;
|
|
184
|
+
writeFileSync(claudeMdPath, result.contents, 'utf-8');
|
|
185
|
+
|
|
186
|
+
switch (report.state) {
|
|
187
|
+
case 'no-file': log('📝 Creating CLAUDE.md with subagent protocol section'); break;
|
|
188
|
+
case 'no-marker': log('📝 Added subagent protocol section to CLAUDE.md'); break;
|
|
189
|
+
case 'legacy-marker': log('📝 Replaced legacy CLAUDE.md section with minimal moflo injection'); break;
|
|
190
|
+
case 'drifted': log('📝 Updated CLAUDE.md moflo section'); break;
|
|
191
|
+
}
|
|
212
192
|
return true;
|
|
213
193
|
}
|
|
214
194
|
|