moflo 4.9.36 → 4.10.0
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-agent-rules.md +12 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +34 -0
- package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
- package/.claude/guidance/shipped/moflo-subagents.md +4 -0
- package/.claude/helpers/gate.cjs +3 -3
- package/.claude/helpers/statusline.cjs +69 -33
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/.claude/skills/eldar/SKILL.md +8 -0
- package/bin/build-embeddings.mjs +6 -20
- package/bin/cli.js +5 -0
- package/bin/gate.cjs +3 -3
- package/bin/generate-code-map.mjs +4 -24
- package/bin/hooks.mjs +3 -12
- package/bin/index-all.mjs +3 -13
- package/bin/index-guidance.mjs +59 -119
- package/bin/index-patterns.mjs +6 -24
- package/bin/index-tests.mjs +4 -23
- package/bin/lib/db-repair.mjs +4 -25
- package/bin/lib/get-backend.mjs +306 -0
- package/bin/lib/incremental-write.mjs +27 -7
- package/bin/lib/moflo-paths.mjs +64 -4
- package/bin/lib/suppress-sqlite-warning.mjs +57 -0
- package/bin/migrations/knowledge-purge.mjs +7 -8
- package/bin/migrations/knowledge-to-learnings.mjs +7 -9
- package/bin/migrations/purge-doc-entries.mjs +52 -0
- package/bin/migrations/strip-context-preambles.mjs +95 -0
- package/bin/run-migrations.mjs +1 -10
- package/bin/semantic-search.mjs +11 -19
- package/bin/session-start-launcher.mjs +102 -100
- package/bin/simplify-classify.cjs +38 -17
- package/dist/src/cli/commands/daemon.js +38 -11
- package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +244 -5
- package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
- package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
- package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
- package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
- package/dist/src/cli/commands/doctor-fixes.js +30 -0
- package/dist/src/cli/commands/doctor-registry.js +14 -0
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +17 -22
- package/dist/src/cli/commands/memory.js +54 -75
- package/dist/src/cli/embeddings/persistent-cache.js +44 -83
- package/dist/src/cli/init/claudemd-generator.js +4 -0
- package/dist/src/cli/init/moflo-init.js +40 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +177 -32
- package/dist/src/cli/memory/bridge-core.js +256 -30
- package/dist/src/cli/memory/bridge-entries.js +76 -8
- package/dist/src/cli/memory/controller-registry.js +7 -2
- package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
- package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
- package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
- package/dist/src/cli/memory/daemon-backend.js +400 -0
- package/dist/src/cli/memory/daemon-write-client.js +192 -15
- package/dist/src/cli/memory/database-provider.js +57 -40
- package/dist/src/cli/memory/hnsw-persistence.js +6 -8
- package/dist/src/cli/memory/index.js +0 -1
- package/dist/src/cli/memory/memory-bridge.js +40 -8
- package/dist/src/cli/memory/memory-initializer.js +286 -220
- package/dist/src/cli/memory/rvf-migration.js +25 -11
- package/dist/src/cli/memory/sqlite-backend.js +573 -0
- package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
- package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
- package/dist/src/cli/services/daemon-dashboard.js +13 -1
- package/dist/src/cli/services/daemon-lock.js +58 -1
- package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
- package/dist/src/cli/services/embeddings-migration.js +9 -12
- package/dist/src/cli/services/ephemeral-namespace-purge.js +6 -11
- package/dist/src/cli/services/learning-service.js +12 -20
- package/dist/src/cli/services/project-root.js +69 -9
- package/dist/src/cli/services/soft-delete-purge.js +6 -11
- package/dist/src/cli/services/sqljs-migration-store.js +4 -1
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/events/event-store.js +26 -55
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -4
- package/dist/src/cli/memory/sqljs-backend.js +0 -643
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: strip the legacy `[Context from previous section:]` /
|
|
3
|
+
* `[Context from next section:]` preamble blocks from every existing chunk
|
|
4
|
+
* (#1053 S5). The chunker no longer writes them — they were a workaround for
|
|
5
|
+
* missing traversal, and once memory_get_neighbors is wired (S2),
|
|
6
|
+
* prevChunk/nextChunk metadata + a real call is the alternative path.
|
|
7
|
+
*
|
|
8
|
+
* For every chunk whose content carries a preamble marker:
|
|
9
|
+
* 1. Strip the preamble block(s) in place
|
|
10
|
+
* 2. NULL the embedding column so build-embeddings regenerates it from the
|
|
11
|
+
* cleaned content on the next indexer pass
|
|
12
|
+
*
|
|
13
|
+
* Idempotent: chunks already in the new shape (no preamble markers) are
|
|
14
|
+
* untouched.
|
|
15
|
+
*
|
|
16
|
+
* @module bin/migrations/strip-context-preambles
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync } from 'fs';
|
|
20
|
+
import { memoryDbPath } from '../lib/moflo-paths.mjs';
|
|
21
|
+
import { openBackend } from '../lib/get-backend.mjs';
|
|
22
|
+
|
|
23
|
+
export const name = 'strip-context-preambles';
|
|
24
|
+
// Run after purge-doc-entries (which itself has order=0 default). Explicit
|
|
25
|
+
// ordering keeps this independent of fs sort order.
|
|
26
|
+
export const order = 20;
|
|
27
|
+
|
|
28
|
+
// Validated against real chunks; the back-to-back `---` runs that earlier
|
|
29
|
+
// drafts mishandled are absorbed by the trailing `(?:---\n\n)*` / leading
|
|
30
|
+
// `(?:\n\n---)+` greediness.
|
|
31
|
+
const PREV_PREAMBLE = /\[Context from previous section:\][\s\S]*?\n\n---\n\n(?:---\n\n)*/g;
|
|
32
|
+
const NEXT_PREAMBLE = /(?:\n\n---)+\n\n\[Context from next section:\][\s\S]*$/g;
|
|
33
|
+
|
|
34
|
+
function strip(content) {
|
|
35
|
+
// Reset lastIndex defensively — global regex state can leak across calls
|
|
36
|
+
// when reused on a hot path.
|
|
37
|
+
PREV_PREAMBLE.lastIndex = 0;
|
|
38
|
+
NEXT_PREAMBLE.lastIndex = 0;
|
|
39
|
+
return content.replace(PREV_PREAMBLE, '').replace(NEXT_PREAMBLE, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} projectRoot
|
|
44
|
+
* @returns {Promise<{stripped:number, untouched:number}>}
|
|
45
|
+
*/
|
|
46
|
+
export async function run(projectRoot) {
|
|
47
|
+
const dbPath = memoryDbPath(projectRoot);
|
|
48
|
+
if (!existsSync(dbPath)) return { stripped: 0, untouched: 0 };
|
|
49
|
+
|
|
50
|
+
const db = await openBackend(projectRoot, { create: false });
|
|
51
|
+
|
|
52
|
+
// Only chunks can carry the preamble — the chunker is the only writer of
|
|
53
|
+
// those markers. Filter on key prefix to keep the LIKE selective; manual
|
|
54
|
+
// memory entries containing the literal string are extremely unlikely and
|
|
55
|
+
// the strip is a no-op for them anyway.
|
|
56
|
+
const stmt = db.prepare(
|
|
57
|
+
`SELECT id, content FROM memory_entries WHERE key LIKE 'chunk-%' AND status = 'active'`,
|
|
58
|
+
);
|
|
59
|
+
const rows = [];
|
|
60
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
61
|
+
stmt.free();
|
|
62
|
+
|
|
63
|
+
if (rows.length === 0) {
|
|
64
|
+
db.close();
|
|
65
|
+
return { stripped: 0, untouched: 0 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let stripped = 0;
|
|
69
|
+
let untouched = 0;
|
|
70
|
+
const update = db.prepare(`UPDATE memory_entries SET content = ?, embedding = NULL WHERE id = ?`);
|
|
71
|
+
try {
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const original = String(row.content || '');
|
|
74
|
+
// Cheap prefix-check to avoid running the regex on chunks that have no
|
|
75
|
+
// preamble — covers the common idempotent re-run case in O(1).
|
|
76
|
+
if (!original.includes('[Context from previous section:]') && !original.includes('[Context from next section:]')) {
|
|
77
|
+
untouched++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const cleaned = strip(original);
|
|
81
|
+
if (cleaned === original) {
|
|
82
|
+
untouched++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
update.run([cleaned, row.id]);
|
|
86
|
+
stripped++;
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
update.free();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (stripped > 0) db.save();
|
|
93
|
+
db.close();
|
|
94
|
+
return { stripped, untouched };
|
|
95
|
+
}
|
package/bin/run-migrations.mjs
CHANGED
|
@@ -23,19 +23,10 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
23
23
|
import { resolve, dirname } from 'path';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
25
25
|
import { hasMigrationRun, markMigrationDone, listMigrations, clearMigration } from './lib/migrations.mjs';
|
|
26
|
+
import { findProjectRoot } from './lib/moflo-paths.mjs';
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
|
|
29
|
-
function findProjectRoot() {
|
|
30
|
-
let dir = process.cwd();
|
|
31
|
-
const root = resolve(dir, '/');
|
|
32
|
-
while (dir !== root) {
|
|
33
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
34
|
-
dir = dirname(dir);
|
|
35
|
-
}
|
|
36
|
-
return process.cwd();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
30
|
const projectRoot = findProjectRoot();
|
|
40
31
|
const args = process.argv.slice(2);
|
|
41
32
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
package/bin/semantic-search.mjs
CHANGED
|
@@ -17,23 +17,12 @@
|
|
|
17
17
|
* flo-search "query" --threshold 0.3
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { existsSync
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
|
|
20
|
+
import { existsSync } from 'fs';
|
|
21
|
+
import { mofloInternalURL } from './lib/moflo-resolve.mjs';
|
|
22
|
+
import { memoryDbPath, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
23
|
+
import { openBackend } from './lib/get-backend.mjs';
|
|
25
24
|
const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
|
|
26
25
|
|
|
27
|
-
function findProjectRoot() {
|
|
28
|
-
let dir = process.cwd();
|
|
29
|
-
const root = resolve(dir, '/');
|
|
30
|
-
while (dir !== root) {
|
|
31
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
32
|
-
dir = dirname(dir);
|
|
33
|
-
}
|
|
34
|
-
return process.cwd();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
const projectRoot = findProjectRoot();
|
|
38
27
|
const DB_PATH = memoryDbPath(projectRoot);
|
|
39
28
|
|
|
@@ -108,9 +97,9 @@ async function getDb() {
|
|
|
108
97
|
if (!existsSync(DB_PATH)) {
|
|
109
98
|
throw new Error(`Database not found: ${DB_PATH}`);
|
|
110
99
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return
|
|
100
|
+
// Read-only: search must never trigger WAL creation in a freshly-cloned
|
|
101
|
+
// consumer repo, and the factory guarantees the same shape across engines.
|
|
102
|
+
return openBackend(projectRoot, { create: false, readOnly: true });
|
|
114
103
|
}
|
|
115
104
|
|
|
116
105
|
async function semanticSearch(queryText, options = {}) {
|
|
@@ -164,6 +153,7 @@ async function semanticSearch(queryText, options = {}) {
|
|
|
164
153
|
preview: entry.content.substring(0, 150).replace(/\n/g, ' '),
|
|
165
154
|
type: metadata.type || 'unknown',
|
|
166
155
|
parentDoc: metadata.parentDoc || null,
|
|
156
|
+
parentPath: metadata.parentPath || null,
|
|
167
157
|
chunkTitle: metadata.chunkTitle || null,
|
|
168
158
|
});
|
|
169
159
|
} catch (err) {
|
|
@@ -262,7 +252,9 @@ async function main() {
|
|
|
262
252
|
console.log(` Key: ${top.key}`);
|
|
263
253
|
console.log(` Score: ${top.score.toFixed(4)}`);
|
|
264
254
|
if (top.chunkTitle) console.log(` Section: ${top.chunkTitle}`);
|
|
265
|
-
|
|
255
|
+
// #1053 S4: doc-* retired — parentPath is the actionable source location.
|
|
256
|
+
if (top.parentPath) console.log(` Parent: ${top.parentPath}`);
|
|
257
|
+
else if (top.parentDoc) console.log(` Parent: ${top.parentDoc}`);
|
|
266
258
|
console.log(` Preview: ${top.preview}...`);
|
|
267
259
|
} catch (err) {
|
|
268
260
|
console.error(`[semantic-search] Error: ${err.message}`);
|
|
@@ -11,7 +11,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, mkdirSync, statSync } from 'fs';
|
|
12
12
|
import { resolve, dirname, join } from 'path';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
|
-
import { mofloDir } from './lib/moflo-paths.mjs';
|
|
14
|
+
import { mofloDir, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
15
15
|
import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
|
|
16
16
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
17
17
|
import { applyRetiredPrune } from './lib/retired-files.mjs';
|
|
@@ -39,20 +39,13 @@ function sessionStartMirrorHeader(file) {
|
|
|
39
39
|
return `${SESSION_START_MIRROR_MARKER} Do not edit — changes will be overwritten. -->\n<!-- Source: node_modules/moflo/.claude/guidance/shipped/${file} -->\n\n`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Detect project root by walking up from cwd to find package.json.
|
|
43
42
|
// IMPORTANT: Do NOT use resolve(__dirname, '..') or '../..' — this script lives
|
|
44
43
|
// in bin/ during development but gets synced to .claude/scripts/ in consumer
|
|
45
|
-
// projects, so __dirname-relative paths break. findProjectRoot()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (existsSync(resolve(dir, 'package.json'))) return dir;
|
|
51
|
-
dir = dirname(dir);
|
|
52
|
-
}
|
|
53
|
-
return process.cwd();
|
|
54
|
-
}
|
|
55
|
-
|
|
44
|
+
// projects, so __dirname-relative paths break. findProjectRoot() (lib/moflo-
|
|
45
|
+
// paths.mjs) resolves identically to the TS bridge (#1057): CLAUDE_PROJECT_DIR
|
|
46
|
+
// first, then walk up for .moflo/moflo.db / .swarm/memory.db / CLAUDE.md+pkg /
|
|
47
|
+
// package.json / .git. Inline walks here have caused N writers to land on
|
|
48
|
+
// different DBs than the bridge reads from — never reintroduce one.
|
|
56
49
|
const projectRoot = findProjectRoot();
|
|
57
50
|
|
|
58
51
|
// Dogfood guard (#928). When this launcher runs inside the moflo repo itself,
|
|
@@ -884,37 +877,42 @@ try {
|
|
|
884
877
|
// longer exists in source, calling a require helper that prints the warning
|
|
885
878
|
// every time `neural_predict` / `neural_patterns` fires.
|
|
886
879
|
//
|
|
887
|
-
// Fix: compare the daemon-lock's `
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
880
|
+
// Fix (epic #1054): compare the daemon-lock's reported moflo `version` against
|
|
881
|
+
// the installed `node_modules/moflo/package.json` version. If they differ —
|
|
882
|
+
// or the lock predates #1054 and has no `version` field at all — recycle the
|
|
883
|
+
// daemon. This is exact (not a heuristic margin like the prior mtime-based
|
|
884
|
+
// check) and named explicitly so the doctor's Daemon Version Skew check
|
|
885
|
+
// (#1059) can share the diagnosis.
|
|
891
886
|
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
const STALE_DAEMON_MTIME_SKEW_MS = 5_000;
|
|
887
|
+
// Pre-#1054 daemons have no `version` in their lock payload — treated as a
|
|
888
|
+
// mismatch by definition because by construction they were launched before
|
|
889
|
+
// version publishing existed.
|
|
896
890
|
try {
|
|
897
891
|
const mofloPkgPathForRecycle = resolve(projectRoot, 'node_modules/moflo/package.json');
|
|
898
892
|
const lockFile = resolve(projectRoot, '.moflo', 'daemon.lock');
|
|
899
|
-
// Cheap stat first — if
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
// Quick reject: if the lock file itself is younger than the install, the
|
|
904
|
-
// daemon was started after install — no read of lock contents needed.
|
|
905
|
-
if (installedAt - lockMtime > STALE_DAEMON_MTIME_SKEW_MS) {
|
|
906
|
-
let daemonStartedAt = 0;
|
|
893
|
+
// Cheap stat first — if either file is gone, no skew check is possible.
|
|
894
|
+
if (existsSync(mofloPkgPathForRecycle) && existsSync(lockFile)) {
|
|
895
|
+
const installedVersion = JSON.parse(readFileSync(mofloPkgPathForRecycle, 'utf-8')).version;
|
|
896
|
+
let daemonVersion;
|
|
907
897
|
try {
|
|
908
898
|
const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
909
|
-
if (typeof lock?.
|
|
910
|
-
} catch { /* corrupt lock —
|
|
911
|
-
if (
|
|
912
|
-
if (recycleDaemon(lockFile, 'daemon-
|
|
913
|
-
|
|
899
|
+
if (typeof lock?.version === 'string') daemonVersion = lock.version;
|
|
900
|
+
} catch { /* corrupt lock — recycleDaemon will unlink it */ }
|
|
901
|
+
if (daemonVersion !== installedVersion) {
|
|
902
|
+
if (recycleDaemon(lockFile, 'daemon-version-skew')) {
|
|
903
|
+
const observed = daemonVersion ?? '<pre-1054 / unknown>';
|
|
904
|
+
emitMutation(
|
|
905
|
+
'recycled stale daemon',
|
|
906
|
+
`version skew: installed ${installedVersion}, daemon ${observed}`,
|
|
907
|
+
);
|
|
914
908
|
}
|
|
915
909
|
}
|
|
916
910
|
}
|
|
917
|
-
} catch {
|
|
911
|
+
} catch (err) {
|
|
912
|
+
// Non-fatal; surface via emitWarning per feedback_no_layered_workarounds —
|
|
913
|
+
// no silent catch on the upgrade path (#854).
|
|
914
|
+
emitWarning(`daemon version-skew check failed: ${errMessage(err)}`);
|
|
915
|
+
}
|
|
918
916
|
|
|
919
917
|
// ── 3a. Auto-migrate settings.json (npx flo → node helpers, PATH setup) ────
|
|
920
918
|
// Existing users may have stale settings.json with `npx flo` hooks that break
|
|
@@ -1490,6 +1488,75 @@ try {
|
|
|
1490
1488
|
} catch { /* writing the failure itself must not throw */ }
|
|
1491
1489
|
}
|
|
1492
1490
|
|
|
1491
|
+
// ── 3e-1057. Run unmet schema migrations BEFORE daemon spawn ────────────────
|
|
1492
|
+
// run-migrations.mjs walks `bin/migrations/*.mjs` and invokes each that has
|
|
1493
|
+
// not been recorded in `.moflo/migrations.json`. Each migration opens sql.js
|
|
1494
|
+
// directly and persists with atomicWriteFileSync. They MUST run before the
|
|
1495
|
+
// daemon spawns or the daemon's in-RAM snapshot races their on-disk writes
|
|
1496
|
+
// — the bug class S2 detects (#1056 version-skew kill) and S3 (#1057)
|
|
1497
|
+
// closes for good. Pre-#1057 this ran in Section 4 AFTER `hooks session-
|
|
1498
|
+
// start` fired off the daemon, which is exactly the race fixed here.
|
|
1499
|
+
//
|
|
1500
|
+
// Synchronous on purpose: blocking by ≤30s on a one-time migration is the
|
|
1501
|
+
// right trade vs. an inconsistent post-upgrade DB. The runner short-circuits
|
|
1502
|
+
// to a no-op when nothing is pending, so steady-state cost is just the node
|
|
1503
|
+
// startup + ESM graph (~80ms on a warm fs).
|
|
1504
|
+
const runMigrations = resolveMofloBin(projectRoot, null, 'run-migrations.mjs');
|
|
1505
|
+
if (runMigrations) {
|
|
1506
|
+
runMigrationsAndAnnounce(runMigrations);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function runMigrationsAndAnnounce(runnerPath) {
|
|
1510
|
+
let raw;
|
|
1511
|
+
try {
|
|
1512
|
+
raw = execFileSync('node', [runnerPath], {
|
|
1513
|
+
cwd: projectRoot,
|
|
1514
|
+
timeout: 30_000,
|
|
1515
|
+
encoding: 'utf-8',
|
|
1516
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
1517
|
+
});
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
// Migrations are best-effort — a failure here must never block session
|
|
1520
|
+
// start. But silent swallowing hides hangs (30s timeout) and corrupted
|
|
1521
|
+
// DBs from the user, so leave a stderr crumb.
|
|
1522
|
+
process.stderr.write(`moflo: migration runner failed (${err.code || err.message}); will retry next session\n`);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const labels = {
|
|
1527
|
+
'knowledge-to-learnings': 'consolidated knowledge → learnings',
|
|
1528
|
+
'knowledge-purge': 'removed legacy knowledge namespace rows',
|
|
1529
|
+
'purge-doc-entries': 'pruned legacy doc-* rows (chunk-only RAG, #1053)',
|
|
1530
|
+
'strip-context-preambles': 'stripped chunk preambles; embeddings will rebuild on next index pass (#1053)',
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1534
|
+
const m = line.match(/^\[migrations\]\s+([\w-]+):\s+done\s+in\s+\d+ms\s*(.*)$/);
|
|
1535
|
+
if (!m) continue;
|
|
1536
|
+
const migrationName = m[1];
|
|
1537
|
+
let parsed = null;
|
|
1538
|
+
try { parsed = m[2] ? JSON.parse(m[2]) : null; } catch { parsed = null; }
|
|
1539
|
+
|
|
1540
|
+
// Silent fast-path: don't announce zero-work runs (no point telling the
|
|
1541
|
+
// user the launcher did nothing). If every numeric detail field is 0,
|
|
1542
|
+
// skip the emit. Stamped migrations don't even reach this loop because
|
|
1543
|
+
// the runner short-circuits via the manifest.
|
|
1544
|
+
if (parsed) {
|
|
1545
|
+
const nums = Object.values(parsed).filter((v) => typeof v === 'number');
|
|
1546
|
+
if (nums.length > 0 && nums.every((v) => v === 0)) continue;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
let detail = '';
|
|
1550
|
+
if (parsed) {
|
|
1551
|
+
if (typeof parsed.purged === 'number') detail = `${parsed.purged} ${parsed.purged === 1 ? 'row' : 'rows'}`;
|
|
1552
|
+
else if (typeof parsed.rowsMigrated === 'number') detail = `${parsed.rowsMigrated} ${parsed.rowsMigrated === 1 ? 'entry' : 'entries'}`;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const label = labels[migrationName] || `migration ${migrationName}`;
|
|
1556
|
+
emitMutation(label, detail);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1493
1560
|
// ── 3f. Flip the upgrade notice to "completed" (#636, #738) ─────────────────
|
|
1494
1561
|
// See the TTL rationale at the constants above for why we switch to a
|
|
1495
1562
|
// short-TTL completed badge instead of clearing the file.
|
|
@@ -1569,71 +1636,6 @@ if (hooksScript) {
|
|
|
1569
1636
|
fireAndForget('node', [hooksScript, 'session-start'], 'hooks session-start');
|
|
1570
1637
|
}
|
|
1571
1638
|
|
|
1572
|
-
// Migration runner — consults `.moflo/migrations.json` and runs only
|
|
1573
|
-
// migrations that haven't been recorded. Fast-paths to a no-op when the
|
|
1574
|
-
// manifest is current; the runner module loads with lazy sql.js init in
|
|
1575
|
-
// each migration, so a stamped session pays only node startup + ESM graph.
|
|
1576
|
-
//
|
|
1577
|
-
// Prefer the npm-package path so first-install consumers run unmet
|
|
1578
|
-
// migrations without waiting for a script-sync round-trip.
|
|
1579
|
-
//
|
|
1580
|
-
// Run synchronously (capture stdout) so each completed migration surfaces
|
|
1581
|
-
// through emitMutation — Claude's session-start hook captures launcher
|
|
1582
|
-
// stdout and that's the only channel that reaches the user.
|
|
1583
|
-
const runMigrations = resolveMofloBin(projectRoot, null, 'run-migrations.mjs');
|
|
1584
|
-
if (runMigrations) {
|
|
1585
|
-
runMigrationsAndAnnounce(runMigrations);
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
function runMigrationsAndAnnounce(runnerPath) {
|
|
1589
|
-
let raw;
|
|
1590
|
-
try {
|
|
1591
|
-
raw = execFileSync('node', [runnerPath], {
|
|
1592
|
-
cwd: projectRoot,
|
|
1593
|
-
timeout: 30_000,
|
|
1594
|
-
encoding: 'utf-8',
|
|
1595
|
-
stdio: ['ignore', 'pipe', 'inherit'],
|
|
1596
|
-
});
|
|
1597
|
-
} catch (err) {
|
|
1598
|
-
// Migrations are best-effort — a failure here must never block session
|
|
1599
|
-
// start. But silent swallowing hides hangs (30s timeout) and corrupted
|
|
1600
|
-
// DBs from the user, so leave a stderr crumb.
|
|
1601
|
-
process.stderr.write(`moflo: migration runner failed (${err.code || err.message}); will retry next session\n`);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const labels = {
|
|
1606
|
-
'knowledge-to-learnings': 'consolidated knowledge → learnings',
|
|
1607
|
-
'knowledge-purge': 'removed legacy knowledge namespace rows',
|
|
1608
|
-
};
|
|
1609
|
-
|
|
1610
|
-
for (const line of raw.split('\n')) {
|
|
1611
|
-
const m = line.match(/^\[migrations\]\s+([\w-]+):\s+done\s+in\s+\d+ms\s*(.*)$/);
|
|
1612
|
-
if (!m) continue;
|
|
1613
|
-
const migrationName = m[1];
|
|
1614
|
-
let parsed = null;
|
|
1615
|
-
try { parsed = m[2] ? JSON.parse(m[2]) : null; } catch { parsed = null; }
|
|
1616
|
-
|
|
1617
|
-
// Silent fast-path: don't announce zero-work runs (no point telling the
|
|
1618
|
-
// user the launcher did nothing). If every numeric detail field is 0,
|
|
1619
|
-
// skip the emit. Stamped migrations don't even reach this loop because
|
|
1620
|
-
// the runner short-circuits via the manifest.
|
|
1621
|
-
if (parsed) {
|
|
1622
|
-
const nums = Object.values(parsed).filter((v) => typeof v === 'number');
|
|
1623
|
-
if (nums.length > 0 && nums.every((v) => v === 0)) continue;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
let detail = '';
|
|
1627
|
-
if (parsed) {
|
|
1628
|
-
if (typeof parsed.purged === 'number') detail = `${parsed.purged} ${parsed.purged === 1 ? 'row' : 'rows'}`;
|
|
1629
|
-
else if (typeof parsed.rowsMigrated === 'number') detail = `${parsed.rowsMigrated} ${parsed.rowsMigrated === 1 ? 'entry' : 'entries'}`;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
const label = labels[migrationName] || `migration ${migrationName}`;
|
|
1633
|
-
emitMutation(label, detail);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
1639
|
// Patches are now baked into moflo@4.0.0 source — no runtime patching needed.
|
|
1638
1640
|
|
|
1639
1641
|
// ── 5. Done — exit immediately ──────────────────────────────────────────────
|
|
@@ -45,34 +45,55 @@ const SECURITY_PATHS = [
|
|
|
45
45
|
/(?:^|[\\\/])\.claude[\\\/]helpers[\\\/]gate/i,
|
|
46
46
|
];
|
|
47
47
|
|
|
48
|
-
function safeExec(cmd) {
|
|
49
|
-
try {
|
|
50
|
-
|
|
48
|
+
function safeExec(cmd, opts) {
|
|
49
|
+
try {
|
|
50
|
+
return execSync(cmd, {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
...(opts && opts.cwd ? { cwd: opts.cwd } : {}),
|
|
54
|
+
});
|
|
55
|
+
} catch { return ''; }
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// Detect the consumer's default branch. Hardcoding 'main' silently miscalibrates
|
|
54
59
|
// classification on repos that use 'master', 'develop', etc. — empty diff →
|
|
55
60
|
// TRIVIAL → gate stamps clean without any real review.
|
|
56
61
|
let _cachedDefaultBranch = null;
|
|
57
|
-
function detectDefaultBranch() {
|
|
58
|
-
|
|
62
|
+
function detectDefaultBranch(cwd) {
|
|
63
|
+
// Cache by cwd so tests probing multiple repos in-process don't return a
|
|
64
|
+
// single stale value; CLI use passes no cwd and benefits from the cache.
|
|
65
|
+
if (cwd === undefined && _cachedDefaultBranch !== null) return _cachedDefaultBranch;
|
|
66
|
+
const opts = cwd ? { cwd } : undefined;
|
|
59
67
|
|
|
60
68
|
// Preferred: origin/HEAD points to whatever the remote considers default.
|
|
61
|
-
const symbolic = safeExec('git symbolic-ref --short refs/remotes/origin/HEAD').trim();
|
|
62
|
-
if (symbolic.startsWith('origin/'))
|
|
69
|
+
const symbolic = safeExec('git symbolic-ref --short refs/remotes/origin/HEAD', opts).trim();
|
|
70
|
+
if (symbolic.startsWith('origin/')) {
|
|
71
|
+
const v = symbolic.slice('origin/'.length);
|
|
72
|
+
if (cwd === undefined) _cachedDefaultBranch = v;
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
63
75
|
|
|
64
76
|
// Fallback: local init.defaultBranch (set by `git init -b <name>` or config).
|
|
65
|
-
const configured = safeExec('git config --get init.defaultBranch').trim();
|
|
66
|
-
if (configured)
|
|
77
|
+
const configured = safeExec('git config --get init.defaultBranch', opts).trim();
|
|
78
|
+
if (configured) {
|
|
79
|
+
if (cwd === undefined) _cachedDefaultBranch = configured;
|
|
80
|
+
return configured;
|
|
81
|
+
}
|
|
67
82
|
|
|
68
83
|
// Last resort: 'main' (most common modern default).
|
|
69
|
-
|
|
84
|
+
if (cwd === undefined) _cachedDefaultBranch = 'main';
|
|
85
|
+
return 'main';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _resetCacheForTest() {
|
|
89
|
+
_cachedDefaultBranch = null;
|
|
70
90
|
}
|
|
71
91
|
|
|
72
|
-
function readDiffFromGit(base) {
|
|
92
|
+
function readDiffFromGit(base, cwd) {
|
|
93
|
+
const opts = cwd ? { cwd } : undefined;
|
|
73
94
|
// Combined diff: committed-since-base + working-tree
|
|
74
|
-
const committed = safeExec(`git diff ${base}...HEAD
|
|
75
|
-
const working = safeExec('git diff HEAD');
|
|
95
|
+
const committed = safeExec(`git diff ${base}...HEAD`, opts);
|
|
96
|
+
const working = safeExec('git diff HEAD', opts);
|
|
76
97
|
return committed + (working ? '\n' + working : '');
|
|
77
98
|
}
|
|
78
99
|
|
|
@@ -206,9 +227,9 @@ function classifyDiff(diffText) {
|
|
|
206
227
|
return decide(parseDiff(diffText));
|
|
207
228
|
}
|
|
208
229
|
|
|
209
|
-
function classifyFromGit(base) {
|
|
210
|
-
const resolved = base || detectDefaultBranch();
|
|
211
|
-
return classifyDiff(readDiffFromGit(resolved));
|
|
230
|
+
function classifyFromGit(base, cwd) {
|
|
231
|
+
const resolved = base || detectDefaultBranch(cwd);
|
|
232
|
+
return classifyDiff(readDiffFromGit(resolved, cwd));
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
if (require.main === module) {
|
|
@@ -232,4 +253,4 @@ if (require.main === module) {
|
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
255
|
|
|
235
|
-
module.exports = { parseDiff, decide, classifyDiff, classifyFromGit, detectDefaultBranch };
|
|
256
|
+
module.exports = { parseDiff, decide, classifyDiff, classifyFromGit, detectDefaultBranch, _resetCacheForTest };
|
|
@@ -14,6 +14,32 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
14
14
|
import { join, resolve } from 'path';
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the dashboard port from CLI flag and env, in that precedence order.
|
|
19
|
+
*
|
|
20
|
+
* Precedence (highest first):
|
|
21
|
+
* 1. `--dashboard-port` flag (explicit caller intent)
|
|
22
|
+
* 2. `MOFLO_DAEMON_PORT` env (shared contract with `daemon-write-client.ts`)
|
|
23
|
+
* 3. `DEFAULT_DASHBOARD_PORT` (3117)
|
|
24
|
+
*
|
|
25
|
+
* The env fallback (#1067) eliminates the client/server asymmetry: prior to
|
|
26
|
+
* this, the client honored `MOFLO_DAEMON_PORT` but the server only read
|
|
27
|
+
* `--dashboard-port`. A consumer pinning the env (e.g. the smoke harness)
|
|
28
|
+
* would point clients at one port while the server bound the default.
|
|
29
|
+
*
|
|
30
|
+
* Exported for unit testing — the command handler calls this once per start.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveDashboardPort(flagValue, envValue) {
|
|
33
|
+
const source = flagValue ?? envValue;
|
|
34
|
+
if (!source)
|
|
35
|
+
return { ok: true, port: DEFAULT_DASHBOARD_PORT };
|
|
36
|
+
const parsed = parseInt(source, 10);
|
|
37
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
38
|
+
const label = flagValue ? 'dashboard port' : 'MOFLO_DAEMON_PORT';
|
|
39
|
+
return { ok: false, error: `Invalid ${label}: ${source} (must be 1-65535)` };
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, port: parsed };
|
|
42
|
+
}
|
|
17
43
|
// Start daemon subcommand
|
|
18
44
|
const startCommand = {
|
|
19
45
|
name: 'start',
|
|
@@ -43,16 +69,13 @@ const startCommand = {
|
|
|
43
69
|
const rawDashboardPort = ctx.flags.dashboardPort;
|
|
44
70
|
const projectRoot = process.cwd();
|
|
45
71
|
const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
output.printError(`Invalid dashboard port: ${rawDashboardPort} (must be 1-65535)`);
|
|
52
|
-
return { success: false, exitCode: 1 };
|
|
53
|
-
}
|
|
54
|
-
dashboardPort = parsed;
|
|
72
|
+
// Resolve dashboard port; see `resolveDashboardPort` for precedence.
|
|
73
|
+
const portResult = resolveDashboardPort(rawDashboardPort, process.env.MOFLO_DAEMON_PORT);
|
|
74
|
+
if (!portResult.ok) {
|
|
75
|
+
output.printError(portResult.error);
|
|
76
|
+
return { success: false, exitCode: 1 };
|
|
55
77
|
}
|
|
78
|
+
const dashboardPort = portResult.port;
|
|
56
79
|
// Parse resource threshold overrides from CLI flags
|
|
57
80
|
const config = {};
|
|
58
81
|
const rawMaxCpu = ctx.flags.maxCpuLoad;
|
|
@@ -433,9 +456,13 @@ const stopCommand = {
|
|
|
433
456
|
},
|
|
434
457
|
};
|
|
435
458
|
/**
|
|
436
|
-
* Kill background daemon process using lock file
|
|
459
|
+
* Kill background daemon process using lock file.
|
|
460
|
+
*
|
|
461
|
+
* Exported so `memory init --force` can stop the daemon before unlinking
|
|
462
|
+
* moflo.db — on Windows the daemon's open file handle otherwise blocks
|
|
463
|
+
* unlinkSync with EBUSY (#1098).
|
|
437
464
|
*/
|
|
438
|
-
async function killBackgroundDaemon(projectRoot) {
|
|
465
|
+
export async function killBackgroundDaemon(projectRoot) {
|
|
439
466
|
const holderPid = getDaemonLockHolder(projectRoot);
|
|
440
467
|
if (!holderPid) {
|
|
441
468
|
// No live daemon — clean up any stale lock
|