ruvnet-brain 4.5.7 → 4.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/install.mjs +50 -1
- package/package.json +5 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +31 -1
- package/plugin/hooks/hook-contracts.json +81 -1
- package/plugin/hooks/hooks.json +31 -1
- package/plugin/scripts/agentdb-recall.mjs +38 -10
- package/plugin/scripts/continuity-hook-policy.mjs +3 -0
- package/plugin/scripts/hook-shim.mjs +1 -1
- package/plugin/scripts/kb-copy-proof.mjs +80 -19
- package/plugin/scripts/learn-capture.mjs +44 -0
- package/plugin/scripts/learn-capture.sh +2 -241
- package/plugin/scripts/learn-flush.mjs +86 -191
- package/plugin/scripts/learning-observation.mjs +51 -0
- package/plugin/scripts/learning-queue.mjs +135 -0
- package/plugin/scripts/learning-store.mjs +105 -0
- package/plugin/scripts/learning-worker-supervisor.mjs +79 -0
- package/plugin/scripts/project-progression-contract.mjs +58 -4
- package/plugin/scripts/runtime-preferences.mjs +45 -4
- package/plugin/scripts/session-start-core.mjs +8 -0
- package/plugin/scripts/session-start-proof.mjs +49 -0
- package/scripts/codex-fresh-host-proof.mjs +179 -0
- package/scripts/codex-host-execution-proof.mjs +246 -0
- package/scripts/codex-host-proof-runtime.mjs +104 -0
- package/scripts/console-engine.mjs +29 -16
- package/scripts/health-repair.mjs +47 -68
- package/scripts/onboarding-console.mjs +25 -54
- package/scripts/qa/progression-validation-benchmark.mjs +66 -0
- package/scripts/release-qualification-contract.mjs +49 -3
- package/scripts/remedy-registry.mjs +9 -2
|
@@ -24,7 +24,11 @@ import path from 'node:path';
|
|
|
24
24
|
import os from 'node:os';
|
|
25
25
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { findStores, diagnose } from './memory-doctor.mjs';
|
|
27
|
-
import {
|
|
27
|
+
import { distillLearning } from '../plugin/scripts/learning-store.mjs';
|
|
28
|
+
import { readSafe } from '../plugin/scripts/learning-queue.mjs';
|
|
29
|
+
import { learningContext } from '../plugin/scripts/runtime-preferences.mjs';
|
|
30
|
+
import { learningQueueDepth, learningQueueFiles, observeLearning } from '../plugin/scripts/learning-observation.mjs';
|
|
31
|
+
import { resolveRuflo, rufloInvocation } from '../plugin/scripts/ruflo-bin.mjs';
|
|
28
32
|
import { projectDirectory } from '../plugin/scripts/project-identity.mjs';
|
|
29
33
|
import { newestSnapshot, snapshotInventory, MTIME_GRACE_MS } from './snapshot-freshness.mjs';
|
|
30
34
|
|
|
@@ -34,9 +38,10 @@ const HOME = os.homedir();
|
|
|
34
38
|
// RESIDUAL of #134: RUVNET_BRAIN_PROJECT_DIR is never set by real hook dispatch on either host, so
|
|
35
39
|
// consult `projectDirectory()` (project-identity.mjs) — the CLAUDE_PROJECT_DIR-with-containment
|
|
36
40
|
// rule #85/#107 already established, reused rather than trusting the variable unconditionally.
|
|
37
|
-
const PROJECT = process.env.RUVNET_BRAIN_PROJECT_DIR || projectDirectory();
|
|
38
41
|
const argv = process.argv.slice(2);
|
|
39
42
|
const has = (f) => argv.includes(f);
|
|
43
|
+
const requestedProject = has('--project') ? argv[argv.indexOf('--project') + 1] : undefined;
|
|
44
|
+
const PROJECT = learningContext({ cwd: requestedProject || process.env.RUVNET_BRAIN_PROJECT_DIR || projectDirectory() }).projectDir;
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
47
|
* Find ruflo HONESTLY.
|
|
@@ -50,13 +55,6 @@ const has = (f) => argv.includes(f);
|
|
|
50
55
|
* Rule 21 still holds — ONE ruflo, the global one, never `npx ruflo@latest`. This resolves WHERE
|
|
51
56
|
* that one global binary is rather than assuming a path.
|
|
52
57
|
*/
|
|
53
|
-
function resolveRuflo() {
|
|
54
|
-
const preferred = path.join(HOME, '.npm-global/bin/ruflo');
|
|
55
|
-
if (fs.existsSync(preferred)) return preferred;
|
|
56
|
-
const which = spawnSync('sh', ['-lc', 'command -v ruflo'], { encoding: 'utf8', timeout: 10_000 });
|
|
57
|
-
const found = String(which.stdout || '').trim().split('\n')[0];
|
|
58
|
-
return found && fs.existsSync(found) ? found : null;
|
|
59
|
-
}
|
|
60
58
|
const RUFLO = resolveRuflo();
|
|
61
59
|
const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
|
|
62
60
|
|
|
@@ -121,61 +119,45 @@ function repairMemory() {
|
|
|
121
119
|
* queue — the reporter needed 15 rounds for 293 entries. A single call would leave a queue that
|
|
122
120
|
* "flushes" every time and never empties, which is the same lie in slow motion.
|
|
123
121
|
*/
|
|
124
|
-
function flushLearning() {
|
|
122
|
+
function flushLearning(legacyUser = false) {
|
|
123
|
+
const original = learningContext({ cwd: PROJECT });
|
|
124
|
+
const context = legacyUser && original.enabled
|
|
125
|
+
? learningContext({ cwd: PROJECT, env: { ...process.env, RUVNET_LEARNING_SCOPE: 'user' } }) : original;
|
|
126
|
+
if (!context.enabled) return { ok: true, noop: true, log: 'learning is switched off for this project — queued evidence is untouched' };
|
|
127
|
+
const { scope, queueDir } = context;
|
|
128
|
+
const bundled = path.resolve(import.meta.dirname, '../plugin/scripts/learn-flush.mjs');
|
|
125
129
|
const flusher = path.join(HOME, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin', 'scripts', 'learn-flush.mjs');
|
|
126
130
|
const local = path.join(PROJECT, 'plugin', 'scripts', 'learn-flush.mjs');
|
|
127
|
-
const script =
|
|
131
|
+
const script = [bundled, local, flusher].find((file) => fs.existsSync(file));
|
|
128
132
|
if (!script) return { ok: false, log: 'learn-flush.mjs not found — cannot drain the queue' };
|
|
129
133
|
|
|
130
|
-
const configured = process.env.RUVNET_LEARNING_SCOPE
|
|
131
|
-
|| loadRuntimePreferences({ cwd: PROJECT }).values.learningScope;
|
|
132
|
-
const scope = ['off', 'project', 'user'].includes(configured) ? configured : 'project';
|
|
133
|
-
if (scope === 'off') {
|
|
134
|
-
return { ok: true, noop: true, log: 'learning is switched off for this project — nothing is being captured, so there is nothing to feed' };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Derived from the scope, never assumed: this is the directory the child will actually read.
|
|
138
|
-
const queueDir = scope === 'user'
|
|
139
|
-
? path.join(HOME, '.cache', 'ruvnet-brain', 'learn')
|
|
140
|
-
: path.join(PROJECT, '.swarm', 'ruvnet-brain-learn');
|
|
141
134
|
// Displayed to a human and matched by tests, so it is normalised to forward slashes on every
|
|
142
135
|
// platform. Without this, Windows reports `~\.cache\ruvnet-brain\learn` while macOS and Linux
|
|
143
136
|
// report `~/.cache/ruvnet-brain/learn` — the same location under two spellings, which is the
|
|
144
137
|
// exact defect class this branch has been closing (one fact, two representations).
|
|
145
138
|
const where = queueDir.replace(HOME, '~').split(path.sep).join('/');
|
|
146
139
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const before = depth();
|
|
140
|
+
const depth = () => learningQueueDepth(queueDir);
|
|
141
|
+
try {
|
|
142
|
+
const lock = JSON.parse(readSafe(path.join(queueDir, '.worker-lock'), 4096));
|
|
143
|
+
if (lock.retirementUnconfirmed || (lock.retirementRequired && lock.expires < Date.now())) return { ok: false, log: 'learning recovery is paused: owned worker retirement is unconfirmed; queue fence and original observations retained' };
|
|
144
|
+
} catch { /* Regular safety checks below still decide whether a queue is readable. */ }
|
|
145
|
+
let before;
|
|
146
|
+
try { before = depth(); }
|
|
147
|
+
catch { return { ok: false, log: `cannot read learning queue ${where} safely — evidence is preserved` }; }
|
|
157
148
|
if (!before) return { ok: true, noop: true, log: `nothing queued in ${where} — the learner is already caught up` };
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const deadline = Date.now() + 540_000; // inside the 600s this action is allowed overall
|
|
149
|
+
const env = { ...process.env, RUVNET_BRAIN_PROJECT_DIR: PROJECT, ...(legacyUser ? { RUVNET_LEARNING_SCOPE: 'user', RUVNET_LEGACY_USER_APPLY: '1' } : {}) };
|
|
150
|
+
const deadline = Date.now() + 540_000;
|
|
161
151
|
const stalled = [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} catch (e) { stalled.push(`${path.basename(file)}: ${String(e.message).split('\n')[0].slice(0, 80)}`); break; }
|
|
172
|
-
const next = depthOf(file);
|
|
173
|
-
// STRICT progress, or stop. learn-flush KEEPS a queue it could not feed (by design — the queue
|
|
174
|
-
// is evidence), so a round that shrinks nothing means the learner is not accepting the work.
|
|
175
|
-
// Spinning on it would burn the whole budget and still report zero.
|
|
176
|
-
if (next >= d) { stalled.push(`${path.basename(file)}: ${d} entr${d === 1 ? 'y' : 'ies'} would not feed`); break; }
|
|
177
|
-
d = next;
|
|
178
|
-
}
|
|
152
|
+
let previous = before;
|
|
153
|
+
while (previous > 0 && Date.now() < deadline) {
|
|
154
|
+
try {
|
|
155
|
+
execFileSync(process.execPath, [script, '--sync'], { env, cwd: PROJECT, stdio: 'ignore',
|
|
156
|
+
timeout: Math.min(25_000, Math.max(1000, deadline - Date.now())) });
|
|
157
|
+
} catch { stalled.push('bounded worker failed'); break; }
|
|
158
|
+
const next = depth();
|
|
159
|
+
if (next >= previous) { stalled.push(`${next} entries would not feed`); break; }
|
|
160
|
+
previous = next;
|
|
179
161
|
}
|
|
180
162
|
|
|
181
163
|
const after = depth();
|
|
@@ -183,10 +165,8 @@ function flushLearning() {
|
|
|
183
165
|
if (fed <= 0) {
|
|
184
166
|
// Name the most likely cause instead of shrugging: learn-flush invokes ruflo at a FIXED path,
|
|
185
167
|
// so on a machine with a different npm prefix it feeds nothing and honestly keeps the queue.
|
|
186
|
-
const rufloAtFixedPath = fs.existsSync(path.join(HOME, '.npm-global/bin/ruflo'));
|
|
187
168
|
const why = stalled.length ? ` (${stalled.slice(0, 3).join('; ')})` : '';
|
|
188
|
-
const hint =
|
|
189
|
-
+ ' `npm i -g ruflo@latest` installs it there.';
|
|
169
|
+
const hint = RUFLO ? '' : ' No global ruflo was resolved from the managed prefix or PATH.';
|
|
190
170
|
return { ok: false, log: `fed 0 of ${before} queued events from ${where}${why} — the queue is preserved for retry.${hint}` };
|
|
191
171
|
}
|
|
192
172
|
return {
|
|
@@ -196,22 +176,20 @@ function flushLearning() {
|
|
|
196
176
|
};
|
|
197
177
|
}
|
|
198
178
|
|
|
199
|
-
/** One training cycle
|
|
179
|
+
/** One explicit training cycle in the same learner the Console measures. */
|
|
200
180
|
function trainLearning() {
|
|
181
|
+
const context = learningContext({ cwd: PROJECT });
|
|
182
|
+
if (!context.enabled) return { ok: true, noop: true, log: 'learning is switched off — queued evidence and learner are untouched' };
|
|
201
183
|
if (!RUFLO) return { ok: false, log: 'ruflo is not on this machine — install it with `npm i -g ruflo@latest` to enable learning' };
|
|
202
184
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// console's resolver so the remedy provably trains the store the card measured.
|
|
212
|
-
execFileSync(RUFLO, ['hooks', 'intelligence', '--train'], { cwd: learnerCwd({ cwd: PROJECT }), env: RUFLO_ENV, stdio: 'ignore', timeout: 600_000 });
|
|
213
|
-
} catch (e) { return { ok: false, log: `training cycle failed: ${e.message}` }; }
|
|
214
|
-
return { ok: true, log: 'ran one training cycle in the cross-project learner' };
|
|
185
|
+
const evidence = distillLearning(RUFLO, context, { deadline: Date.now() + 18_000, allowed: () => {
|
|
186
|
+
const current = learningContext({ cwd: PROJECT }); return current.enabled && current.scope === context.scope && current.queueDir === context.queueDir;
|
|
187
|
+
} });
|
|
188
|
+
const verified = evidence.completed && evidence.patternDelta > 0;
|
|
189
|
+
return { ok: verified, log: verified
|
|
190
|
+
? `verified ${evidence.patternDelta} new structural patterns in ${evidence.db}; no ratified lessons claimed; snapshot ${evidence.snapshot}`
|
|
191
|
+
: `distillation returned without measurable pattern progress in ${evidence.db}; recorded observations remain separate; snapshot retained` };
|
|
192
|
+
} catch { return { ok: false, log: 'canonical distillation unavailable; observations and pending queue retained' }; }
|
|
215
193
|
}
|
|
216
194
|
|
|
217
195
|
/**
|
|
@@ -326,6 +304,7 @@ function distillFleet() {
|
|
|
326
304
|
}
|
|
327
305
|
|
|
328
306
|
const action = has('--repair-memory') ? repairMemory
|
|
307
|
+
: has('--flush-legacy-user-learning') ? () => flushLearning(true)
|
|
329
308
|
: has('--flush-learning') ? flushLearning
|
|
330
309
|
: has('--train-learning') ? trainLearning
|
|
331
310
|
: has('--distill-fleet') ? distillFleet
|
|
@@ -67,9 +67,9 @@ import { loadLessons, updateLessons, ratify, demote, restore, pending, weightOf,
|
|
|
67
67
|
import {
|
|
68
68
|
openRouterCredentialStatus,
|
|
69
69
|
saveOpenRouterCredential,
|
|
70
|
-
learnerCwd,
|
|
71
70
|
loadRuntimePreferences,
|
|
72
71
|
} from '../plugin/scripts/runtime-preferences.mjs';
|
|
72
|
+
import { observeLearning as observeScopedLearning } from '../plugin/scripts/learning-observation.mjs';
|
|
73
73
|
import { applyNightlyChoice, nightlyStatus } from './nightly-controller.mjs';
|
|
74
74
|
// One canonical answer to "which directory is this, and have I counted it already?" — shared with
|
|
75
75
|
// the PreCompact snapshot producer (#85) and with memory-doctor's root scan (#107).
|
|
@@ -2461,8 +2461,8 @@ function journalUndo(entry) {
|
|
|
2461
2461
|
fs.appendFileSync(UNDO_JOURNAL, JSON.stringify({ token, at: new Date().toISOString(), ...entry }) + '\n');
|
|
2462
2462
|
return token;
|
|
2463
2463
|
}
|
|
2464
|
-
function runNode(scriptRelPath, args) {
|
|
2465
|
-
const r = spawnSync(process.execPath, [path.join(REPO, scriptRelPath), ...args], { encoding: 'utf8', timeout: 16 * 60 * 1000, cwd: REPO });
|
|
2464
|
+
function runNode(scriptRelPath, args, options = {}) {
|
|
2465
|
+
const r = spawnSync(process.execPath, [path.join(REPO, scriptRelPath), ...args], { encoding: 'utf8', timeout: 16 * 60 * 1000, cwd: REPO, ...options });
|
|
2466
2466
|
return { ok: r.status === 0, code: r.status, log: `${r.stdout || ''}${r.stderr || ''}`.trim().slice(-4000) };
|
|
2467
2467
|
}
|
|
2468
2468
|
function elapsedMs(startedAt) {
|
|
@@ -2483,57 +2483,11 @@ function resolveProjectDir(project) {
|
|
|
2483
2483
|
/**
|
|
2484
2484
|
* Observe the learner's REAL state, for the health recommendations.
|
|
2485
2485
|
*
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
* that made the console display a dead learner (5 trajectories, last trained 6 days earlier) while
|
|
2489
|
-
* the live one held 412 — rUv documents this fragmentation as issue #2245, "four contradictory
|
|
2490
|
-
* sources". Until it is unified upstream we read the store that learning writes, never the corpse.
|
|
2486
|
+
* The queue and learner are selected together for the served project. Apply uses this same
|
|
2487
|
+
* snapshot and remeasures it, so a successful child exit alone cannot clear a learning finding.
|
|
2491
2488
|
*/
|
|
2492
2489
|
function observeLearning() {
|
|
2493
|
-
const
|
|
2494
|
-
let queueDepth = 0;
|
|
2495
|
-
try {
|
|
2496
|
-
for (const f of fs.readdirSync(queueDir)) {
|
|
2497
|
-
if (!f.endsWith('.jsonl')) continue;
|
|
2498
|
-
queueDepth += fs.readFileSync(path.join(queueDir, f), 'utf8').split('\n').filter(Boolean).length;
|
|
2499
|
-
}
|
|
2500
|
-
} catch { /* no queue dir yet — depth stays 0, which is honest */ }
|
|
2501
|
-
|
|
2502
|
-
let lastTrainSeconds = null; let trajectories = 0;
|
|
2503
|
-
try {
|
|
2504
|
-
// ISSUE #136 — THE LEARNER IS PROJECT-SCOPED, so this must ask about the SERVED project.
|
|
2505
|
-
//
|
|
2506
|
-
// `ruflo hooks intelligence --status` reports `Data Dir: <cwd>/.claude-flow/neural`. With
|
|
2507
|
-
// `cwd: SYSTEM_HOME` this measured `~/.claude-flow/neural` — a store nothing writes to on a
|
|
2508
|
-
// machine whose work happens inside project directories. Measured on one machine, one minute:
|
|
2509
|
-
// the home store held 1,216 trajectories last trained 6.9 DAYS ago while the served project held
|
|
2510
|
-
// 9,940 last trained 22 SECONDS ago. The card said "Your learner has gone quiet" about a learner
|
|
2511
|
-
// training every few seconds.
|
|
2512
|
-
//
|
|
2513
|
-
// This file already carries the verdict on this exact mistake at the refresh-child spawn: "cwd =
|
|
2514
|
-
// the SERVED project, NOT REPO … it was a real console-honesty bug". Same rule, same file,
|
|
2515
|
-
// different call site — #104's and #134's residual arriving a third time.
|
|
2516
|
-
// ISSUE #139 (@ObiWanKenobi) — `process.cwd()` was a HARDCODE in the opposite direction from
|
|
2517
|
-
// #136's `SYSTEM_HOME`. Neither asked which scope was in effect; the first was wrong by default
|
|
2518
|
-
// and the second is right only BECAUSE `project` is the default. Under
|
|
2519
|
-
// `RUVNET_LEARNING_SCOPE=user` the flush feeds ~/.claude-flow/neural while this read
|
|
2520
|
-
// <project>/.claude-flow/neural — the same false-positive card, inverted. The writer and this
|
|
2521
|
-
// reader now call ONE resolver (runtime-preferences.learnerCwd), so they agree by construction
|
|
2522
|
-
// instead of by coincidence.
|
|
2523
|
-
const r = spawnSync(path.join(SYSTEM_HOME, '.npm-global/bin/ruflo'),
|
|
2524
|
-
['hooks', 'intelligence', '--status'],
|
|
2525
|
-
{
|
|
2526
|
-
cwd: learnerCwd(),
|
|
2527
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
2528
|
-
encoding: 'utf8',
|
|
2529
|
-
timeout: 20_000,
|
|
2530
|
-
});
|
|
2531
|
-
const out = `${r.stdout || ''}`;
|
|
2532
|
-
const t = out.match(/Last Training:\s*(\d+)s ago/);
|
|
2533
|
-
const j = out.match(/Trajectories\s*\|\s*(\d+)/);
|
|
2534
|
-
if (t) lastTrainSeconds = Number(t[1]);
|
|
2535
|
-
if (j) trajectories = Number(j[1]);
|
|
2536
|
-
} catch { /* ruflo absent or slow — leave null, and null NEVER produces a recommendation */ }
|
|
2490
|
+
const learning = observeScopedLearning({ cwd: projectDirectory() });
|
|
2537
2491
|
|
|
2538
2492
|
// The fleet is what makes ADR-027's North Star recommendation constructible at all — without it,
|
|
2539
2493
|
// `learning:distill-fleet` can never be built, so it can never be offered, so clicking it would be
|
|
@@ -2545,7 +2499,7 @@ function observeLearning() {
|
|
|
2545
2499
|
// produces no recommendation, which is the correct answer when we have not looked.
|
|
2546
2500
|
const fleet = readJSON(MEMORY_CACHE)?.data?.fleet ?? [];
|
|
2547
2501
|
|
|
2548
|
-
return {
|
|
2502
|
+
return { ...learning, fleet };
|
|
2549
2503
|
}
|
|
2550
2504
|
|
|
2551
2505
|
function currentValidIds(onlyId = null) {
|
|
@@ -2649,7 +2603,23 @@ function apply(ids) {
|
|
|
2649
2603
|
const undoToken = journalUndo(undoSpec);
|
|
2650
2604
|
phaseMs.undoJournalMs += elapsedMs(journalStartedAt);
|
|
2651
2605
|
const remedyStartedAt = performance.now();
|
|
2652
|
-
const
|
|
2606
|
+
const learningBefore = ['learning:flush', 'learning:train', 'learning:flush-legacy-user'].includes(id) ? observeLearning() : null;
|
|
2607
|
+
const res = runNode(plan.exec.script, args, learningBefore ? {
|
|
2608
|
+
cwd: learningBefore.projectDir,
|
|
2609
|
+
env: { ...process.env, RUVNET_BRAIN_PROJECT_DIR: learningBefore.projectDir },
|
|
2610
|
+
} : {});
|
|
2611
|
+
if (res.ok && learningBefore) {
|
|
2612
|
+
const after = observeLearning();
|
|
2613
|
+
const progressed = id === 'learning:flush-legacy-user'
|
|
2614
|
+
? after.legacyUserKnown && after.legacyUserDepth < learningBefore.legacyUserDepth
|
|
2615
|
+
: id === 'learning:flush'
|
|
2616
|
+
? after.queueKnown && after.queueDepth < learningBefore.queueDepth
|
|
2617
|
+
: learningBefore.statusKnown && after.statusKnown && after.patterns > learningBefore.patterns;
|
|
2618
|
+
if (!progressed) {
|
|
2619
|
+
res.ok = false;
|
|
2620
|
+
res.log += `\nNo measured learning progress: queue ${after.queueDir}; learner ${after.learnerCwd}. Reload to inspect the remaining evidence.`;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2653
2623
|
phaseMs.childRemedyMs += elapsedMs(remedyStartedAt);
|
|
2654
2624
|
results.push({ id, ...res, undoToken });
|
|
2655
2625
|
}
|
|
@@ -3531,6 +3501,7 @@ export {
|
|
|
3531
3501
|
autoEligibleIds,
|
|
3532
3502
|
gatherConfig,
|
|
3533
3503
|
gatherSavings,
|
|
3504
|
+
observeLearning,
|
|
3534
3505
|
};
|
|
3535
3506
|
// Exported for the cross-project cache-isolation test (console-cache-scope.test.mjs). serveCached's
|
|
3536
3507
|
// scopeKey is the guard that stops one project's cached state being served for another.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Source-bound synthetic validation comparison; deliberately excludes database/host I/O. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import { createProgressionSnapshot, restoreProjectProgression, digestCanonical } from '../../plugin/scripts/project-progression-contract.mjs';
|
|
8
|
+
|
|
9
|
+
const flag = (name) => process.argv[process.argv.indexOf(name) + 1];
|
|
10
|
+
if (!process.argv.includes('--baseline-module') || !process.argv.includes('--output')) {
|
|
11
|
+
throw new Error('Require --baseline-module <unchanged contract module> --output <receipt.json>');
|
|
12
|
+
}
|
|
13
|
+
const baselinePath = path.resolve(flag('--baseline-module'));
|
|
14
|
+
const baseline = await import(pathToFileURL(baselinePath).href);
|
|
15
|
+
const currentPath = fileURLToPath(new URL('../../plugin/scripts/project-progression-contract.mjs', import.meta.url));
|
|
16
|
+
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
17
|
+
const sizes = (process.argv.includes('--sizes') ? flag('--sizes') : '100,400,1000').split(',').map(Number);
|
|
18
|
+
if (!sizes.every(n => Number.isSafeInteger(n) && n > 0 && n <= 10_000)) throw new Error('sizes must be positive integers <= 10000');
|
|
19
|
+
const projectIdentity = { id: 'synthetic-validation', canonicalAgentDbPath: '/synthetic/project/.swarm/memory.db' };
|
|
20
|
+
const sourceIdentity = { checkoutPath: '/synthetic/project', worktreeId: 'primary', branch: 'main', head: 'a'.repeat(40),
|
|
21
|
+
trackedDigest: 'b'.repeat(64), untrackedDigest: 'c'.repeat(64), dirtyTreeDigest: 'd'.repeat(64) };
|
|
22
|
+
const empty = Object.fromEntries(['plan', 'completed', 'inProgress', 'blockers', 'failures', 'decisions',
|
|
23
|
+
'changedFiles', 'commands', 'proofArtifacts', 'untested', 'resumeConflicts'].map(k => [k, []]));
|
|
24
|
+
const receipt = { scope: 'Synthetic CPU-only complete-history validation; no native capture/restore deadline claim',
|
|
25
|
+
node: process.version, platform: process.platform, arch: process.arch,
|
|
26
|
+
sources: { baseline: { path: baselinePath, sha256: hash(baselinePath) }, candidate: { path: currentPath, sha256: hash(currentPath) } },
|
|
27
|
+
workloads: [] };
|
|
28
|
+
for (const size of sizes) {
|
|
29
|
+
const snapshots = [], observations = [];
|
|
30
|
+
let parents = [];
|
|
31
|
+
for (let sequence = 1; sequence <= size; sequence++) {
|
|
32
|
+
observations.push({ id: `event-${sequence}`, occurredAt: '2026-10-05T00:00:00.000Z', trigger: 'PostToolUse',
|
|
33
|
+
kind: 'tool-observation', source: 'host-observation', authoritative: false, outcome: 'success', exitCode: 0,
|
|
34
|
+
intent: { action: 'verify', subjects: ['project memory'] }, tool: 'exec_command' });
|
|
35
|
+
const snapshot = createProgressionSnapshot({ projectIdentity, sourceIdentity,
|
|
36
|
+
hostIdentity: { host: 'codex', adapterVersion: 'synthetic-benchmark' }, sessionIdentity: 'synthetic',
|
|
37
|
+
sequence, parentEventKeys: parents, occurredAt: '2026-10-05T00:00:00.000Z', trigger: 'PostToolUse', dedupId: `event-${sequence}`,
|
|
38
|
+
completeProjectState: { ...empty, currentGoal: 'Preserve complete history', nextAction: 'Validate all evidence',
|
|
39
|
+
acceptanceContract: { required: ['exact equality'] }, activeProcess: 'verification', activeStep: 'PostToolUse',
|
|
40
|
+
observations: [...observations], commands: [...observations] } });
|
|
41
|
+
snapshots.push(snapshot); parents = [snapshot.eventKey];
|
|
42
|
+
}
|
|
43
|
+
const run = (restore) => {
|
|
44
|
+
const started = performance.now();
|
|
45
|
+
const value = restore(snapshots, { expectedProjectIdentity: projectIdentity });
|
|
46
|
+
return { ms: performance.now() - started, value };
|
|
47
|
+
};
|
|
48
|
+
const original = run(baseline.restoreProjectProgression);
|
|
49
|
+
const candidate = run(restoreProjectProgression);
|
|
50
|
+
if (JSON.stringify(original.value) !== JSON.stringify(candidate.value)) throw new Error('full restored result differs from baseline');
|
|
51
|
+
// Check a historical mutation whose digest was not recomputed, not just the current head.
|
|
52
|
+
const oldGoal = snapshots[0].completeProjectState.currentGoal;
|
|
53
|
+
snapshots[0].completeProjectState.currentGoal = 'tampered historical state';
|
|
54
|
+
const mutatedOriginal = baseline.restoreProjectProgression(snapshots, { expectedProjectIdentity: projectIdentity });
|
|
55
|
+
const mutatedCandidate = restoreProjectProgression(snapshots, { expectedProjectIdentity: projectIdentity });
|
|
56
|
+
if (JSON.stringify(mutatedOriginal) !== JSON.stringify(mutatedCandidate) || mutatedCandidate.ok) {
|
|
57
|
+
throw new Error('historical mutation negative semantics differ');
|
|
58
|
+
}
|
|
59
|
+
snapshots[0].completeProjectState.currentGoal = oldGoal;
|
|
60
|
+
receipt.workloads.push({ snapshots: size, bytes: snapshots.reduce((n, s) => n + Buffer.byteLength(JSON.stringify(s)), 0),
|
|
61
|
+
baselineMs: original.ms, candidateMs: candidate.ms, resultDigest: digestCanonical(candidate.value),
|
|
62
|
+
fullResultEqual: true, historicalMutationRejected: true, observations: candidate.value.state.observations.length,
|
|
63
|
+
commands: candidate.value.state.commands.length, peakRssKiB: process.resourceUsage().maxRSS });
|
|
64
|
+
fs.writeFileSync(path.resolve(flag('--output')), JSON.stringify(receipt, null, 2));
|
|
65
|
+
process.stdout.write(`${JSON.stringify(receipt.workloads.at(-1))}\n`);
|
|
66
|
+
}
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
// Reviewed stabilization release boundaries. Legacy tests remain developer diagnostics.
|
|
2
2
|
export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
3
3
|
"source": [
|
|
4
|
+
{
|
|
5
|
+
"id": "owned-startup-execution-evidence",
|
|
6
|
+
"reason": "Opt-in native execution and private stage diagnostics bind released source and preserve unknown convergence and incomplete cleanup boundaries",
|
|
7
|
+
"files": ["tests/unit/codex-host-execution-proof.test.mjs", "tests/unit/codex-host-proof-runtime.test.mjs", "tests/unit/session-start-proof.test.mjs"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "canonical-learning-capture",
|
|
11
|
+
"reason": "Fixed metadata capture, canonical scope, consent, acknowledgement and bounded owned recovery retain privacy and originals",
|
|
12
|
+
"files": [
|
|
13
|
+
"tests/unit/learn-flush-partial-failure.test.mjs",
|
|
14
|
+
"tests/unit/learn-capture-project-root.test.mjs",
|
|
15
|
+
"tests/unit/learn-capture-redaction.test.mjs",
|
|
16
|
+
"tests/unit/learner-scope-agreement.test.mjs",
|
|
17
|
+
"tests/unit/health-repair-flush-learning.test.mjs",
|
|
18
|
+
"tests/unit/learning-worker-supervisor.test.mjs"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "fresh-owned-host-proof",
|
|
23
|
+
"reason": "Source-bound native registry declarations reject warnings, trust changes and unretired owned processes",
|
|
24
|
+
"files": ["tests/unit/codex-fresh-host-proof.test.mjs"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "complete-progression-validation",
|
|
28
|
+
"reason": "Canonical digest validation retains full-history and serialization semantics",
|
|
29
|
+
"files": ["tests/unit/project-progression-contract.test.mjs"]
|
|
30
|
+
},
|
|
4
31
|
{
|
|
5
32
|
"id": "signed-artifacts",
|
|
6
33
|
"reason": "Signature verification and exact assembled coverage reject changed bytes",
|
|
@@ -46,7 +73,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
46
73
|
"tests/unit/install-activation-rollback.test.mjs",
|
|
47
74
|
"tests/unit/forge-update-apply-rollback.test.mjs",
|
|
48
75
|
"tests/unit/forge-update-archive-digest.test.mjs",
|
|
49
|
-
"tests/unit/kb-copy-proof-legacy-sidecars.test.mjs"
|
|
76
|
+
"tests/unit/kb-copy-proof-legacy-sidecars.test.mjs",
|
|
77
|
+
"tests/unit/kb-copy-proof-unknown-content.test.mjs"
|
|
50
78
|
]
|
|
51
79
|
},
|
|
52
80
|
{
|
|
@@ -183,6 +211,20 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
183
211
|
}
|
|
184
212
|
],
|
|
185
213
|
"integration": [
|
|
214
|
+
{
|
|
215
|
+
"id": "canonical-learning-recovery",
|
|
216
|
+
"reason": "Cross-session recovery and Console evidence agree on the same canonical scope without ratifying tool metadata as instructions",
|
|
217
|
+
"files": ["tests/integration/learning-recovery-377.test.mjs", "tests/integration/learning-console-scope.test.mjs"]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "canonical-progression-store",
|
|
221
|
+
"reason": "Actual adopted canonical storage, exact readback, and concurrent session restoration retain consent and complete history",
|
|
222
|
+
"files": ["tests/integration/project-progression-concurrent-sessions.test.mjs", "tests/integration/project-progression-reader-identity.test.mjs"],
|
|
223
|
+
"platformFiles": {
|
|
224
|
+
"linux": ["tests/integration/project-progression-store.test.mjs"],
|
|
225
|
+
"macos": ["tests/integration/project-progression-store.test.mjs"]
|
|
226
|
+
}
|
|
227
|
+
},
|
|
186
228
|
{
|
|
187
229
|
"id": "native-explicit-interface",
|
|
188
230
|
"reason": "Real MCP subprocess readiness, command policy and literal argv safety",
|
|
@@ -215,10 +257,14 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
215
257
|
},
|
|
216
258
|
{
|
|
217
259
|
"id": "owned-uninstall",
|
|
218
|
-
"reason": "Actual offline uninstall
|
|
260
|
+
"reason": "Actual offline uninstall and POSIX copy-cleanup callers preserve unrelated files, changed unknown bytes and private state",
|
|
219
261
|
"files": [
|
|
220
262
|
"tests/integration/uninstall-footprint.test.mjs"
|
|
221
|
-
]
|
|
263
|
+
],
|
|
264
|
+
"platformFiles": {
|
|
265
|
+
"linux": ["tests/unit/brain-footprint.test.mjs"],
|
|
266
|
+
"macos": ["tests/unit/brain-footprint.test.mjs"]
|
|
267
|
+
}
|
|
222
268
|
},
|
|
223
269
|
{
|
|
224
270
|
"id": "canonical-memory-native-boundary",
|
|
@@ -77,12 +77,19 @@ export const REMEDIES = [
|
|
|
77
77
|
// purpose, and the human string says what a user would actually do instead.
|
|
78
78
|
inverse: () => ({ kind: K.NONE, human: 'nothing to reverse — this only adds observations the learner already had queued; learned state can be reset separately' }),
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
key: 'learning-legacy-user-flush',
|
|
82
|
+
summary: 'drain retained legacy user history into its original home learner',
|
|
83
|
+
match: (id) => (id === 'learning:flush-legacy-user' ? {} : null),
|
|
84
|
+
plan: () => ({ script: 'scripts/health-repair.mjs', args: ['--flush-legacy-user-learning'] }),
|
|
85
|
+
inverse: () => ({ kind: K.NONE, human: 'original queue bytes stay retained; learned observations can be reset separately' }),
|
|
86
|
+
},
|
|
80
87
|
{
|
|
81
88
|
key: 'learning-train',
|
|
82
|
-
summary: '
|
|
89
|
+
summary: 'distill canonical observations into structural patterns',
|
|
83
90
|
match: (id) => (id === 'learning:train' ? {} : null),
|
|
84
91
|
plan: () => ({ script: 'scripts/health-repair.mjs', args: ['--train-learning'] }),
|
|
85
|
-
inverse: () => ({ kind: K.NONE, human: '
|
|
92
|
+
inverse: () => ({ kind: K.NONE, human: 'a verified snapshot is retained; automatic exact restore is unavailable and no ratified lessons are claimed' }),
|
|
86
93
|
},
|
|
87
94
|
{
|
|
88
95
|
// THE ONE THAT HAD NO EXECUTOR. See ADR-027's North Star case: stores full of memories that
|