ruvnet-brain 4.3.8 → 4.3.10

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 (85) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +11 -2
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +472 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  36. package/scripts/build-bundle.mjs +8 -0
  37. package/scripts/build-primer.mjs +2 -3
  38. package/scripts/candidate-host-evidence.mjs +59 -41
  39. package/scripts/ci/build-fixture-kb.mjs +59 -2
  40. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  41. package/scripts/ci/stranger-scenario.mjs +13 -28
  42. package/scripts/claims-verify.mjs +219 -21
  43. package/scripts/console-engine.mjs +4 -18
  44. package/scripts/console-runtime-identity.mjs +6 -0
  45. package/scripts/development-maintenance.mjs +47 -0
  46. package/scripts/development-push-check.mjs +35 -0
  47. package/scripts/distill-project.mjs +24 -47
  48. package/scripts/doc-currency.mjs +91 -19
  49. package/scripts/git-hooks/pre-push +4 -162
  50. package/scripts/health-repair.mjs +22 -4
  51. package/scripts/hook-retirement-check.mjs +22 -0
  52. package/scripts/host-install-matrix.mjs +125 -61
  53. package/scripts/integration-evidence.mjs +11 -6
  54. package/scripts/learning-replay-fixture.mjs +23 -2
  55. package/scripts/nightly-two-run-proof.mjs +415 -0
  56. package/scripts/nightly-watchdog.mjs +17 -2
  57. package/scripts/npm-invocation.mjs +20 -0
  58. package/scripts/prepublication-evidence.mjs +42 -6
  59. package/scripts/primer-grounding.mjs +20 -0
  60. package/scripts/product-integrity-contract.mjs +22 -2
  61. package/scripts/public-verification-abandon.mjs +101 -0
  62. package/scripts/public-verification-aggregate.mjs +53 -19
  63. package/scripts/public-verification-finalizer.mjs +8 -2
  64. package/scripts/public-verification-lane.mjs +108 -8
  65. package/scripts/publication-receipt.mjs +173 -66
  66. package/scripts/published-surface-probe.mjs +10 -2
  67. package/scripts/qa-contract.mjs +57 -0
  68. package/scripts/qa-lanes.mjs +40 -0
  69. package/scripts/qa-runner.mjs +54 -64
  70. package/scripts/qe/ux-suite.mjs +10 -34
  71. package/scripts/qualified-candidate-check.mjs +124 -0
  72. package/scripts/release-abort-stale.mjs +2 -2
  73. package/scripts/release-projection.mjs +20 -1
  74. package/scripts/release-publication-handoff.mjs +101 -0
  75. package/scripts/release-qualification-contract.mjs +87 -0
  76. package/scripts/release-qualification.mjs +135 -0
  77. package/scripts/release-transaction.mjs +81 -1
  78. package/scripts/release.mjs +10 -1
  79. package/scripts/remedy-registry.mjs +6 -13
  80. package/scripts/retrieval-canary.mjs +91 -18
  81. package/scripts/selfcheck.mjs +12 -4
  82. package/scripts/snapshot-freshness.mjs +58 -0
  83. package/scripts/stack-sync.mjs +41 -43
  84. package/scripts/staged-host-verifier.mjs +63 -14
  85. package/scripts/wired-check.mjs +31 -2
