moflo 4.10.20 → 4.10.21
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 -0
- package/.claude/guidance/shipped/moflo-core-guidance.md +1 -0
- package/.claude/guidance/shipped/moflo-skills-reference.md +108 -0
- package/.claude/guidance/shipped/moflo-yaml-reference.md +13 -0
- package/.claude/skills/commune/SKILL.md +140 -0
- package/.claude/skills/divine/SKILL.md +130 -0
- package/.claude/skills/meditate/SKILL.md +122 -0
- package/README.md +39 -3
- package/bin/index-all.mjs +2 -1
- package/bin/index-reference.mjs +221 -0
- package/bin/lib/file-sync.mjs +50 -1
- package/bin/lib/hook-io.mjs +63 -0
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/bin/lib/internal-skills.mjs +16 -0
- package/bin/lib/meditate.mjs +497 -0
- package/bin/lib/pii-scrub.mjs +119 -0
- package/bin/lib/reference-docs.mjs +218 -0
- package/bin/lib/session-continuity.mjs +372 -0
- package/bin/lib/shipped-scripts.json +36 -0
- package/bin/lib/shipped-scripts.mjs +33 -0
- package/bin/lib/yaml-upgrader.mjs +62 -0
- package/bin/meditate-capture.mjs +123 -0
- package/bin/meditate-distill.mjs +121 -0
- package/bin/session-continuity.mjs +206 -0
- package/bin/session-start-launcher.mjs +140 -60
- package/dist/src/cli/config/moflo-config.js +18 -0
- package/dist/src/cli/init/executor.js +11 -17
- package/dist/src/cli/init/moflo-init.js +21 -19
- package/dist/src/cli/init/moflo-yaml-template.js +21 -0
- package/dist/src/cli/init/settings-generator.js +23 -1
- package/dist/src/cli/init/shipped-scripts.js +39 -0
- package/dist/src/cli/memory/bridge-core.js +20 -0
- package/dist/src/cli/memory/bridge-entries.js +8 -2
- package/dist/src/cli/memory/memory-bridge.js +6 -2
- package/dist/src/cli/memory/memory-initializer.js +6 -2
- package/dist/src/cli/services/hook-block-hash.js +9 -1
- package/dist/src/cli/services/hook-wiring.js +38 -0
- package/dist/src/cli/transfer/anonymization/index.js +146 -40
- package/dist/src/cli/transfer/deploy-seraphine.js +1 -1
- package/dist/src/cli/transfer/export.js +2 -2
- package/dist/src/cli/transfer/store/publish.js +1 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/scripts/post-install-bootstrap.mjs +22 -42
- package/dist/src/cli/hooks/llm/index.js +0 -11
- package/dist/src/cli/hooks/llm/llm-hooks.js +0 -382
|
@@ -15,7 +15,22 @@ import { mofloDir, findProjectRoot, findAncestorMofloRoot, COMMON_WALK_SKIP_NAME
|
|
|
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';
|
|
18
|
-
import { makeSyncer, contentEqual } from './lib/file-sync.mjs';
|
|
18
|
+
import { makeSyncer, contentEqual, syncDirRecursive } from './lib/file-sync.mjs';
|
|
19
|
+
import { INTERNAL_SKILLS } from './lib/internal-skills.mjs';
|
|
20
|
+
import { loadShippedScripts } from './lib/shipped-scripts.mjs';
|
|
21
|
+
import {
|
|
22
|
+
readContinuityConfig,
|
|
23
|
+
readGitState,
|
|
24
|
+
readDigests,
|
|
25
|
+
selectBestDigest,
|
|
26
|
+
formatInjection,
|
|
27
|
+
} from './lib/session-continuity.mjs';
|
|
28
|
+
import {
|
|
29
|
+
readMeditateConfig,
|
|
30
|
+
readLedger as readMeditateLedger,
|
|
31
|
+
pendingEntries as pendingMeditateEntries,
|
|
32
|
+
purgeLegacyFiles as purgeLegacyMeditateFiles,
|
|
33
|
+
} from './lib/meditate.mjs';
|
|
19
34
|
|
|
20
35
|
// Headless skip (#860). The daemon's headless workers spawn `claude --print`
|
|
21
36
|
// with CLAUDE_CODE_HEADLESS=true (see src/cli/services/headless-worker-
|
|
@@ -238,10 +253,24 @@ const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
|
238
253
|
// can persist `.moflo/upgrade-notice.json` for the statusline (#636).
|
|
239
254
|
let upgradeNoticeContext = null;
|
|
240
255
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
256
|
+
// Commit the version stamp = "this version's files are in place". Written the
|
|
257
|
+
// moment sync + manifest succeed (dogfood has no sync to do), NOT deferred to
|
|
258
|
+
// the end of §3 — so a launcher killed by the 5s SessionStart hook-timeout
|
|
259
|
+
// during later best-effort §3 work (hook-drift, CLAUDE.md injection drift,
|
|
260
|
+
// embeddings migration, …) still records the upgrade, and the next session
|
|
261
|
+
// stops re-detecting it. That end-of-§3 deferral was the root of the indefinite
|
|
262
|
+
// "updating…" re-detect loop. #730 is still honored: the stamp commits only
|
|
263
|
+
// after the sync that installs this version's files succeeds; every section
|
|
264
|
+
// after the §3 sync block runs unconditionally + idempotently each session, so
|
|
265
|
+
// an abort past this point strands no upgrade work.
|
|
266
|
+
function commitVersionStamp(stampPath, version) {
|
|
267
|
+
try {
|
|
268
|
+
mkdirSync(dirname(stampPath), { recursive: true });
|
|
269
|
+
writeFileSync(stampPath, version);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
emitWarning(`version stamp write failed (${errMessage(err)}) — next launcher will re-detect the upgrade`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
245
274
|
|
|
246
275
|
// 5-min TTL is a safety net for zombie launchers (statusline ignores past-TTL
|
|
247
276
|
// files). The 2-min "completed" TTL lets the user see the post-upgrade badge
|
|
@@ -309,6 +338,15 @@ if (existsSync(UPGRADE_NOTICE_PATH())) {
|
|
|
309
338
|
} catch { /* deleted between stat and unlink — fine */ }
|
|
310
339
|
}
|
|
311
340
|
|
|
341
|
+
// ── 0-pre.b Purge orphaned pre-rebrand reflect-*.json (auto-meditate rebrand) ─
|
|
342
|
+
// auto-reflect → auto-meditate renamed the ledger/state files. The old pair is
|
|
343
|
+
// never read again by the new code, so on a consumer's first session after
|
|
344
|
+
// upgrade we delete them here — self-healing, same posture as the upgrade-notice
|
|
345
|
+
// and hook-command migrations. Idempotent: a no-op once the files are gone.
|
|
346
|
+
try {
|
|
347
|
+
purgeLegacyMeditateFiles(projectRoot);
|
|
348
|
+
} catch { /* cleanup is best-effort; never block session start on it */ }
|
|
349
|
+
|
|
312
350
|
// #1173 Option D: defensive cleanup of in-progress upgrade notice if the
|
|
313
351
|
// launcher aborts before §3f writes 'completed'. Without this, a mid-flight
|
|
314
352
|
// abort leaves the (updating…) badge on the statusline until the 5-min TTL
|
|
@@ -915,7 +953,7 @@ try {
|
|
|
915
953
|
// memory; a concurrent sql.js flush would clobber the cherry-picked
|
|
916
954
|
// rows below, and old-path writes would resurrect ghost files in legacy
|
|
917
955
|
// dirs. Section 4's `hooks.mjs session-start` spawns a fresh daemon
|
|
918
|
-
// under the current code
|
|
956
|
+
// under the current code after the version stamp is committed (below).
|
|
919
957
|
const upgradeDaemonLock = resolve(projectRoot, '.moflo', 'daemon.lock');
|
|
920
958
|
if (stopDaemon(upgradeDaemonLock)) {
|
|
921
959
|
emitMutation('stopped daemon for upgrade', 'will restart fresh after upgrade work');
|
|
@@ -978,7 +1016,7 @@ try {
|
|
|
978
1016
|
// (the stopDaemon call earlier handled this) and 3a-pre will spawn a
|
|
979
1017
|
// fresh daemon under the new code.
|
|
980
1018
|
if (isMofloDogfood) {
|
|
981
|
-
|
|
1019
|
+
commitVersionStamp(versionStampPath, installedVersion);
|
|
982
1020
|
emitMutation('skipped file-sync', 'moflo dogfood — committed dogfood copies preserved');
|
|
983
1021
|
} else {
|
|
984
1022
|
|
|
@@ -1025,6 +1063,24 @@ try {
|
|
|
1025
1063
|
onSuccess: (key, dest) => recordManifestEntry(key, dest),
|
|
1026
1064
|
});
|
|
1027
1065
|
|
|
1066
|
+
// Single source of truth for what we sync into the consumer's .claude/ —
|
|
1067
|
+
// bin/lib/shipped-scripts.json (#1191). Read from the freshly-installed
|
|
1068
|
+
// package's binDir so an upgrade picks up newly-added scripts, not our
|
|
1069
|
+
// own (older) synced copy. Degrade to no-op on a broken/unreadable
|
|
1070
|
+
// manifest — the postinstall bootstrap already deployed the critical set,
|
|
1071
|
+
// and crashing session-start would be worse than skipping one sync.
|
|
1072
|
+
let scriptFiles = [];
|
|
1073
|
+
let binHelperFiles = [];
|
|
1074
|
+
let sourceHelperFiles = [];
|
|
1075
|
+
try {
|
|
1076
|
+
const shipped = loadShippedScripts(resolve(binDir, 'lib'));
|
|
1077
|
+
scriptFiles = shipped.scriptFiles;
|
|
1078
|
+
binHelperFiles = shipped.binHelperFiles;
|
|
1079
|
+
sourceHelperFiles = shipped.sourceHelperFiles;
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
emitWarning(`shipped-scripts manifest unreadable (${errMessage(err)}) — skipping script/helper sync this session`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1028
1084
|
// Version changed — sync scripts from bin/
|
|
1029
1085
|
if (autoUpdateConfig.scripts) {
|
|
1030
1086
|
const scriptsDir = resolve(projectRoot, '.claude/scripts');
|
|
@@ -1032,12 +1088,6 @@ try {
|
|
|
1032
1088
|
// not have it yet, in which case every copyFileSync below would
|
|
1033
1089
|
// silently ENOENT (#854).
|
|
1034
1090
|
if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
|
|
1035
|
-
const scriptFiles = [
|
|
1036
|
-
'hooks.mjs', 'session-start-launcher.mjs', 'index-guidance.mjs',
|
|
1037
|
-
'build-embeddings.mjs', 'generate-code-map.mjs', 'semantic-search.mjs',
|
|
1038
|
-
'index-tests.mjs', 'index-patterns.mjs', 'index-all.mjs',
|
|
1039
|
-
'setup-project.mjs', 'run-migrations.mjs',
|
|
1040
|
-
];
|
|
1041
1091
|
for (const file of scriptFiles) {
|
|
1042
1092
|
await syncFile(resolve(binDir, file), resolve(scriptsDir, file), `.claude/scripts/${file}`);
|
|
1043
1093
|
}
|
|
@@ -1087,10 +1137,8 @@ try {
|
|
|
1087
1137
|
const helpersDir = resolve(projectRoot, '.claude/helpers');
|
|
1088
1138
|
if (!existsSync(helpersDir)) mkdirSync(helpersDir, { recursive: true });
|
|
1089
1139
|
|
|
1090
|
-
// Gate and hook helpers — shipped as static files in bin
|
|
1091
|
-
|
|
1092
|
-
'gate.cjs', 'gate-hook.mjs', 'prompt-hook.mjs', 'hook-handler.cjs', 'simplify-classify.cjs',
|
|
1093
|
-
];
|
|
1140
|
+
// Gate and hook helpers — shipped as static files in bin/.
|
|
1141
|
+
// List comes from the canonical manifest loaded above (#1191).
|
|
1094
1142
|
for (const file of binHelperFiles) {
|
|
1095
1143
|
await syncFile(resolve(binDir, file), resolve(helpersDir, file), `.claude/helpers/${file}`);
|
|
1096
1144
|
}
|
|
@@ -1100,11 +1148,6 @@ try {
|
|
|
1100
1148
|
resolve(projectRoot, 'node_modules/moflo/.claude/helpers'),
|
|
1101
1149
|
resolve(projectRoot, 'node_modules/moflo/src/cli/.claude/helpers'),
|
|
1102
1150
|
];
|
|
1103
|
-
const sourceHelperFiles = [
|
|
1104
|
-
'auto-memory-hook.mjs', 'statusline.cjs', 'intelligence.cjs',
|
|
1105
|
-
'subagent-start.cjs', 'subagent-bootstrap.json',
|
|
1106
|
-
'pre-commit', 'post-commit',
|
|
1107
|
-
];
|
|
1108
1151
|
for (const file of sourceHelperFiles) {
|
|
1109
1152
|
const dest = resolve(helpersDir, file);
|
|
1110
1153
|
for (const srcDir of helperSources) {
|
|
@@ -1139,35 +1182,20 @@ try {
|
|
|
1139
1182
|
// because they don't exist in node_modules/moflo/, so they never enter
|
|
1140
1183
|
// the manifest and never get pruned — same proven safety story as
|
|
1141
1184
|
// scripts/helpers above.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
} catch (err) {
|
|
1148
|
-
emitWarning(`${destPrefix} readdir failed (${errMessage(err)})`);
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
for (const entry of entries) {
|
|
1152
|
-
if (!entry.isFile()) continue;
|
|
1153
|
-
if (!entry.name.toLowerCase().endsWith('.md')) continue;
|
|
1154
|
-
const parent = entry.parentPath || entry.path || srcDir;
|
|
1155
|
-
const absSrc = resolve(parent, entry.name);
|
|
1156
|
-
const rel = absSrc.slice(srcDir.length + 1).split(/[\\/]/).join('/');
|
|
1157
|
-
const absDest = resolve(projectRoot, destPrefix, rel);
|
|
1158
|
-
try { mkdirSync(dirname(absDest), { recursive: true }); } catch (err) {
|
|
1159
|
-
emitWarning(`${destPrefix} subdir mkdir failed for ${rel} (${errMessage(err)})`);
|
|
1160
|
-
}
|
|
1161
|
-
await syncFile(absSrc, absDest, `${destPrefix}/${rel}`);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1185
|
+
//
|
|
1186
|
+
// syncDirRecursive lives in bin/lib/file-sync.mjs so the exclusion logic
|
|
1187
|
+
// is unit-testable (tests/bin/file-sync-dir.test.ts). Skills pass
|
|
1188
|
+
// INTERNAL_SKILLS as excludeTopLevel so moflo-internal skills (`/publish`,
|
|
1189
|
+
// `/reset-epic`) ship in the tarball but never land in a consumer project.
|
|
1164
1190
|
await syncDirRecursive(
|
|
1165
1191
|
resolve(projectRoot, 'node_modules/moflo/.claude/agents'),
|
|
1166
1192
|
'.claude/agents',
|
|
1193
|
+
{ projectRoot, syncFile, onWarn: emitWarning },
|
|
1167
1194
|
);
|
|
1168
1195
|
await syncDirRecursive(
|
|
1169
1196
|
resolve(projectRoot, 'node_modules/moflo/.claude/skills'),
|
|
1170
1197
|
'.claude/skills',
|
|
1198
|
+
{ projectRoot, syncFile, excludeTopLevel: new Set(INTERNAL_SKILLS), onWarn: emitWarning },
|
|
1171
1199
|
);
|
|
1172
1200
|
|
|
1173
1201
|
// Sync all shipped guidance files from node_modules/moflo/.claude/guidance/shipped/
|
|
@@ -1269,7 +1297,7 @@ try {
|
|
|
1269
1297
|
// The daemon was already stopped above so the lock file is gone and
|
|
1270
1298
|
// there's no live PID to recycle here. Section 4's `hooks.mjs
|
|
1271
1299
|
// session-start` will spawn a fresh daemon under the current moflo
|
|
1272
|
-
// image
|
|
1300
|
+
// image after the version stamp is committed (below, on sync success).
|
|
1273
1301
|
|
|
1274
1302
|
// Surface per-file copy failures so the user / Claude can see what
|
|
1275
1303
|
// didn't sync (#854). The file isn't in the manifest either, so the
|
|
@@ -1283,8 +1311,10 @@ try {
|
|
|
1283
1311
|
);
|
|
1284
1312
|
}
|
|
1285
1313
|
|
|
1286
|
-
// Manifest reflects synced files immediately; version stamp is
|
|
1287
|
-
//
|
|
1314
|
+
// Manifest reflects synced files immediately; the version stamp is
|
|
1315
|
+
// committed right after the manifest write (below), gated on it succeeding
|
|
1316
|
+
// — so an abort BEFORE sync still re-runs upgrade detection (#730), while
|
|
1317
|
+
// an abort AFTER sync no longer strands the stamp in a re-detect loop.
|
|
1288
1318
|
//
|
|
1289
1319
|
// Exclude paths that `applyRetiredPrune` just deleted from disk —
|
|
1290
1320
|
// recording a non-existent file in `installed-files.json` triggers
|
|
@@ -1302,7 +1332,7 @@ try {
|
|
|
1302
1332
|
const cfDir = resolve(projectRoot, '.moflo');
|
|
1303
1333
|
if (!existsSync(cfDir)) mkdirSync(cfDir, { recursive: true });
|
|
1304
1334
|
writeFileSync(manifestPath, JSON.stringify(persistedManifest, null, 2));
|
|
1305
|
-
|
|
1335
|
+
commitVersionStamp(versionStampPath, installedVersion);
|
|
1306
1336
|
} catch (err) {
|
|
1307
1337
|
// #854: manifest write must surface — without it the next launcher
|
|
1308
1338
|
// can't tell what was installed and the version stamp never gets
|
|
@@ -2145,18 +2175,15 @@ if (upgradeNoticeContext) {
|
|
|
2145
2175
|
upgradeNoticeFinalized = true;
|
|
2146
2176
|
}
|
|
2147
2177
|
|
|
2148
|
-
// ── 3g.
|
|
2149
|
-
//
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
//
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
emitWarning(`version stamp write failed (${errMessage(err)}) — next launcher will re-detect the upgrade`);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2178
|
+
// ── 3g. (removed) Version stamp now commits eagerly on sync success ──────────
|
|
2179
|
+
// The stamp used to be deferred to here ("written LAST", #730). That made it
|
|
2180
|
+
// vulnerable to the 5s SessionStart hook-timeout kill: a launcher killed during
|
|
2181
|
+
// the best-effort §3 stages above never reached this point, so the stamp stayed
|
|
2182
|
+
// stale and every subsequent session re-detected the same upgrade — the
|
|
2183
|
+
// indefinite "updating…" loop. The stamp now commits inside §3's sync block the
|
|
2184
|
+
// moment this version's files are in place (see commitVersionStamp). #730 is
|
|
2185
|
+
// preserved because that commit is gated on sync success; every stage after the
|
|
2186
|
+
// sync block runs unconditionally + idempotently each session.
|
|
2160
2187
|
|
|
2161
2188
|
// ── 3h. Clear bootstrap sentinel if section-3 sync resolved it (#975) ───────
|
|
2162
2189
|
// Section 3 above re-attempts the same file copies the bootstrap was supposed
|
|
@@ -2193,6 +2220,59 @@ if (bootstrapSentinelData?.failures?.length > 0) {
|
|
|
2193
2220
|
}
|
|
2194
2221
|
}
|
|
2195
2222
|
|
|
2223
|
+
// Passive session-continuity injection (#1185). Relevance-gated: read recent
|
|
2224
|
+
// digests, score them against the current branch/changed-files/recency, and
|
|
2225
|
+
// emit ONLY the single best one if it clears the threshold. A fresh, unrelated
|
|
2226
|
+
// session injects nothing — that's what keeps it from going context-negative.
|
|
2227
|
+
// Framed as a verifiable lead, never ground truth. Wrapped so it can never
|
|
2228
|
+
// block or break session start. NOT routed through emitMutation: it's context,
|
|
2229
|
+
// not a mutation, so it must not inflate the count or trigger the spawn notice.
|
|
2230
|
+
function maybeInjectContinuity() {
|
|
2231
|
+
const cfg = readContinuityConfig(projectRoot);
|
|
2232
|
+
if (!cfg.inject) return;
|
|
2233
|
+
|
|
2234
|
+
const rows = readDigests(projectRoot, { limit: 12 });
|
|
2235
|
+
if (rows.length === 0) return; // first-ever session — nothing to inject, no git calls
|
|
2236
|
+
|
|
2237
|
+
const git = readGitState(projectRoot);
|
|
2238
|
+
const best = selectBestDigest(
|
|
2239
|
+
rows,
|
|
2240
|
+
{ branch: git.branch, changedFiles: git.changedFiles },
|
|
2241
|
+
{ maxAgeHours: cfg.maxAgeHours, now: Date.now() },
|
|
2242
|
+
);
|
|
2243
|
+
if (!best) return;
|
|
2244
|
+
|
|
2245
|
+
const block = formatInjection(best, Date.now());
|
|
2246
|
+
if (block) {
|
|
2247
|
+
try { process.stdout.write(block + '\n'); } catch { /* broken stdout must not throw */ }
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
try {
|
|
2251
|
+
maybeInjectContinuity();
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
emitWarning(`continuity injection skipped (${errMessage(err)})`);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Auto-meditate Stage 2 — DISTILL (#1198). Default-off. When enabled AND the
|
|
2257
|
+
// capture ledger holds un-distilled lessons, fire-and-forget the DETACHED
|
|
2258
|
+
// distill orchestrator — it runs ONE bounded headless Haiku /meditate over the
|
|
2259
|
+
// ledger one-liners and writes `learnings` via memory_store (daemon-routed,
|
|
2260
|
+
// writer-safe). The gate here is cheap (a config read + a ledger read); the
|
|
2261
|
+
// spawn + model call live entirely in the detached child, so the launcher's
|
|
2262
|
+
// spawn-and-exit contract holds. CLAUDE_CODE_HEADLESS is guarded at the top of
|
|
2263
|
+
// this file, so a headless session never reaches here (no infinite spawn).
|
|
2264
|
+
function maybeFireMeditateDistill() {
|
|
2265
|
+
if (!readMeditateConfig(projectRoot).enabled) return;
|
|
2266
|
+
if (pendingMeditateEntries(readMeditateLedger(projectRoot)).length === 0) return;
|
|
2267
|
+
const distillScript = resolveMofloBin(projectRoot, null, 'meditate-distill.mjs');
|
|
2268
|
+
if (distillScript) fireAndForget('node', [distillScript], 'meditate-distill');
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
maybeFireMeditateDistill();
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
emitWarning(`meditate distill spawn skipped (${errMessage(err)})`);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2196
2276
|
// Bypasses emitMutation — framing, not a mutation, so it must not inflate the count.
|
|
2197
2277
|
if (mutationCount > 0) {
|
|
2198
2278
|
try {
|
|
@@ -40,6 +40,14 @@ const DEFAULT_CONFIG = {
|
|
|
40
40
|
guidance: true,
|
|
41
41
|
code_map: true,
|
|
42
42
|
},
|
|
43
|
+
session_continuity: {
|
|
44
|
+
capture: true,
|
|
45
|
+
inject: true,
|
|
46
|
+
max_age_hours: 72,
|
|
47
|
+
},
|
|
48
|
+
auto_meditate: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
},
|
|
43
51
|
memory: {
|
|
44
52
|
backend: 'node-sqlite',
|
|
45
53
|
embedding_model: 'Xenova/all-MiniLM-L6-v2',
|
|
@@ -198,6 +206,16 @@ function mergeConfig(raw, root) {
|
|
|
198
206
|
guidance: raw.auto_index?.guidance ?? raw.autoIndex?.guidance ?? DEFAULT_CONFIG.auto_index.guidance,
|
|
199
207
|
code_map: raw.auto_index?.code_map ?? raw.autoIndex?.code_map ?? DEFAULT_CONFIG.auto_index.code_map,
|
|
200
208
|
},
|
|
209
|
+
session_continuity: {
|
|
210
|
+
capture: raw.session_continuity?.capture ?? raw.sessionContinuity?.capture ?? DEFAULT_CONFIG.session_continuity.capture,
|
|
211
|
+
inject: raw.session_continuity?.inject ?? raw.sessionContinuity?.inject ?? DEFAULT_CONFIG.session_continuity.inject,
|
|
212
|
+
max_age_hours: raw.session_continuity?.max_age_hours ?? raw.sessionContinuity?.maxAgeHours ?? DEFAULT_CONFIG.session_continuity.max_age_hours,
|
|
213
|
+
},
|
|
214
|
+
auto_meditate: {
|
|
215
|
+
// Back-compat: honour the legacy auto_reflect / autoReflect keys (pre-rebrand)
|
|
216
|
+
// so an existing opt-out survives until the yaml-upgrader renames the key.
|
|
217
|
+
enabled: raw.auto_meditate?.enabled ?? raw.autoMeditate?.enabled ?? raw.auto_reflect?.enabled ?? raw.autoReflect?.enabled ?? DEFAULT_CONFIG.auto_meditate.enabled,
|
|
218
|
+
},
|
|
201
219
|
memory: {
|
|
202
220
|
backend: coerceMemoryBackend(raw.memory?.backend),
|
|
203
221
|
embedding_model: raw.memory?.embedding_model || raw.memory?.embeddingModel || DEFAULT_CONFIG.memory.embedding_model,
|
|
@@ -16,6 +16,7 @@ import { generateSettingsJson, generateSettings } from './settings-generator.js'
|
|
|
16
16
|
import { generateMCPJson } from './mcp-generator.js';
|
|
17
17
|
import { generatePreCommitHook, generatePostCommitHook, generateAutoMemoryHook, generateGateScript, generateGateHookScript, generatePromptHookScript, generateHookHandlerScript, } from './helpers-generator.js';
|
|
18
18
|
import { generateClaudeMd } from './claudemd-generator.js';
|
|
19
|
+
import { loadShippedScripts } from './shipped-scripts.js';
|
|
19
20
|
import { writeEnvrc } from './envrc-generator.js';
|
|
20
21
|
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
21
22
|
import { locateMofloRootPath } from '../services/moflo-require.js';
|
|
@@ -31,12 +32,15 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
31
32
|
// skills that ship in the tarball but are deliberately NOT installed.
|
|
32
33
|
export const SKILLS_MAP = {
|
|
33
34
|
core: [
|
|
35
|
+
'commune',
|
|
34
36
|
'eldar',
|
|
35
37
|
'guidance',
|
|
36
38
|
'healer',
|
|
37
39
|
'flo-simplify',
|
|
38
40
|
'luminarium',
|
|
39
41
|
'reasoningbank-intelligence',
|
|
42
|
+
'meditate',
|
|
43
|
+
'divine',
|
|
40
44
|
],
|
|
41
45
|
memory: [
|
|
42
46
|
'memory-patterns',
|
|
@@ -401,25 +405,15 @@ export async function executeUpgrade(targetDir, _upgradeSettings = false) {
|
|
|
401
405
|
if (!fs.existsSync(scriptsDir)) {
|
|
402
406
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
403
407
|
}
|
|
404
|
-
// Must mirror the list in bin/session-start-launcher.mjs — divergence
|
|
405
|
-
// here means the launcher's drift-repair will delete files this upgrade
|
|
406
|
-
// didn't track, even though they ship in the package (#777).
|
|
407
|
-
const UPGRADE_SCRIPT_MAP = [
|
|
408
|
-
'hooks.mjs',
|
|
409
|
-
'session-start-launcher.mjs',
|
|
410
|
-
'index-guidance.mjs',
|
|
411
|
-
'build-embeddings.mjs',
|
|
412
|
-
'generate-code-map.mjs',
|
|
413
|
-
'semantic-search.mjs',
|
|
414
|
-
'index-tests.mjs',
|
|
415
|
-
'index-patterns.mjs',
|
|
416
|
-
'index-all.mjs',
|
|
417
|
-
'setup-project.mjs',
|
|
418
|
-
'run-migrations.mjs',
|
|
419
|
-
];
|
|
420
408
|
const binDir = findMofloBinDir();
|
|
421
409
|
if (binDir) {
|
|
422
|
-
|
|
410
|
+
// Canonical sync list — single source of truth in bin/lib/shipped-scripts.json
|
|
411
|
+
// (#1191). Read from the SAME binDir we copy from so the list and the files
|
|
412
|
+
// can't diverge (in dogfood, findMofloPackageRoot and findMofloBinDir can
|
|
413
|
+
// resolve to different installs). Drift across launcher / bootstrap / init
|
|
414
|
+
// is now impossible — there's one list.
|
|
415
|
+
const scriptFiles = loadShippedScripts(path.join(binDir, 'lib')).scriptFiles;
|
|
416
|
+
for (const name of scriptFiles) {
|
|
423
417
|
const srcPath = path.join(binDir, name);
|
|
424
418
|
const destPath = path.join(scriptsDir, name);
|
|
425
419
|
if (!fs.existsSync(srcPath))
|
|
@@ -17,6 +17,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
17
17
|
import { discoverGuidanceDirs, discoverSrcDirs, discoverTestDirs, detectExtensions, renderMofloYaml, } from './moflo-yaml-template.js';
|
|
18
18
|
import { generateClaudeMd as generateMofloSection } from './claudemd-generator.js';
|
|
19
19
|
import { applyInjectionReplacement } from '../services/claudemd-injection.js';
|
|
20
|
+
import { loadShippedScripts } from './shipped-scripts.js';
|
|
20
21
|
import { DEFAULT_INIT_OPTIONS } from './types.js';
|
|
21
22
|
export { discoverTestDirs };
|
|
22
23
|
// ============================================================================
|
|
@@ -425,23 +426,10 @@ function generateClaudeMd(root, _force) {
|
|
|
425
426
|
// These scripts are used by session-start hooks for indexing, code map, etc.
|
|
426
427
|
// Always overwrite to keep them in sync with the installed moflo version.
|
|
427
428
|
// ============================================================================
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
export const SCRIPT_MAP = [
|
|
433
|
-
'hooks.mjs',
|
|
434
|
-
'session-start-launcher.mjs',
|
|
435
|
-
'index-guidance.mjs',
|
|
436
|
-
'build-embeddings.mjs',
|
|
437
|
-
'generate-code-map.mjs',
|
|
438
|
-
'semantic-search.mjs',
|
|
439
|
-
'index-tests.mjs',
|
|
440
|
-
'index-patterns.mjs',
|
|
441
|
-
'index-all.mjs',
|
|
442
|
-
'setup-project.mjs',
|
|
443
|
-
'run-migrations.mjs',
|
|
444
|
-
];
|
|
429
|
+
// The script sync list is read from the canonical manifest
|
|
430
|
+
// bin/lib/shipped-scripts.json (#1191) — single source of truth shared with the
|
|
431
|
+
// launcher, the post-install bootstrap, and executor.ts. No more hand-mirrored
|
|
432
|
+
// arrays that drift (which #1184 hit across all four sites).
|
|
445
433
|
function syncScripts(root, force) {
|
|
446
434
|
const scriptsDir = path.join(root, '.claude', 'scripts');
|
|
447
435
|
if (!fs.existsSync(scriptsDir)) {
|
|
@@ -462,8 +450,15 @@ function syncScripts(root, force) {
|
|
|
462
450
|
if (!binDir) {
|
|
463
451
|
return { name: '.claude/scripts/', status: 'skipped', detail: 'moflo bin/ not found' };
|
|
464
452
|
}
|
|
453
|
+
let scriptFiles;
|
|
454
|
+
try {
|
|
455
|
+
scriptFiles = loadShippedScripts(path.join(binDir, 'lib')).scriptFiles;
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
return { name: '.claude/scripts/', status: 'skipped', detail: `shipped-scripts manifest unreadable: ${errorDetail(err)}` };
|
|
459
|
+
}
|
|
465
460
|
let copied = 0;
|
|
466
|
-
for (const name of
|
|
461
|
+
for (const name of scriptFiles) {
|
|
467
462
|
const srcPath = path.join(binDir, name);
|
|
468
463
|
const destPath = path.join(scriptsDir, name);
|
|
469
464
|
if (!fs.existsSync(srcPath))
|
|
@@ -532,6 +527,13 @@ function isStale(srcPath, destPath) {
|
|
|
532
527
|
// ============================================================================
|
|
533
528
|
export function updateGitignore(root) {
|
|
534
529
|
const gitignorePath = path.join(root, '.gitignore');
|
|
530
|
+
// Script ignore patterns from the canonical manifest (#1191); a broken/absent
|
|
531
|
+
// manifest just omits them — gitignore is non-critical (scripts are derived).
|
|
532
|
+
let scriptIgnorePatterns = [];
|
|
533
|
+
try {
|
|
534
|
+
scriptIgnorePatterns = loadShippedScripts().scriptFiles.map(name => `/.claude/scripts/${name}`);
|
|
535
|
+
}
|
|
536
|
+
catch { /* manifest unreadable — omit (non-critical) */ }
|
|
535
537
|
const entries = [
|
|
536
538
|
'.claude-epic/',
|
|
537
539
|
'.moflo/',
|
|
@@ -543,7 +545,7 @@ export function updateGitignore(root) {
|
|
|
543
545
|
// swallowed shipped/internal subdirs and broke `npm pack`
|
|
544
546
|
// (guidance-gitignore-shipped-trap).
|
|
545
547
|
'/.claude/guidance/moflo-*.md',
|
|
546
|
-
...
|
|
548
|
+
...scriptIgnorePatterns,
|
|
547
549
|
];
|
|
548
550
|
// Treat `/.foo` and `.foo` as the same rule when checking for prior presence
|
|
549
551
|
// — both forms anchor at gitignore root, so a consumer who wrote either
|
|
@@ -213,6 +213,25 @@ auto_index:
|
|
|
213
213
|
code_map: ${codeMap}
|
|
214
214
|
tests: ${tests}
|
|
215
215
|
|
|
216
|
+
# Passive session-continuity — pick up where you left off across sessions.
|
|
217
|
+
# capture: silently record a compact "where you left off" digest at turn-end.
|
|
218
|
+
# inject: surface the single most-relevant recent digest at session-start
|
|
219
|
+
# (relevance-gated by branch / changed files / recency, so an unrelated
|
|
220
|
+
# session shows nothing). Add "<private>" to a message to skip capturing
|
|
221
|
+
# that session. Set either to false to opt out.
|
|
222
|
+
session_continuity:
|
|
223
|
+
capture: true
|
|
224
|
+
inject: true
|
|
225
|
+
max_age_hours: 72 # ignore digests older than this when injecting
|
|
226
|
+
|
|
227
|
+
# Auto-meditate (#1198) — the automatic counterpart to /meditate. When enabled,
|
|
228
|
+
# moflo recognizes durable lessons in the LIVE session (a tiny answer-first note
|
|
229
|
+
# on course-corrections / errors / decisions) and distills them into long-term
|
|
230
|
+
# memory at the next session-start via a cheap headless Haiku pass — deduped.
|
|
231
|
+
# Ships ON; set false to opt out.
|
|
232
|
+
auto_meditate:
|
|
233
|
+
enabled: true
|
|
234
|
+
|
|
216
235
|
# Memory backend
|
|
217
236
|
memory:
|
|
218
237
|
backend: node-sqlite
|
|
@@ -312,6 +331,8 @@ export const REQUIRED_TOP_LEVEL_SECTIONS = [
|
|
|
312
331
|
'tests',
|
|
313
332
|
'gates',
|
|
314
333
|
'auto_index',
|
|
334
|
+
'session_continuity',
|
|
335
|
+
'auto_meditate',
|
|
315
336
|
'memory',
|
|
316
337
|
'hooks',
|
|
317
338
|
'mcp',
|
|
@@ -178,6 +178,17 @@ function hookHandlerCmd(subcommand) {
|
|
|
178
178
|
function autoMemoryCmd(subcommand) {
|
|
179
179
|
return hookCmdEsm('"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs"', subcommand);
|
|
180
180
|
}
|
|
181
|
+
/** Shorthand for the ESM session-continuity capture command (#1185). Lives in
|
|
182
|
+
* .claude/scripts/ (a synced scriptFile), beside its ./lib/ helpers. */
|
|
183
|
+
function continuityCmd(subcommand) {
|
|
184
|
+
return hookCmdEsm('"$CLAUDE_PROJECT_DIR/.claude/scripts/session-continuity.mjs"', subcommand);
|
|
185
|
+
}
|
|
186
|
+
/** Shorthand for the ESM auto-meditate capture command (#1198). Default-ON
|
|
187
|
+
* (the script no-ops only when auto_meditate.enabled is false). Lives in
|
|
188
|
+
* .claude/scripts/ beside its ./lib/ helpers. */
|
|
189
|
+
function meditateCaptureCmd(subcommand) {
|
|
190
|
+
return hookCmdEsm('"$CLAUDE_PROJECT_DIR/.claude/scripts/meditate-capture.mjs"', subcommand);
|
|
191
|
+
}
|
|
181
192
|
/** Shorthand for gate commands (lightweight JSON state checks) */
|
|
182
193
|
function gateCmd(subcommand) {
|
|
183
194
|
return hookCmd('"$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs"', subcommand);
|
|
@@ -345,6 +356,13 @@ function generateHooksConfig(config) {
|
|
|
345
356
|
{ type: 'command', command: gateHookCmd('prompt-state-reset'), timeout: 3000 },
|
|
346
357
|
],
|
|
347
358
|
},
|
|
359
|
+
// #1198 — auto-meditate Stage 1 (detect). Default-ON; injects an
|
|
360
|
+
// answer-first capture directive only on a strong signal + within limits.
|
|
361
|
+
{
|
|
362
|
+
hooks: [
|
|
363
|
+
{ type: 'command', command: meditateCaptureCmd('meditate-detect'), timeout: 3000 },
|
|
364
|
+
],
|
|
365
|
+
},
|
|
348
366
|
];
|
|
349
367
|
}
|
|
350
368
|
// SubagentStart — inject directive for subagents to read guidance protocol
|
|
@@ -378,13 +396,17 @@ function generateHooksConfig(config) {
|
|
|
378
396
|
},
|
|
379
397
|
];
|
|
380
398
|
}
|
|
381
|
-
// Stop — persist session + sync auto memory
|
|
399
|
+
// Stop — persist session + sync auto memory + capture continuity digest (#1185)
|
|
382
400
|
if (config.stop) {
|
|
383
401
|
hooks.Stop = [
|
|
384
402
|
{
|
|
385
403
|
hooks: [
|
|
386
404
|
{ type: 'command', command: hookHandlerCmd('session-end'), timeout: 5000 },
|
|
387
405
|
{ type: 'command', command: autoMemoryCmd('sync'), timeout: 10000 },
|
|
406
|
+
{ type: 'command', command: continuityCmd('capture'), timeout: 5000 },
|
|
407
|
+
// #1198 — auto-meditate Stage 1 (scrape). Default-ON; harvests
|
|
408
|
+
// <meditate-capture> tags from the assistant turn into the ledger.
|
|
409
|
+
{ type: 'command', command: meditateCaptureCmd('meditate-scrape'), timeout: 5000 },
|
|
388
410
|
],
|
|
389
411
|
},
|
|
390
412
|
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS reader for the canonical shipped-scripts manifest
|
|
3
|
+
* (`bin/lib/shipped-scripts.json`) — the single source of truth for the scripts
|
|
4
|
+
* + helpers moflo syncs into a consumer's `.claude/` (issue #1191).
|
|
5
|
+
*
|
|
6
|
+
* The `.mjs` twin is `bin/lib/shipped-scripts.mjs`; both read the same JSON.
|
|
7
|
+
* This module resolves it through `findMofloPackageRoot()` — the sanctioned
|
|
8
|
+
* dist→bin resolver (see `.claude/guidance/internal/dogfooding.md` §2) that
|
|
9
|
+
* works in dogfood TS, compiled dist, and consumer installs alike. NEVER a
|
|
10
|
+
* hardcoded `../../../../bin/...` path (the depth differs between src and dist).
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { findMofloPackageRoot } from '../services/moflo-require.js';
|
|
15
|
+
/**
|
|
16
|
+
* Read the canonical shipped-scripts manifest. Pass the resolved `bin/lib`
|
|
17
|
+
* directory to read a specific install's copy (callers that already resolved a
|
|
18
|
+
* binDir — e.g. init's syncScripts — pass it so the list matches the exact dir
|
|
19
|
+
* they copy from); omit it to resolve via `findMofloPackageRoot()`. Throws if
|
|
20
|
+
* the manifest can't be located — that signals a broken install, which init and
|
|
21
|
+
* upgrade can't proceed past anyway.
|
|
22
|
+
*/
|
|
23
|
+
export function loadShippedScripts(binLibDir) {
|
|
24
|
+
let dir = binLibDir;
|
|
25
|
+
if (!dir) {
|
|
26
|
+
const root = findMofloPackageRoot();
|
|
27
|
+
if (!root) {
|
|
28
|
+
throw new Error('moflo package root not found — cannot read shipped-scripts manifest');
|
|
29
|
+
}
|
|
30
|
+
dir = join(root, 'bin', 'lib');
|
|
31
|
+
}
|
|
32
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'shipped-scripts.json'), 'utf-8'));
|
|
33
|
+
return {
|
|
34
|
+
scriptFiles: manifest.scriptFiles ?? [],
|
|
35
|
+
binHelperFiles: manifest.binHelperFiles ?? [],
|
|
36
|
+
sourceHelperFiles: manifest.sourceHelperFiles ?? [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=shipped-scripts.js.map
|
|
@@ -32,6 +32,26 @@ export function _resetProjectRootForTest() {
|
|
|
32
32
|
export function _getBridgeCoherenceCursorForTest() {
|
|
33
33
|
return lastSeenMtimeMs;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Candidate-pool ceiling for the brute-force search path (#1201).
|
|
37
|
+
*
|
|
38
|
+
* The bridge search scores candidates one-by-one (cosine + BM25), so it must
|
|
39
|
+
* cap how many rows it pulls. The old `LIMIT 1000` with NO `ORDER BY` truncated
|
|
40
|
+
* by rowid (insertion order): on a populated DB the first 1000 rows are all
|
|
41
|
+
* bulk-indexed `code-map`, so a no-namespace search silently scored ZERO
|
|
42
|
+
* `learnings`/`patterns`/etc. — they were invisible to default recall.
|
|
43
|
+
*
|
|
44
|
+
* The fix pairs this cap with `ORDER BY created_at DESC`, so when truncation
|
|
45
|
+
* does happen (DB larger than the cap) it keeps the most RECENT entries — where
|
|
46
|
+
* curated learnings and recent work live — instead of the oldest rowids.
|
|
47
|
+
* Realistic DBs (thousands–low tens of thousands) fall under the cap and are
|
|
48
|
+
* scored in full; measured ~13ms per 1000 rows. Env-overridable for ops tuning
|
|
49
|
+
* and tests. Beyond the cap, a true HNSW candidate path is the scale answer.
|
|
50
|
+
*/
|
|
51
|
+
export function searchCandidateCap() {
|
|
52
|
+
const raw = Number(process.env.MOFLO_SEARCH_CANDIDATE_CAP);
|
|
53
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 25000;
|
|
54
|
+
}
|
|
35
55
|
/**
|
|
36
56
|
* Resolve the bridge's project root.
|
|
37
57
|
*
|