moflo 4.9.7 → 4.9.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/bin/index-all.mjs CHANGED
@@ -2,9 +2,13 @@
2
2
  /**
3
3
  * Sequential indexer chain for session-start.
4
4
  *
5
- * Runs all DB-writing indexers one at a time to avoid sql.js last-write-wins
6
- * concurrency issues (#78), then triggers HNSW rebuild once everything is
7
- * committed (#81).
5
+ * Each step is gated independently see `lib/index-fingerprint.mjs`. The
6
+ * orchestrator just walks the plan, asks the gate per step, runs the
7
+ * survivors, and saves the post-run fingerprint when each succeeds.
8
+ *
9
+ * Steps run sequentially (DB-writing) to avoid sql.js last-write-wins
10
+ * concurrency issues (#78). HNSW rebuild is last, after every other step
11
+ * has committed (#81).
8
12
  *
9
13
  * Spawned as a single detached background process by hooks.mjs session-start.
10
14
  */
@@ -15,7 +19,12 @@ import { fileURLToPath } from 'url';
15
19
  import { spawn, spawnSync } from 'child_process';
16
20
  import { platform } from 'os';
17
21
  import { hnswIndexPath } from './lib/moflo-paths.mjs';
18
- import { decideIndexGate, saveFingerprint } from './lib/index-fingerprint.mjs';
22
+ import {
23
+ decideStepGate,
24
+ computeStepFingerprint,
25
+ saveStepFingerprint,
26
+ cleanupLegacyFingerprint,
27
+ } from './lib/index-fingerprint.mjs';
19
28
 
20
29
  // Cap fastembed/ONNX thread count when spawning the heavy steps. Without
21
30
  // this, ONNX defaults to one thread per CPU core (22+ on a modern dev box),