@@ -0,0 +1,472 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ export const NIGHTLY_LABEL = 'com.ruvnet.brain-update';
8
+ export const NIGHTLY_HOUR = 3;
9
+ export const NIGHTLY_MINUTE = 47;
10
+ export const NIGHTLY_PROOF_LABEL = /^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/;
11
+ export const NIGHTLY_ENV_ALLOWLIST = Object.freeze([
12
+ 'PATH', 'HOME', 'USERPROFILE', 'RUVNET_BRAIN_HOME', 'RUVNET_BRAIN_KB', 'npm_config_cache', 'NO_COLOR', 'SystemRoot', 'SYSTEMROOT', 'ComSpec', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP',
13
+ ]);
14
+
15
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
16
+ const xmlEscape = (value) => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
17
+ const shellQuote = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
18
+ const homeOf = (env) => env.HOME || env.USERPROFILE || os.homedir();
19
+ const SAFE_PHASE = /^[A-Za-z0-9._-]+$/;
20
+
21
+ function validateIdentity(identity) {
22
+ if (identity !== NIGHTLY_LABEL && !NIGHTLY_PROOF_LABEL.test(identity)) {
23
+ throw new Error(`unsafe nightly scheduler identity: ${identity || '(unset)'}`);
24
+ }
25
+ return identity;
26
+ }
27
+
28
+ function registrationName(identity) {
29
+ return identity === NIGHTLY_LABEL ? 'registration.json' : `registration-${identity}.json`;
30
+ }
31
+
32
+ function normalizePackageTarget(target = { spec: 'ruvnet-brain@latest', sha256: null }) {
33
+ const spec = String(target?.spec || '');
34
+ const digest = target?.sha256 ?? null;
35
+ if (spec === 'ruvnet-brain@latest' && digest === null) return { spec, sha256: null };
36
+ if (!path.isAbsolute(spec) || !spec.endsWith('.tgz') || !/^[a-f0-9]{64}$/.test(String(digest))) {
37
+ throw new Error('nightly proof package target must be an absolute tarball with a SHA-256 digest');
38
+ }
39
+ const actual = sha256(fs.readFileSync(spec));
40
+ if (actual !== digest) throw new Error('nightly proof package target digest mismatch');
41
+ return { spec: path.resolve(spec), sha256: digest };
42
+ }
43
+
44
+ function normalizeBundleTarget(target, identity) {
45
+ if (identity === NIGHTLY_LABEL) {
46
+ if (target !== null && target !== undefined) throw new Error('production nightly registration cannot pin a local bundle');
47
+ return null;
48
+ }
49
+ const spec = String(target?.spec || '');
50
+ const digest = String(target?.sha256 || '');
51
+ let stat;
52
+ try { stat = fs.lstatSync(spec); } catch { /* handled below */ }
53
+ if (!path.isAbsolute(spec) || !spec.endsWith('.zip') || !stat?.isFile() || stat.isSymbolicLink()
54
+ || !/^[a-f0-9]{64}$/.test(digest)) {
55
+ throw new Error('nightly proof bundle target must be a regular absolute ZIP with a SHA-256 digest');
56
+ }
57
+ const actual = sha256(fs.readFileSync(spec));
58
+ if (actual !== digest) throw new Error('nightly proof bundle target digest mismatch');
59
+ return { spec: path.resolve(spec), sha256: digest };
60
+ }
61
+
62
+ // The scheduler is a self-contained installed read model. The producer owns the phase vocabulary
63
+ // and carries it in each receipt; this adapter validates that declared contract without redefining
64
+ // the semantic phase list inside the plugin payload.
65
+ export function validateRefreshReceiptEnvelope(receipt) {
66
+ if (!receipt || receipt.schemaVersion !== 3 || receipt.kind !== 'ruvnet-brain-refresh-run') {
67
+ return { ok: false, why: 'receipt schema is invalid' };
68
+ }
69
+ const declared = receipt.requiredPhaseOrder;
70
+ const phases = receipt.phases;
71
+ if (!Array.isArray(declared) || declared.length === 0 || !Array.isArray(phases)
72
+ || declared.some((phase) => typeof phase !== 'string' || !SAFE_PHASE.test(phase))
73
+ || new Set(declared).size !== declared.length) {
74
+ return { ok: false, why: 'declared phase contract is invalid' };
75
+ }
76
+ if (JSON.stringify(phases.map(({ phase }) => phase)) !== JSON.stringify(declared)) {
77
+ return { ok: false, why: 'phase ledger differs from its declared contract' };
78
+ }
79
+ const requiredFailure = phases.find((phase) => phase.required !== false && phase.status !== 'PASS');
80
+ if (receipt.status !== 'SUCCEEDED' || !['applied', 'noop'].includes(receipt.terminalVerdict) || requiredFailure) {
81
+ return { ok: false, why: requiredFailure
82
+ ? `required phase ${requiredFailure.phase}=${requiredFailure.status}`
83
+ : `terminal state is ${receipt.status}/${receipt.terminalVerdict || 'unknown'}` };
84
+ }
85
+ return { ok: true };
86
+ }
87
+
88
+ function sameExecutable(left, right) {
89
+ try { return fs.realpathSync(left) === fs.realpathSync(right); }
90
+ catch { return path.resolve(left || '') === path.resolve(right || ''); }
91
+ }
92
+
93
+ function inspectRefreshOwner(owner) {
94
+ if (!owner || owner.host !== os.hostname() || !Number.isSafeInteger(owner.pid)
95
+ || owner.pid < 1 || !owner.processStart || !owner.executable) return 'unknown';
96
+ try { process.kill(owner.pid, 0); } catch (error) { return error?.code === 'EPERM' ? 'unknown' : 'dead'; }
97
+ try {
98
+ if (process.platform === 'win32') {
99
+ const result = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command',
100
+ `$p=Get-Process -Id ${owner.pid} -ErrorAction Stop; `
101
+ + `Write-Output ($p.StartTime.ToUniversalTime().ToString('o') + '|' + $p.Path)`], { encoding: 'utf8' });
102
+ if (result.status !== 0) return 'unknown';
103
+ const [processStart, ...rest] = String(result.stdout || '').trim().split('|');
104
+ return processStart === owner.processStart && sameExecutable(rest.join('|'), owner.executable) ? 'live' : 'dead';
105
+ }
106
+ const started = spawnSync('ps', ['-p', String(owner.pid), '-o', 'lstart='], {
107
+ encoding: 'utf8', env: { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' },
108
+ });
109
+ const command = spawnSync('ps', ['-p', String(owner.pid), '-o', 'comm='], { encoding: 'utf8' });
110
+ if (started.status !== 0 || command.status !== 0) return 'unknown';
111
+ return String(started.stdout || '').trim() === owner.processStart
112
+ && sameExecutable(String(command.stdout || '').trim(), owner.executable) ? 'live' : 'dead';
113
+ } catch { return 'unknown'; }
114
+ }
115
+
116
+ export function installNightlyRunner({ brainHome, source, nodePath = process.execPath,
117
+ identity = NIGHTLY_LABEL, packageTarget, bundleTarget, env = {} } = {}) {
118
+ validateIdentity(identity);
119
+ if (!source || !fs.existsSync(source)) throw new Error(`nightly runner source is missing: ${source || '(unset)'}`);
120
+ const bytes = fs.readFileSync(source);
121
+ const digest = sha256(bytes);
122
+ const dir = path.join(brainHome, 'scheduler');
123
+ const runnerPath = path.join(dir, `nightly-refresh-${digest}.mjs`);
124
+ fs.mkdirSync(dir, { recursive: true });
125
+ if (fs.existsSync(runnerPath) && sha256(fs.readFileSync(runnerPath)) !== digest) {
126
+ throw new Error(`nightly runner at ${runnerPath} does not match its content-addressed identity`);
127
+ }
128
+ if (!fs.existsSync(runnerPath)) fs.writeFileSync(runnerPath, bytes, { mode: 0o755 });
129
+ const record = { schemaVersion: 2, kind: 'ruvnet-brain-nightly-scheduler', identity,
130
+ nodePath: path.resolve(nodePath), runnerPath, runnerSha256: digest, argv: [],
131
+ environment: Object.fromEntries(NIGHTLY_ENV_ALLOWLIST.filter(key => env[key] !== undefined).map(key => [key, String(env[key])])),
132
+ packageTarget: normalizePackageTarget(packageTarget), bundleTarget: normalizeBundleTarget(bundleTarget, identity) };
133
+ const recordPath = path.join(dir, registrationName(identity));
134
+ const tmp = `${recordPath}.tmp-${process.pid}`;
135
+ fs.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`);
136
+ fs.renameSync(tmp, recordPath);
137
+ return { ...record, recordPath };
138
+ }
139
+
140
+ export function readNightlyRegistration({ brainHome, identity = NIGHTLY_LABEL,
141
+ recordPath = path.join(brainHome, 'scheduler', registrationName(identity)) }) {
142
+ try {
143
+ validateIdentity(identity);
144
+ const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
145
+ if (record.schemaVersion !== 2 || record.kind !== 'ruvnet-brain-nightly-scheduler' || record.identity !== identity
146
+ || !path.isAbsolute(record.nodePath) || !path.isAbsolute(record.runnerPath)
147
+ || !Array.isArray(record.argv) || record.argv.length !== 0) throw new Error('invalid registration schema');
148
+ if (record.environment !== undefined && (!record.environment || Array.isArray(record.environment) || Object.entries(record.environment).some(([key, value]) => !NIGHTLY_ENV_ALLOWLIST.includes(key) || typeof value !== 'string'))) throw new Error('invalid registered environment');
149
+ const actual = sha256(fs.readFileSync(record.runnerPath));
150
+ if (actual !== record.runnerSha256) throw new Error('runner digest mismatch');
151
+ const packageTarget = normalizePackageTarget(record.packageTarget);
152
+ const bundleTarget = normalizeBundleTarget(record.bundleTarget, identity);
153
+ return { ok: true, record: { ...record, packageTarget, bundleTarget, recordPath } };
154
+ } catch (error) {
155
+ return { ok: false, recordPath, why: error.message };
156
+ }
157
+ }
158
+
159
+ export function verifyNightlyExecutionIdentity({ brainHome, env = process.env } = {}) {
160
+ const recordPath = String(env.RUVNET_NIGHTLY_REGISTRATION || '');
161
+ if (!path.isAbsolute(recordPath)) return { ok: false, why: 'nightly registration path is missing or not absolute' };
162
+ const identity = String(env.RUVNET_NIGHTLY_IDENTITY || '');
163
+ const registration = readNightlyRegistration({ brainHome, identity, recordPath });
164
+ if (!registration.ok) return registration;
165
+ const record = registration.record;
166
+ const observed = {
167
+ schedulerIdentity: String(env.RUVNET_NIGHTLY_IDENTITY || ''),
168
+ registrationPath: recordPath,
169
+ nodePath: String(env.RUVNET_NIGHTLY_NODE_PATH || ''),
170
+ runnerPath: String(env.RUVNET_NIGHTLY_RUNNER_PATH || ''),
171
+ runnerSha256: String(env.RUVNET_NIGHTLY_RUNNER_SHA256 || ''),
172
+ argv: [],
173
+ };
174
+ if (observed.schedulerIdentity !== record.identity
175
+ || path.resolve(observed.nodePath || '') !== record.nodePath
176
+ || path.resolve(observed.runnerPath || '') !== record.runnerPath
177
+ || observed.runnerSha256 !== record.runnerSha256) {
178
+ return { ok: false, why: 'nightly execution identity differs from the verified registration', record };
179
+ }
180
+ return { ok: true, identity: observed, record };
181
+ }
182
+
183
+ export function resolveNightlyProofBundle({ brainHome, env = process.env } = {}) {
184
+ if (env.RUVNET_NIGHTLY !== '1') return null;
185
+ const execution = verifyNightlyExecutionIdentity({ brainHome, env });
186
+ if (!execution.ok) throw new Error(execution.why);
187
+ return execution.record.bundleTarget;
188
+ }
189
+
190
+ export function refreshRunHealth({ brainHome, identity = NIGHTLY_LABEL, now = Date.now(), maxAgeHours = 30,
191
+ inspectOwner = inspectRefreshOwner } = {}) {
192
+ const dir = path.join(brainHome, 'refresh-runs');
193
+ let files = [];
194
+ try {
195
+ files = fs.readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
196
+ } catch { return { state: 'never-ran', evidence: 'No nightly refresh receipt exists yet.', receipt: null }; }
197
+ let receipt = null;
198
+ let unreadable = null;
199
+ for (const name of files) {
200
+ try {
201
+ const candidate = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
202
+ if (candidate.kind === 'ruvnet-brain-refresh-run' && candidate.action === 'nightly'
203
+ && candidate.schedulerIdentity === identity) {
204
+ const candidateStamp = Date.parse(candidate.finishedAt || candidate.startedAt || '');
205
+ const receiptStamp = Date.parse(receipt?.finishedAt || receipt?.startedAt || '');
206
+ if (!receipt || (Number.isFinite(candidateStamp) && (!Number.isFinite(receiptStamp) || candidateStamp > receiptStamp))) {
207
+ receipt = candidate;
208
+ }
209
+ }
210
+ } catch (error) { unreadable ||= `${name}: ${error.message}`; }
211
+ }
212
+ if (!receipt) return unreadable
213
+ ? { state: 'failed', evidence: `Nightly receipt is unreadable (${unreadable}).`, receipt: null }
214
+ : { state: 'never-ran', evidence: 'No nightly refresh receipt exists yet.', receipt: null };
215
+ const stamp = Date.parse(receipt.finishedAt || receipt.startedAt || '');
216
+ if (!Number.isFinite(stamp)) return { state: 'failed', evidence: 'Latest nightly receipt has no valid timestamp.', receipt };
217
+ const ageHours = (now - stamp) / 3_600_000;
218
+ const registration = readNightlyRegistration({ brainHome, identity });
219
+ if (!registration.ok) return { state: 'failed', ageHours,
220
+ evidence: `Nightly registration is invalid: ${registration.why}`, receipt };
221
+ const expectedIdentity = { schedulerIdentity: registration.record.identity,
222
+ registrationPath: registration.record.recordPath, nodePath: registration.record.nodePath,
223
+ runnerPath: registration.record.runnerPath, runnerSha256: registration.record.runnerSha256, argv: [] };
224
+ if (receipt.schedulerIdentity !== registration.record.identity
225
+ || JSON.stringify(receipt.executableIdentity) !== JSON.stringify(expectedIdentity)) {
226
+ return { state: 'failed', ageHours, evidence: 'Nightly receipt is not bound to the registered runner bytes.', receipt };
227
+ }
228
+ if (['RUNNING', 'SETTLING'].includes(receipt.status)) {
229
+ const owner = inspectOwner(receipt.ownerToken);
230
+ if (owner === 'live') return { state: 'running', ageHours, evidence: `Nightly refresh ${receipt.runId} has a live exact owner.`, receipt };
231
+ return { state: owner === 'dead' ? 'failed' : 'unknown', ageHours,
232
+ evidence: owner === 'dead'
233
+ ? `Nightly refresh ${receipt.runId} has a dead exact owner.`
234
+ : `Nightly refresh ${receipt.runId} owner cannot be established exactly.`, receipt };
235
+ }
236
+ const envelope = validateRefreshReceiptEnvelope(receipt);
237
+ if (!envelope.ok) return { state: 'failed', ageHours,
238
+ evidence: `Nightly refresh ${receipt.runId} is invalid: ${envelope.why}.`, receipt };
239
+ if (ageHours > maxAgeHours) {
240
+ return { state: 'stale', ageHours, evidence: `Last verified nightly refresh is ${ageHours.toFixed(1)}h old.`, receipt };
241
+ }
242
+ return { state: receipt.status === 'SUCCEEDED' ? 'ok' : 'failed', ageHours,
243
+ evidence: `Last nightly refresh ${receipt.terminalVerdict} successfully ${ageHours.toFixed(1)}h ago.`, receipt };
244
+ }
245
+
246
+ export function nightlyCommand(record) {
247
+ return `${shellQuote(record.nodePath)} ${shellQuote(record.runnerPath)} --registration ${shellQuote(record.recordPath)}`;
248
+ }
249
+
250
+ export function nightlyArtifact({ platform = process.platform, env = process.env, brainHome,
251
+ identity = NIGHTLY_LABEL } = {}) {
252
+ validateIdentity(identity);
253
+ const home = homeOf(env);
254
+ if (platform === 'darwin') return { supported: true, platform, kind: 'launchd', label: identity,
255
+ path: path.join(home, 'Library', 'LaunchAgents', `${identity}.plist`) };
256
+ if (platform === 'linux') return { supported: true, platform, kind: 'cron', label: identity,
257
+ path: `crontab:${identity}` };
258
+ if (platform === 'win32') return { supported: true, platform, kind: 'task-scheduler', label: identity,
259
+ path: `Task Scheduler:${identity}` };
260
+ return { supported: false, platform, kind: 'unsupported', label: identity,
261
+ path: path.join(brainHome || home, 'scheduler', registrationName(identity)) };
262
+ }
263
+
264
+ function schedulerEnvironment(record, env = {}) {
265
+ const result = {};
266
+ for (const key of NIGHTLY_ENV_ALLOWLIST) if (env[key] !== undefined) result[key] = String(env[key]);
267
+ return { ...result,
268
+ RUVNET_NIGHTLY_REGISTRATION: record.recordPath,
269
+ RUVNET_NIGHTLY_IDENTITY: record.identity,
270
+ };
271
+ }
272
+
273
+ export function launchdPlist(record, { kbDir, logPath, pathValue, env = {} }) {
274
+ const home = homeOf(env);
275
+ const schedulerPath = [...new Set([
276
+ path.dirname(record.nodePath), path.join(home, '.npm-global', 'bin'), path.join(home, '.local', 'bin'),
277
+ ...String(pathValue || '').split(':').filter(Boolean), '/usr/bin', '/bin', '/usr/sbin', '/sbin',
278
+ ])].join(':');
279
+ const environment = schedulerEnvironment(record, { ...env, ...record.environment, PATH: schedulerPath });
280
+ const environmentXml = Object.entries(environment).map(([key, value]) =>
281
+ `<key>${xmlEscape(key)}</key><string>${xmlEscape(value)}</string>`).join('');
282
+ return `<?xml version="1.0" encoding="UTF-8"?>
283
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
284
+ <plist version="1.0"><dict>
285
+ <key>Label</key><string>${xmlEscape(record.identity)}</string>
286
+ <key>ProgramArguments</key><array>
287
+ <string>${xmlEscape(record.nodePath)}</string>
288
+ <string>${xmlEscape(record.runnerPath)}</string>
289
+ </array>
290
+ <key>EnvironmentVariables</key><dict>${environmentXml}</dict>
291
+ <key>WorkingDirectory</key><string>${xmlEscape(kbDir)}</string>
292
+ <key>StandardOutPath</key><string>${xmlEscape(logPath)}</string>
293
+ <key>StandardErrorPath</key><string>${xmlEscape(logPath)}</string>
294
+ <key>StartCalendarInterval</key><dict><key>Hour</key><integer>${NIGHTLY_HOUR}</integer><key>Minute</key><integer>${NIGHTLY_MINUTE}</integer></dict>
295
+ <key>RunAtLoad</key><false/>
296
+ </dict></plist>
297
+ `;
298
+ }
299
+
300
+ export function cronLine(record, logPath, { proofTick = false, proofAt } = {}) {
301
+ if (proofTick && !NIGHTLY_PROOF_LABEL.test(record.identity)) throw new Error('proof cadence requires proof identity');
302
+ const at = new Date(proofAt);
303
+ if (proofTick && !Number.isFinite(at.getTime())) throw new Error('proof cadence requires explicit proofAt timestamp');
304
+ const schedule = proofTick ? `${at.getMinutes()} ${at.getHours()} ${at.getDate()} ${at.getMonth() + 1} *` : `${NIGHTLY_MINUTE} ${NIGHTLY_HOUR} * * *`;
305
+ return `${schedule} ${nightlyCommand(record)} >> ${shellQuote(logPath)} 2>&1 # ${record.identity}`;
306
+ }
307
+
308
+ const xmlDecode = value => value.replace(/&#(?:x([a-f0-9]+)|([0-9]+));|&(amp|lt|gt|quot|apos);/gi,
309
+ (_match, hex, dec, named) => hex || dec ? String.fromCodePoint(parseInt(hex || dec, hex ? 16 : 10))
310
+ : ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" })[named.toLowerCase()]);
311
+ const onlyXmlMatch = (xml, pattern) => { const hits = [...xml.matchAll(pattern)]; return hits.length === 1 ? hits[0][1] : null; };
312
+ const windowsArguments = record => `"${record.runnerPath}" --registration "${record.recordPath}"`;
313
+ function exactWindowsCommand(xml, record) {
314
+ const actions = onlyXmlMatch(xml, /<Actions(?:\s[^>]*)?>([\s\S]*?)<\/Actions>/g);
315
+ if (actions === null || actions.replace(/<Exec(?:\s[^>]*)?>[\s\S]*?<\/Exec>/g, '').trim()) return false;
316
+ const exec = onlyXmlMatch(actions, /<Exec(?:\s[^>]*)?>([\s\S]*?)<\/Exec>/g);
317
+ if (exec === null) return false;
318
+ const command = onlyXmlMatch(exec, /<Command>([^<]*)<\/Command>/g);
319
+ const args = onlyXmlMatch(exec, /<Arguments>([^<]*)<\/Arguments>/g);
320
+ return command !== null && args !== null && [record.nodePath, `"${record.nodePath}"`].includes(xmlDecode(command))
321
+ && xmlDecode(args) === windowsArguments(record);
322
+ }
323
+ function exactLaunchdCommand(plist, record) {
324
+ const array = onlyXmlMatch(plist, /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/g);
325
+ const environment = onlyXmlMatch(plist, /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/g);
326
+ if (/<key>Program<\/key>/.test(plist) || array === null || environment === null || array.replace(/<string>[^<]*<\/string>/g, '').trim()) return false;
327
+ const argv = [...array.matchAll(/<string>([^<]*)<\/string>/g)].map(match => xmlDecode(match[1]));
328
+ if (JSON.stringify(argv) !== JSON.stringify([record.nodePath, record.runnerPath])) return false;
329
+ const entries = [...environment.matchAll(/<key>([^<]*)<\/key>\s*<string>([^<]*)<\/string>/g)];
330
+ if (environment.replace(/<key>[^<]*<\/key>\s*<string>[^<]*<\/string>/g, '').trim()) return false;
331
+ const env = Object.fromEntries(entries.map(([, key, value]) => [xmlDecode(key), xmlDecode(value)]));
332
+ if (Object.keys(env).length !== entries.length || Object.keys(env).some(key => ![...NIGHTLY_ENV_ALLOWLIST, 'RUVNET_NIGHTLY_REGISTRATION', 'RUVNET_NIGHTLY_IDENTITY'].includes(key))) return false;
333
+ return env.RUVNET_NIGHTLY_REGISTRATION === record.recordPath && env.RUVNET_NIGHTLY_IDENTITY === record.identity
334
+ && Object.entries(record.environment || {}).every(([key, value]) => key === 'PATH' || env[key] === value);
335
+ }
336
+
337
+ // Test mode persists simulated state only below the explicitly isolated fixture home.
338
+ function fixtureSchedulerRun(env, platform, identity) {
339
+ const home = env.HOME || env.USERPROFILE;
340
+ if (!home || !path.isAbsolute(home)) throw new Error('scheduler test mode requires an explicit absolute fixture home');
341
+ const file = path.join(home, '.ruvnet-scheduler-test', `${platform}-${identity}.json`);
342
+ return (_cmd, args, options = {}) => {
343
+ if (args[0] === '-l') return { status: 0, stdout: fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '' };
344
+ if (args[0] === '/Query') return fs.existsSync(file)
345
+ ? { status: 0, stdout: fs.readFileSync(file, 'utf8') }
346
+ : { status: 1, stderr: 'The system cannot find the file specified.' };
347
+ if (args[0] === '/Delete') { fs.rmSync(file, { force: true }); return { status: 0 }; }
348
+ fs.mkdirSync(path.dirname(file), { recursive: true });
349
+ const command = args[args.indexOf('/TR') + 1];
350
+ const task = command?.match(/^"([^"]+)" (.*)$/);
351
+ fs.writeFileSync(file, args[0] === '-' ? options.input
352
+ : `<Task><Actions><Exec><Command>${xmlEscape(task[1])}</Command><Arguments>${xmlEscape(task[2])}</Arguments></Exec></Actions></Task>`);
353
+ return { status: 0 };
354
+ };
355
+ }
356
+
357
+ const ownsCronRow = (row, identity) => row.trimEnd().endsWith(`# ${identity}`);
358
+
359
+ function cronRows(run) {
360
+ const listed = run('crontab', ['-l'], { encoding: 'utf8' });
361
+ if (listed.error) return { ok: false, why: 'no crontab command was found on this system' };
362
+ if (listed.status !== 0 && !/^(?:[^\n]*: )?no crontab for [^\n]+$/i.test(String(listed.stderr || '').trim())) return { ok: false, why: String(listed.stderr || listed.stdout || `crontab exited ${listed.status}`).trim() };
363
+ return { ok: true, rows: String(listed.stdout || '').split('\n').filter(Boolean) };
364
+ }
365
+
366
+ export function installScheduler(record, { platform = process.platform, env = process.env, kbDir,
367
+ run = spawnSync, testMode = false, proofTick = false, proofAt, pathValue = process.env.PATH || '' } = {}) {
368
+ validateIdentity(record.identity);
369
+ if (proofTick && !NIGHTLY_PROOF_LABEL.test(record.identity)) throw new Error('proof cadence requires proof identity');
370
+ const artifact = nightlyArtifact({ platform, env, brainHome: path.dirname(kbDir), identity: record.identity });
371
+ if (!artifact.supported) return { ok: false, artifact, why: `unsupported platform: ${platform}` };
372
+ if (testMode && platform !== 'darwin') run = fixtureSchedulerRun(env, platform, record.identity);
373
+ const logPath = path.join(kbDir, 'update.log');
374
+ if (platform === 'darwin') {
375
+ fs.mkdirSync(path.dirname(artifact.path), { recursive: true });
376
+ fs.writeFileSync(artifact.path, launchdPlist(record, { kbDir, logPath, pathValue, env }));
377
+ if (!testMode) {
378
+ const uid = process.getuid();
379
+ run('launchctl', ['bootout', `gui/${uid}/${record.identity}`], { stdio: 'ignore' });
380
+ const boot = run('launchctl', ['bootstrap', `gui/${uid}`, artifact.path], { encoding: 'utf8' });
381
+ if (boot.error || boot.status !== 0) return { ok: false, artifact, why: boot.error?.message || String(boot.stderr || `exit ${boot.status}`).trim() };
382
+ }
383
+ return { ok: true, artifact, already: false };
384
+ }
385
+ if (platform === 'linux') {
386
+ const current = cronRows(run);
387
+ if (!current.ok) return { ...current, artifact };
388
+ const line = cronLine(record, logPath, { proofTick, proofAt });
389
+ const retained = current.rows.filter((row) => !ownsCronRow(row, record.identity));
390
+ if (current.rows.includes(line)) return { ok: true, artifact, already: true };
391
+ const write = run('crontab', ['-'], { input: `${[...retained, line].join('\n')}\n`, encoding: 'utf8' });
392
+ if (write.error || write.status !== 0) return { ok: false, artifact, why: write.error?.message || String(write.stderr || `exit ${write.status}`).trim() };
393
+ return { ok: true, artifact, already: false };
394
+ }
395
+ const taskCommand = `"${record.nodePath}" ${windowsArguments(record)}`;
396
+ const created = run('schtasks', ['/Create', '/SC', 'DAILY', '/TN', record.identity, '/TR', taskCommand,
397
+ '/ST', `${String(NIGHTLY_HOUR).padStart(2, '0')}:${String(NIGHTLY_MINUTE).padStart(2, '0')}`, '/F'], { encoding: 'utf8' });
398
+ if (created.error || created.status !== 0) return { ok: false, artifact, why: created.error?.message || String(created.stderr || `exit ${created.status}`).trim() };
399
+ return { ok: true, artifact, already: false };
400
+ }
401
+
402
+ export function removeScheduler({ platform = process.platform, env = process.env, run = spawnSync, testMode = false,
403
+ identity = NIGHTLY_LABEL } = {}) {
404
+ const artifact = nightlyArtifact({ platform, env, identity });
405
+ if (!artifact.supported) return { ok: false, artifact, why: `unsupported platform: ${platform}` };
406
+ if (testMode && platform !== 'darwin') run = fixtureSchedulerRun(env, platform, identity);
407
+ if (platform === 'darwin') {
408
+ const existed = fs.existsSync(artifact.path);
409
+ if (!testMode) {
410
+ run('launchctl', ['bootout', `gui/${process.getuid()}/${identity}`], { encoding: 'utf8' });
411
+ const remaining = run('launchctl', ['print', `gui/${process.getuid()}/${identity}`], { encoding: 'utf8' });
412
+ if (remaining.error || remaining.status === 0 || !/could not find|not found|no such process/i.test(String(remaining.stderr || remaining.stdout))) {
413
+ return { ok: false, artifact, why: 'LaunchAgent absence could not be confirmed; local files retained' };
414
+ }
415
+ }
416
+ fs.rmSync(artifact.path, { force: true });
417
+ return { ok: true, artifact, already: !existed };
418
+ }
419
+ if (platform === 'linux') {
420
+ const current = cronRows(run);
421
+ if (!current.ok) return { ...current, artifact };
422
+ const retained = current.rows.filter((row) => !ownsCronRow(row, identity));
423
+ if (retained.length === current.rows.length) return { ok: true, artifact, already: true };
424
+ const write = run('crontab', ['-'], { input: retained.length ? `${retained.join('\n')}\n` : '', encoding: 'utf8' });
425
+ if (write.error || write.status !== 0) return { ok: false, artifact, why: write.error?.message || String(write.stderr || `exit ${write.status}`).trim() };
426
+ return { ok: true, artifact };
427
+ }
428
+ const removed = run('schtasks', ['/Delete', '/TN', identity, '/F'], { encoding: 'utf8' });
429
+ const absent = removed.status === 1 && /cannot find|does not exist/i.test(String(removed.stderr || removed.stdout));
430
+ if (removed.error || (removed.status !== 0 && !absent)) return { ok: false, artifact, why: removed.error?.message || String(removed.stderr || `exit ${removed.status}`).trim() };
431
+ return { ok: true, artifact, already: absent };
432
+ }
433
+
434
+ export function schedulerStatus({ platform = process.platform, env = process.env, brainHome,
435
+ kbDir = path.join(brainHome, 'kb'), run = spawnSync, testMode = false, proofTick = false, proofAt, identity = NIGHTLY_LABEL } = {}) {
436
+ const finish = (status) => ({ ...status, runHealth: refreshRunHealth({ brainHome, identity }) });
437
+ const artifact = nightlyArtifact({ platform, env, brainHome, identity });
438
+ if (!artifact.supported) return finish({ state: 'unsupported', evidence: `No scheduler adapter for ${platform}.`, artifact });
439
+ const registration = readNightlyRegistration({ brainHome, identity });
440
+ if (!registration.ok) return finish({ state: fs.existsSync(registration.recordPath) ? 'degraded' : 'off',
441
+ evidence: `Registration invalid: ${registration.why}`, artifact, registration });
442
+ const record = registration.record;
443
+ if (testMode && platform !== 'darwin') run = fixtureSchedulerRun(env, platform, identity);
444
+ if (platform === 'darwin') {
445
+ if (!fs.existsSync(artifact.path)) return finish({ state: 'off', evidence: `No LaunchAgent plist at ${artifact.path}`, artifact, registration });
446
+ const plist = fs.readFileSync(artifact.path, 'utf8');
447
+ if (!exactLaunchdCommand(plist, record)) {
448
+ return finish({ state: 'degraded', evidence: 'LaunchAgent command does not match the registered runner', artifact, registration });
449
+ }
450
+ if (testMode) return finish({ state: 'on', evidence: `LaunchAgent plist and runner digest verified at ${artifact.path}`, artifact, registration });
451
+ const listed = run('launchctl', ['print', `gui/${process.getuid()}/${identity}`], { encoding: 'utf8' });
452
+ const loaded = !listed.error && listed.status === 0;
453
+ if (!loaded) return finish({ state: 'degraded', evidence: 'LaunchAgent plist exists but job is not loaded', artifact, registration });
454
+ const exit = String(listed.stdout || '').match(/last exit code\s*=\s*(-?\d+)/i);
455
+ if (exit && Number(exit[1]) !== 0) {
456
+ return finish({ state: 'degraded', evidence: `LaunchAgent is loaded and runner digest verified, but last exited ${exit[1]}`, artifact, registration });
457
+ }
458
+ return finish({ state: 'on', evidence: exit
459
+ ? 'LaunchAgent is loaded, runner digest verified, and last exited cleanly'
460
+ : 'LaunchAgent is loaded and runner digest verified; no completed run is recorded yet', artifact, registration });
461
+ }
462
+ if (platform === 'linux') {
463
+ const current = cronRows(run);
464
+ const expected = cronLine(record, path.join(kbDir, 'update.log'), { proofTick, proofAt });
465
+ const on = current.ok && current.rows.includes(expected);
466
+ return finish({ state: on ? 'on' : !current.ok || current.rows.some((row) => ownsCronRow(row, identity)) ? 'degraded' : 'off', evidence: on ? 'Managed crontab entry and runner digest verified' : current.why || 'Exact managed crontab entry is absent', artifact, registration });
467
+ }
468
+ const queried = run('schtasks', ['/Query', '/TN', identity, '/XML'], { encoding: 'utf8' });
469
+ const xml = String(queried.stdout || '');
470
+ const on = !queried.error && queried.status === 0 && exactWindowsCommand(xml, record);
471
+ return finish({ state: on ? 'on' : !queried.error && queried.status === 1 && /cannot find|does not exist/i.test(String(queried.stderr || queried.stdout)) ? 'off' : 'degraded', evidence: on ? 'Task Scheduler entry and runner digest verified' : 'Exact Task Scheduler entry is absent', artifact, registration });
472
+ }
@@ -108,10 +108,10 @@ export function redactProgression(value) {
108
108
  let output = current;
109
109
  for (const rule of INLINE_SECRETS) {
110
110
  rule.pattern.lastIndex = 0;
111
- if (rule.pattern.test(output)) {
111
+ const replaced = output.replace(rule.pattern, rule.replace);
112
+ if (replaced !== output) {
112
113
  mark(currentPath, rule.kind);
113
- rule.pattern.lastIndex = 0;
114
- output = output.replace(rule.pattern, rule.replace);
114
+ output = replaced;
115
115
  }
116
116
  }
117
117
  return output;
@@ -122,7 +122,7 @@ export function redactProgression(value) {
122
122
  const path = `${currentPath}.${key}`;
123
123
  const kind = secretKind(key);
124
124
  if (kind && current[key] !== null && current[key] !== undefined) {
125
- mark(path, kind);
125
+ if (current[key] !== `[REDACTED:${kind}]`) mark(path, kind);
126
126
  return [key, `[REDACTED:${kind}]`];
127
127
  }
128
128
  return [key, walk(current[key], path)];
@@ -7,14 +7,15 @@ import {
7
7
  validateProgressionSnapshot,
8
8
  } from './project-progression-contract.mjs';
9
9
  import { resolveProjectStore } from './project-store-resolver.mjs';
10
- import { resolveRuflo, RUFLO_MISSING } from './ruflo-bin.mjs';
10
+ import { resolveRuflo, rufloInvocation, RUFLO_MISSING } from './ruflo-bin.mjs';
11
11
 
12
12
  const PROGRESSION_NAMESPACE = 'project-progression';
13
13
  const RESUME_SCHEMA = 'ruvnet-brain.project-resume';
14
14
  const RESUME_VERSION = 1;
15
15
 
16
16
  function defaultRunner(binary, args, options) {
17
- return spawnSync(binary, args, options);
17
+ const invocation = rufloInvocation(binary, args);
18
+ return spawnSync(invocation.executable, invocation.args, { ...options, shell: false });
18
19
  }
19
20
 
20
21
  function resultStatus(result) {
@@ -153,23 +154,51 @@ export class ProjectProgressionStore {
153
154
  return receipts;
154
155
  }
155
156
 
156
- listSnapshotKeys({ pageSize = 100 } = {}) {
157
+ listSnapshotKeys({ pageSize = 100, maxEntries = 10_000 } = {}) {
157
158
  requirePositiveInteger(pageSize, 'pageSize');
159
+ requirePositiveInteger(maxEntries, 'maxEntries');
160
+ if (pageSize > maxEntries) throw new Error('pageSize exceeds the enumeration bound');
158
161
  const keys = [];
159
162
  const seen = new Set();
160
163
  let offset = 0;
161
164
  let total = null;
165
+ let protocol = null;
166
+ let limit = pageSize;
162
167
  do {
163
168
  const listed = this.run([
164
169
  'memory', 'list', '--namespace', PROGRESSION_NAMESPACE,
165
- '--limit', String(pageSize), '--offset', String(offset), '--page-info', '--format', 'json',
170
+ '--limit', String(limit), '--offset', String(offset), '--page-info', '--format', 'json',
166
171
  '--path', this.resolution.canonicalAgentDbPath,
167
172
  ]);
168
173
  if (resultStatus(listed) !== 0) {
169
174
  throw new Error(`progression structural pagination failed: ${resultText(listed, 'stderr').trim() || 'unknown error'}`);
170
175
  }
171
176
  const page = parseJson(resultText(listed, 'stdout'), 'progression pagination page');
177
+ const nextProtocol = Array.isArray(page) ? 'array' : 'page';
178
+ if (protocol && protocol !== nextProtocol) throw new Error('enumeration protocol changed during restoration');
179
+ protocol = nextProtocol;
180
+ if (protocol === 'array') {
181
+ // Global Ruflo's CLI returns the first `limit` entries and ignores offset/page-info.
182
+ // Grow from zero until its result is shorter than the requested limit. The current
183
+ // CLI honors limit (memory.js -> listEntries); a full response at our cap is ambiguous
184
+ // and must never become a partial successful restore.
185
+ if (page.length > limit) throw new Error('malformed pagination array');
186
+ const current = new Set();
187
+ for (const entry of page) {
188
+ if (!plainRecord(entry) || typeof entry.key !== 'string' || !entry.key
189
+ || entry.namespace !== PROGRESSION_NAMESPACE) throw new Error('malformed pagination entry');
190
+ if (current.has(entry.key)) throw new Error(`duplicate progression key in array: ${entry.key}`);
191
+ current.add(entry.key);
192
+ }
193
+ if ([...seen].some((key) => !current.has(key))) throw new Error('enumeration keys changed during restoration');
194
+ if (page.length < limit) return [...current].sort();
195
+ if (limit === maxEntries) throw new Error('progression enumeration reached its bound without proving completeness');
196
+ for (const key of current) seen.add(key);
197
+ limit = Math.min(maxEntries, limit * 2);
198
+ continue;
199
+ }
172
200
  total = validatePage(page, { offset, pageSize, total });
201
+ if (total > maxEntries) throw new Error('progression enumeration exceeds its bound');
173
202
  for (const entry of page.entries) {
174
203
  if (!plainRecord(entry) || typeof entry.key !== 'string' || !entry.key
175
204
  || entry.namespace !== PROGRESSION_NAMESPACE) throw new Error('malformed pagination entry');
@@ -209,10 +238,10 @@ export class ProjectProgressionStore {
209
238
  return { snapshots, rejected: sortRejected(rejected) };
210
239
  }
211
240
 
212
- restoreLatest({ pageSize = 100, maxOutputBytes = 64 * 1024 } = {}) {
241
+ restoreLatest({ pageSize = 100, maxEntries = 10_000, maxOutputBytes = 64 * 1024 } = {}) {
213
242
  requirePositiveInteger(maxOutputBytes, 'maxOutputBytes');
214
243
  this.replay();
215
- const keys = this.listSnapshotKeys({ pageSize });
244
+ const keys = this.listSnapshotKeys({ pageSize, maxEntries });
216
245
  const exact = this.retrieveSnapshots(keys);
217
246
  const restored = restoreProjectProgression(exact.snapshots, {
218
247
  expectedProjectIdentity: this.resolution.projectIdentity,
@@ -40,6 +40,26 @@ import fs from 'node:fs';
40
40
  import os from 'node:os';
41
41
  import path from 'node:path';
42
42
 
43
+ // npm's Windows .cmd shim cannot be spawned without a shell. Resolve the declared entry
44
+ // inside that same managed installation and pass argv directly to Node instead.
45
+ export function rufloInvocation(binary, args, { platform = process.platform } = {}) {
46
+ if (platform !== 'win32' || !/\.cmd$/i.test(binary)) return { executable: binary, args };
47
+ if (path.basename(binary).toLowerCase() !== 'ruflo.cmd') throw new Error('unsupported managed Ruflo shim');
48
+ const packageRoot = fs.realpathSync(path.join(path.dirname(fs.realpathSync(binary)), 'node_modules', 'ruflo'));
49
+ const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
50
+ const declared = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.ruflo;
51
+ if (manifest.name !== 'ruflo' || typeof declared !== 'string' || !declared
52
+ || path.isAbsolute(declared) || path.win32.isAbsolute(declared) || declared.split(/[\\/]/).includes('..')) {
53
+ throw new Error('managed Ruflo package has no safe declared CLI entry');
54
+ }
55
+ const entry = fs.realpathSync(path.resolve(packageRoot, declared));
56
+ const relative = path.relative(packageRoot, entry);
57
+ if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !fs.statSync(entry).isFile()) {
58
+ throw new Error('managed Ruflo CLI entry escapes its package');
59
+ }
60
+ return { executable: process.execPath, args: [entry, ...args] };
61
+ }
62
+
43
63
  /**
44
64
  * Locate the global ruflo. LOCATES, NEVER EXECUTES — no shell, no daemon, no side effects.
45
65
  *
@@ -67,7 +67,12 @@ function legacy(projectDir, now) {
67
67
  let names;
68
68
  try {
69
69
  const stat = fs.lstatSync(directory);
70
- if (!stat.isDirectory() || stat.isSymbolicLink()) return { values: [], malformed: true };
70
+ // A malformed ROOT (a stray file or symlink where `sessions/` should be a directory) must not
71
+ // discard every OTHER root's evidence. This used to `return` here, so a bad `.claude/sessions`
72
+ // entry made a genuinely fresh `.claude-flow/sessions` legacy session invisible to the whole
73
+ // scan — `continue` so the loop still inspects the remaining roots, same as the readdirSync
74
+ // catch below already does for an unreadable directory.
75
+ if (!stat.isDirectory() || stat.isSymbolicLink()) { malformed = true; continue; }
71
76
  names = fs.readdirSync(directory).filter((name) => /^session-.*\.json$/.test(name)).slice(-64);
72
77
  } catch { malformed = true; continue; }
73
78
  for (const name of names) {