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.
Files changed (46) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +1 -0
  2. package/.claude/guidance/shipped/moflo-core-guidance.md +1 -0
  3. package/.claude/guidance/shipped/moflo-skills-reference.md +108 -0
  4. package/.claude/guidance/shipped/moflo-yaml-reference.md +13 -0
  5. package/.claude/skills/commune/SKILL.md +140 -0
  6. package/.claude/skills/divine/SKILL.md +130 -0
  7. package/.claude/skills/meditate/SKILL.md +122 -0
  8. package/README.md +39 -3
  9. package/bin/index-all.mjs +2 -1
  10. package/bin/index-reference.mjs +221 -0
  11. package/bin/lib/file-sync.mjs +50 -1
  12. package/bin/lib/hook-io.mjs +63 -0
  13. package/bin/lib/index-fingerprint.mjs +0 -0
  14. package/bin/lib/internal-skills.mjs +16 -0
  15. package/bin/lib/meditate.mjs +497 -0
  16. package/bin/lib/pii-scrub.mjs +119 -0
  17. package/bin/lib/reference-docs.mjs +218 -0
  18. package/bin/lib/session-continuity.mjs +372 -0
  19. package/bin/lib/shipped-scripts.json +36 -0
  20. package/bin/lib/shipped-scripts.mjs +33 -0
  21. package/bin/lib/yaml-upgrader.mjs +62 -0
  22. package/bin/meditate-capture.mjs +123 -0
  23. package/bin/meditate-distill.mjs +121 -0
  24. package/bin/session-continuity.mjs +206 -0
  25. package/bin/session-start-launcher.mjs +140 -60
  26. package/dist/src/cli/config/moflo-config.js +18 -0
  27. package/dist/src/cli/init/executor.js +11 -17
  28. package/dist/src/cli/init/moflo-init.js +21 -19
  29. package/dist/src/cli/init/moflo-yaml-template.js +21 -0
  30. package/dist/src/cli/init/settings-generator.js +23 -1
  31. package/dist/src/cli/init/shipped-scripts.js +39 -0
  32. package/dist/src/cli/memory/bridge-core.js +20 -0
  33. package/dist/src/cli/memory/bridge-entries.js +8 -2
  34. package/dist/src/cli/memory/memory-bridge.js +6 -2
  35. package/dist/src/cli/memory/memory-initializer.js +6 -2
  36. package/dist/src/cli/services/hook-block-hash.js +9 -1
  37. package/dist/src/cli/services/hook-wiring.js +38 -0
  38. package/dist/src/cli/transfer/anonymization/index.js +146 -40
  39. package/dist/src/cli/transfer/deploy-seraphine.js +1 -1
  40. package/dist/src/cli/transfer/export.js +2 -2
  41. package/dist/src/cli/transfer/store/publish.js +1 -1
  42. package/dist/src/cli/version.js +1 -1
  43. package/package.json +2 -2
  44. package/scripts/post-install-bootstrap.mjs +22 -42
  45. package/dist/src/cli/hooks/llm/index.js +0 -11
  46. 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
- // Deferred so we commit it AFTER every upgrade-work block (see 3g). The stamp
242
- // is the "launcher fully completed" signal writing it mid-flight lets an
243
- // aborted launcher strand consumers on a half-applied upgrade (#730).
244
- let pendingVersionStampWrite = null;
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 once 3g writes the version stamp.
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
- pendingVersionStampWrite = { path: versionStampPath, version: installedVersion };
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
- const binHelperFiles = [
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
- async function syncDirRecursive(srcDir, destPrefix) {
1143
- if (!existsSync(srcDir)) return;
1144
- let entries;
1145
- try {
1146
- entries = readdirSync(srcDir, { recursive: true, withFileTypes: true });
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 once 3g writes the version stamp.
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 deferred
1287
- // to 3g so an aborted launcher re-runs upgrade detection (#730).
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
- pendingVersionStampWrite = { path: versionStampPath, version: installedVersion };
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. Commit deferred version stamp (#730) ────────────────────────────────
2149
- // Written LAST so an abort above leaves the stamp unchanged and the next
2150
- // launcher re-detects the upgrade. Failure here is surfaced (#854) so a
2151
- // permanently-broken stamp write (filesystem permissions, AV holds) doesn't
2152
- // silently strand consumers in re-detect-on-every-session loops.
2153
- if (pendingVersionStampWrite) {
2154
- try {
2155
- writeFileSync(pendingVersionStampWrite.path, pendingVersionStampWrite.version);
2156
- } catch (err) {
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
- for (const name of UPGRADE_SCRIPT_MAP) {
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
- // Must mirror UPGRADE_SCRIPT_MAP in src/cli/init/executor.ts and the
429
- // scriptFiles array in bin/session-start-launcher.mjsfirst-init drops any
430
- // script missing here, and the launcher's manifest cleanup later treats it as
431
- // orphan residue and deletes it (#777, feedback_scriptfiles_sync.md).
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 SCRIPT_MAP) {
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
- ...SCRIPT_MAP.map(name => `/.claude/scripts/${name}`),
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
  *