@@ -66,7 +75,6 @@ function resolveBin(binName, localScript) {
66
75
 
67
76
  function getLocalCliPath() {
68
77
  const paths = [
69
- resolve(projectRoot, 'node_modules/moflo/bin/cli.js'),
70
78
  resolve(projectRoot, 'node_modules/moflo/bin/cli.js'),
71
79
  resolve(projectRoot, 'node_modules/.bin/flo'),
72
80
  // Development: local CLI
@@ -159,128 +167,140 @@ function runStep(label, cmd, args, timeoutMs = 120_000, extraEnv = null) {
159
167
  });
160
168
  }
161
169
 
162
- async function main() {
163
- const startTime = Date.now();
164
-
165
- // ── Fingerprint gate ─────────────────────────────────────────────────────
166
- // Skip the entire chain when none of {memory.db, moflo pkg, moflo.yaml,
167
- // .claude/guidance/} have changed since the last successful run. Without
168
- // this gate the chain re-embeds + rebuilds HNSW on every Claude session-
169
- // start even when nothing changed — the customer-visible CPU peg this
170
- // module exists to fix. Override with FLO_FORCE_INDEX=1.
171
- const gate = decideIndexGate(projectRoot);
172
- if (gate.skip) {
173
- log(`SKIP full chain — ${gate.reason} (no inputs changed since last run)`);
174
- return;
175
- }
176
- log(`Sequential indexing chain started (gate: ${gate.reason})`);
177
-
178
- // 1. Guidance indexer
179
- if (isIndexEnabled('guidance')) {
180
- const guidanceScript = resolveBin('flo-index', 'index-guidance.mjs');
181
- if (guidanceScript) {
182
- await runStep('guidance-index', 'node', [guidanceScript, '--no-embeddings']);
183
- } else {
184
- log('SKIP guidance-index (script not found)');
185
- }
186
- } else {
187
- log('SKIP guidance-index (disabled in moflo.yaml)');
188
- }
170
+ /**
171
+ * Build the ordered step plan. Each entry is `{ name, cmd, args, timeoutMs, env? }`.
172
+ * Steps disabled in moflo.yaml or whose script can't be located are filtered
173
+ * out here so the run loop only sees runnable steps.
174
+ */
175
+ function buildStepPlan() {
176
+ const plan = [];
177
+ const localCli = getLocalCliPath();
189
178
 
190
- // 2. Code map generator (the big one ~22s)
191
- if (isIndexEnabled('code_map')) {
192
- const codeMapScript = resolveBin('flo-codemap', 'generate-code-map.mjs');
193
- if (codeMapScript) {
194
- await runStep('code-map', 'node', [codeMapScript, '--no-embeddings'], 180_000);
195
- } else {
196
- log('SKIP code-map (script not found)');
179
+ const consider = (name, cfgKey, scriptName, binName, args, timeoutMs = 120_000, env = null) => {
180
+ if (cfgKey && !isIndexEnabled(cfgKey)) {
181
+ log(`SKIP ${name} (disabled in moflo.yaml)`);
182
+ return;
197
183
  }
198
- } else {
199
- log('SKIP code-map (disabled in moflo.yaml)');
200
- }
201
-
202
- // 3. Test indexer
203
- if (isIndexEnabled('tests')) {
204
- const testScript = resolveBin('flo-testmap', 'index-tests.mjs');
205
- if (testScript) {
206
- await runStep('test-index', 'node', [testScript, '--no-embeddings']);
207
- } else {
208
- log('SKIP test-index (script not found)');
184
+ const script = scriptName ? resolveBin(binName, scriptName) : null;
185
+ if (scriptName && !script) {
186
+ log(`SKIP ${name} (script not found)`);
187
+ return;
209
188
  }
210
- } else {
211
- log('SKIP test-index (disabled in moflo.yaml)');
212
- }
189
+ plan.push({
190
+ name,
191
+ cmd: 'node',
192
+ args: scriptName ? [script, ...args] : args,
193
+ timeoutMs,
194
+ env,
195
+ });
196
+ };
213
197
 
214
- // 4. Patterns indexer
215
- if (isIndexEnabled('patterns')) {
216
- const patternsScript = resolveBin('flo-patterns', 'index-patterns.mjs');
217
- if (patternsScript) {
218
- await runStep('patterns-index', 'node', [patternsScript]);
219
- } else {
220
- log('SKIP patterns-index (script not found)');
221
- }
222
- } else {
223
- log('SKIP patterns-index (disabled in moflo.yaml)');
224
- }
198
+ consider('guidance-index', 'guidance', 'index-guidance.mjs', 'flo-index', ['--no-embeddings']);
199
+ consider('code-map', 'code_map', 'generate-code-map.mjs', 'flo-codemap', ['--no-embeddings'], 180_000);
200
+ consider('test-index', 'tests', 'index-tests.mjs', 'flo-testmap', ['--no-embeddings']);
201
+ consider('patterns-index', 'patterns', 'index-patterns.mjs', 'flo-patterns', []);
225
202
 
226
- // 5. Pretrain (extracts patterns from repository)
227
- const localCli = getLocalCliPath();
203
+ // Pretrain extracts patterns from the repo via the CLI subcommand. No
204
+ // direct script — invoke through the local flo binary.
228
205
  if (localCli) {
229
- await runStep('pretrain', 'node', [localCli, 'hooks', 'pretrain']);
206
+ plan.push({
207
+ name: 'pretrain',
208
+ cmd: 'node',
209
+ args: [localCli, 'hooks', 'pretrain'],
210
+ timeoutMs: 120_000,
211
+ });
230
212
  } else {
231
213
  log('SKIP pretrain (CLI not found)');
232
214
  }
233
215
 
234
- // 6. Build embeddings single pass for ALL namespaces, after all indexers finish.
235
- // Individual indexers are called with --no-embeddings to prevent background
236
- // embedding spawns that race with this chain (sql.js last-write-wins).
237
- // Thread-capped: fastembed/ONNX would otherwise pin every CPU core.
238
- const embeddingsScript = resolveBin('flo-embeddings', 'build-embeddings.mjs');
239
- if (embeddingsScript) {
240
- await runStep('build-embeddings', 'node', [embeddingsScript], 300_000, ONNX_THREAD_CAP);
216
+ // build-embeddings runs fastembed thread-capped to keep CPU usable.
217
+ consider('build-embeddings', null, 'build-embeddings.mjs', 'flo-embeddings', [], 300_000, ONNX_THREAD_CAP);
218
+
219
+ // HNSW MUST run last (after all DB writes are committed, #81). Same thread
220
+ // cap rebuild-index loads fastembed for stats lookups.
221
+ //
222
+ // No `--force`: the embeddings-migration service (run by the launcher
223
+ // before this chain) handles model bumps, and `build-embeddings` above
224
+ // fills any rows that lack embeddings. So `rebuild-index` finds nothing
225
+ // to embed in steady state and takes the no-work path, which still
226
+ // refreshes the HNSW sidecar via `writeSidecarOrFail` and is followed by
227
+ // the existsSync post-check below. `--force` only added a 4000-row
228
+ // re-embed that the fingerprint gate (#858) is specifically trying to
229
+ // avoid (#859).
230
+ if (localCli) {
231
+ plan.push({
232
+ name: 'hnsw-rebuild',
233
+ cmd: 'node',
234
+ args: [localCli, 'memory', 'rebuild-index'],
235
+ timeoutMs: 300_000,
236
+ env: ONNX_THREAD_CAP,
237
+ });
241
238
  } else {
242
- log('SKIP build-embeddings (script not found)');
239
+ log('SKIP hnsw-rebuild (CLI not found)');
243
240
  }
244
241
 
245
- // 7. HNSW rebuild — MUST run last, after all writes are committed (#81).
246
- // rebuild-index now also writes the binary HNSW sidecar at
247
- // .moflo/hnsw.index, which can take longer than the default 120s on a
248
- // populated consumer DB — match build-embeddings' 300s budget.
249
- // Thread-capped: same fastembed CPU-peg risk as build-embeddings.
242
+ return plan;
243
+ }
244
+
245
+ async function main() {
246
+ const startTime = Date.now();
247
+ const plan = buildStepPlan();
248
+
249
+ let ranAny = false;
250
+ let hnswAttempted = false;
250
251
  let hnswOk = true;
251
- if (localCli) {
252
- const ok = await runStep('hnsw-rebuild', 'node', [localCli, 'memory', 'rebuild-index', '--force'], 300_000, ONNX_THREAD_CAP);
252
+
253
+ for (const step of plan) {
254
+ const gate = decideStepGate(step.name, projectRoot);
255
+ if (gate.skip) {
256
+ log(`SKIP ${step.name} (${gate.reason})`);
257
+ continue;
258
+ }
259
+ log(`RUN ${step.name} (${gate.reason})`);
260
+ if (step.name === 'hnsw-rebuild') hnswAttempted = true;
261
+ const ok = await runStep(step.name, step.cmd, step.args, step.timeoutMs, step.env || null);
253
262
  if (ok) {
254
- const sidecar = hnswIndexPath(projectRoot);
255
- if (!existsSync(sidecar)) {
256
- // Loud failure: missing sidecar means cold-start memory search
257
- // silently rebuilds from SQL on every consumer process — the exact
258
- // regression this guard exists to surface.
259
- log(`FAIL hnsw-rebuild post-check: sidecar missing at ${sidecar}`);
260
- hnswOk = false;
263
+ // POST-run fingerprint: re-compute to capture any state mutated by
264
+ // this step (e.g. build-embeddings bumping memory.db mtime). Saving
265
+ // the POST value lets next session correctly compare against the
266
+ // stable post-step state.
267
+ try {
268
+ const post = computeStepFingerprint(step.name, projectRoot);
269
+ if (!saveStepFingerprint(step.name, projectRoot, post)) {
270
+ log(`WARN ${step.name} fingerprint save failed (next session will re-run)`);
271
+ }
272
+ } catch (err) {
273
+ const msg = (err && err.message ? err.message.split('\n')[0] : 'unknown');
274
+ log(`WARN ${step.name} fingerprint compute failed: ${msg}`);
261
275
  }
262
- } else {
276
+ ranAny = true;
277
+ } else if (step.name === 'hnsw-rebuild') {
263
278
  hnswOk = false;
264
279
  }
265
- } else {
266
- log('SKIP hnsw-rebuild (CLI not found)');
267
280
  }
268
281
 
269
- const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
270
- log(`Sequential indexing chain complete (${totalElapsed}s)`);
271
-
272
- // Save the fingerprint AFTER a successful chain run. If hnsw failed, we
273
- // intentionally don't save — the next session-start will retry. Save
274
- // failures are non-fatal (next run will recompute and just re-run the
275
- // chain, no correctness hazard).
276
- if (hnswOk) {
277
- if (saveFingerprint(projectRoot, gate.current)) {
278
- log('Saved index-all fingerprint for next-session gate');
279
- } else {
280
- log('WARN fingerprint save failed (next session will re-run chain)');
282
+ // hnsw-rebuild post-check: sidecar must physically exist after the step
283
+ // ran successfully. Missing sidecar means cold-start memory search will
284
+ // silently rebuild from SQL on every consumer process — the regression
285
+ // this guard exists to surface (#854). Only meaningful when we actually
286
+ // tried to rebuild.
287
+ if (hnswAttempted && hnswOk) {
288
+ const sidecar = hnswIndexPath(projectRoot);
289
+ if (!existsSync(sidecar)) {
290
+ log(`FAIL hnsw-rebuild post-check: sidecar missing at ${sidecar}`);
291
+ hnswOk = false;
281
292
  }
282
293
  }
283
294
 
295
+ // Always tidy up the v1 fingerprint file from 4.9.7 — even on all-skip
296
+ // sessions, otherwise the orphan survives indefinitely.
297
+ cleanupLegacyFingerprint(projectRoot);
298
+
299
+ const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
300
+ log(ranAny
301
+ ? `Sequential indexing chain complete (${totalElapsed}s)`
302
+ : `Sequential indexing chain skipped — all steps gated unchanged (${totalElapsed}s)`);
303
+
284
304
  if (!hnswOk) process.exit(1);
285
305
  }
286
306
 
Binary file
@@ -14,6 +14,17 @@ import { fileURLToPath } from 'url';
14
14
  import { mofloDir } from './lib/moflo-paths.mjs';
15
15
  import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
16
16
 
17
+ // Headless skip (#860). The daemon's headless workers spawn `claude --print`
18
+ // with CLAUDE_CODE_HEADLESS=true (see src/cli/services/headless-worker-
19
+ // executor.ts). Each spawned Claude inherits SessionStart hooks, which
20
+ // would re-enter this launcher and fork the indexer chain — bumping
21
+ // memory.db mtime, invalidating the 4.9.7 fingerprint gate, and pegging
22
+ // CPU on the daemon's 15-min worker cycle.
23
+ if (process.env.CLAUDE_CODE_HEADLESS === 'true' || process.env.CLAUDE_CODE_HEADLESS === '1') {
24
+ emitWarning('session-start-launcher skipped (CLAUDE_CODE_HEADLESS=true)');
25
+ process.exit(0);
26
+ }
27
+
17
28
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
29
 
19
30
  // Single source of truth for the launcher's guidance-mirror header. Section 3
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.7';
5
+ export const VERSION = '4.9.8';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.7",
3
+ "version": "4.9.8",
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",
@@ -81,7 +81,7 @@
81
81
  "@typescript-eslint/eslint-plugin": "^7.18.0",
82
82
  "@typescript-eslint/parser": "^7.18.0",
83
83
  "eslint": "^8.0.0",
84
- "moflo": "^4.9.6",
84
+ "moflo": "^4.9.7",
85
85
  "tsx": "^4.21.0",
86
86
  "typescript": "^5.9.3",
87
87
  "vitest": "^4.0.0"