ruvnet-brain 4.6.4 → 4.7.1
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 +65 -54
- package/config/practical-rule-catalog.json +22 -22
- package/console/app.js +1 -1
- package/console/architecture.html +2 -2
- package/package.json +1 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.grok-plugin/plugin.json +12 -0
- package/plugin/commands/checkpoint.md +22 -40
- package/plugin/commands/rnb-brief.md +18 -15
- package/plugin/hooks/codex-hooks.json +1 -28
- package/plugin/hooks/grok-hooks.json +95 -0
- package/plugin/hooks/hook-contracts.json +6 -127
- package/plugin/hooks/hooks.json +1 -40
- package/plugin/host-adapters/grok.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +0 -56
- package/plugin/scripts/agentdb-recall-reader.mjs +29 -9
- package/plugin/scripts/agentdb-recall.mjs +35 -27
- package/plugin/scripts/automated-run.mjs +42 -0
- package/plugin/scripts/brain-footprint.mjs +1 -1
- package/plugin/scripts/checkpoint-keys.mjs +46 -0
- package/plugin/scripts/codex-hook-wrapper.mjs +13 -3
- package/plugin/scripts/completion-claim-evidence.mjs +1 -1
- package/plugin/scripts/continuation-gate.mjs +6 -0
- package/plugin/scripts/continuity-events.mjs +90 -2
- package/plugin/scripts/continuity-hook-policy.mjs +5 -14
- package/plugin/scripts/decision-gate.mjs +2 -2
- package/plugin/scripts/decision-record.mjs +11 -40
- package/plugin/scripts/grok-hook-adapter.mjs +155 -0
- package/plugin/scripts/hook-failures.mjs +16 -0
- package/plugin/scripts/hook-shim.mjs +1 -2
- package/plugin/scripts/learn-capture.mjs +60 -42
- package/plugin/scripts/learning-store.mjs +70 -60
- package/plugin/scripts/node-sqlite.mjs +1 -1
- package/plugin/scripts/ruflo-bin.mjs +71 -1
- package/plugin/scripts/ruflo-memory-hooks.mjs +204 -0
- package/plugin/scripts/runtime-preferences.mjs +1 -1
- package/plugin/scripts/session-snapshot-hook.mjs +85 -341
- package/plugin/scripts/session-start-core.mjs +2 -12
- package/plugin/scripts/signal-watch.mjs +1 -1
- package/plugin/scripts/substitution-wall.mjs +222 -0
- package/plugin/scripts/turn-outcome-capture.mjs +68 -74
- package/scripts/codex-host-execution-proof.mjs +1 -1
- package/scripts/console-engine.mjs +0 -32
- package/scripts/health-repair.mjs +1 -81
- package/scripts/hook-qualify-core.mjs +2 -2
- package/scripts/model-managed-workflow-service.mjs +9 -10
- package/scripts/model-native-qualification.mjs +17 -5
- package/scripts/model-routing-controller.mjs +9 -8
- package/scripts/model-routing-execution-adapters.mjs +24 -2
- package/scripts/model-weekly-cycle.mjs +13 -12
- package/scripts/no-silent-substitution.mjs +65 -1
- package/scripts/onboarding-console.mjs +8 -23
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/release-qualification-contract.mjs +21 -30
- package/scripts/remedy-registry.mjs +0 -18
- package/scripts/user-model-prompt-hook.mjs +11 -10
- package/scripts/wired-check.mjs +1 -5
- package/plugin/scripts/continuity-brief.mjs +0 -351
- package/plugin/scripts/continuity-journal.mjs +0 -553
- package/plugin/scripts/learn-flush.mjs +0 -96
- package/plugin/scripts/learning-observation.mjs +0 -51
- package/plugin/scripts/learning-queue.mjs +0 -135
- package/plugin/scripts/learning-worker-supervisor.mjs +0 -79
- package/plugin/scripts/project-capture-queue.mjs +0 -357
- package/plugin/scripts/project-memory-enrollment.mjs +0 -203
- package/plugin/scripts/project-progression-checkpoint.mjs +0 -145
- package/plugin/scripts/project-progression-contract.mjs +0 -496
- package/plugin/scripts/project-progression-hook.mjs +0 -233
- package/plugin/scripts/project-progression-outbox.mjs +0 -225
- package/plugin/scripts/project-progression-producer.mjs +0 -309
- package/plugin/scripts/project-progression-reader.mjs +0 -304
- package/plugin/scripts/project-progression-session-start.mjs +0 -320
- package/plugin/scripts/project-progression-sources.mjs +0 -246
- package/plugin/scripts/project-progression-store.mjs +0 -723
- package/plugin/scripts/project-progression-suspension.mjs +0 -60
- package/plugin/scripts/project-transition-hook.mjs +0 -267
- package/plugin/scripts/session-snapshot-contract.mjs +0 -105
- package/plugin/scripts/turn-transport-journal.mjs +0 -108
- package/scripts/qa/progression-validation-benchmark.mjs +0 -66
- package/scripts/session-snapshot-contract.mjs +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"kind": "ruvnet-brain-host-adapter",
|
|
4
|
+
"id": "grok",
|
|
5
|
+
"displayName": "Grok CLI",
|
|
6
|
+
"os": ["linux", "macos", "windows"],
|
|
7
|
+
"modes": [],
|
|
8
|
+
"manifest": "plugin/.grok-plugin/plugin.json",
|
|
9
|
+
"hooks": "plugin/hooks/grok-hooks.json",
|
|
10
|
+
"loader": "plugin/scripts/grok-hook-adapter.mjs"
|
|
11
|
+
}
|
|
@@ -7,9 +7,6 @@ import { managedGenerationIdentity } from './managed-cli-generation.mjs';
|
|
|
7
7
|
import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
|
|
8
8
|
import { projectDirectory } from '../scripts/project-identity.mjs';
|
|
9
9
|
import { recordManagedCliObservation, recordRegistryLatestObservation } from '../scripts/capability-claim-evidence.mjs';
|
|
10
|
-
import { runSessionSnapshotHook } from '../scripts/session-snapshot-hook.mjs';
|
|
11
|
-
import { resolveProjectStore } from '../scripts/project-store-resolver.mjs';
|
|
12
|
-
import { isAutomaticProgressionSuspension } from '../scripts/project-progression-suspension.mjs';
|
|
13
10
|
|
|
14
11
|
export const MANAGED_EXECUTABLES = Object.freeze([
|
|
15
12
|
'ruflo',
|
|
@@ -275,40 +272,6 @@ export function normalizeManagedExecution(execution) {
|
|
|
275
272
|
return { outcome: execution.code === 0 ? 'success' : 'failure', output, contradictoryFailure };
|
|
276
273
|
}
|
|
277
274
|
|
|
278
|
-
function managedSessionId(env) {
|
|
279
|
-
const supplied = env.RUVNET_BRAIN_SESSION_ID || env.RUVNET_HOOK_SESSION_ID;
|
|
280
|
-
return typeof supplied === 'string' && supplied.trim() ? supplied.trim() : `managed-cli-${process.pid}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Bind managed CLI execution to the existing progression writer when this project has adopted it. */
|
|
284
|
-
function managedProgressionCapture({ executable, argv, projectRoot, env, event, execution, normalized }) {
|
|
285
|
-
const adopted = fs.existsSync(path.join(projectRoot, '.swarm'));
|
|
286
|
-
if (!adopted) return { adopted: false, skipped: 'project has not adopted the canonical store' };
|
|
287
|
-
const host = String(env.RUVNET_HOOK_HOST || '').toLowerCase();
|
|
288
|
-
if (!['claude', 'codex'].includes(host)) return { adopted: true, error: 'managed host identity unavailable' };
|
|
289
|
-
let resolution;
|
|
290
|
-
try { resolution = resolveProjectStore({ projectDir: projectRoot }); }
|
|
291
|
-
catch (error) { return { adopted: true, error: `project store could not be resolved: ${error?.message || error}` }; }
|
|
292
|
-
const payload = {
|
|
293
|
-
session_id: managedSessionId(env),
|
|
294
|
-
hook_event_name: event,
|
|
295
|
-
tool_name: executable,
|
|
296
|
-
tool_input: { command: [executable, ...argv].join(' ') },
|
|
297
|
-
...(execution ? {
|
|
298
|
-
tool_response: {
|
|
299
|
-
stdout: execution.stdout,
|
|
300
|
-
stderr: execution.stderr,
|
|
301
|
-
exit_code: execution.code,
|
|
302
|
-
...(execution.signal ? { signal: execution.signal, interrupted: true } : {}),
|
|
303
|
-
outcome: normalized?.outcome,
|
|
304
|
-
...(execution.error ? { error: execution.error } : {}),
|
|
305
|
-
},
|
|
306
|
-
} : {}),
|
|
307
|
-
};
|
|
308
|
-
const result = runSessionSnapshotHook(projectRoot, event, { rawInput: JSON.stringify(payload), host, env });
|
|
309
|
-
return isAutomaticProgressionSuspension(result) ? result : { adopted: true, ...result };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
275
|
function resultOf(executable, argv, result) {
|
|
313
276
|
const output = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? '\n' : '');
|
|
314
277
|
const normalized = normalizeManagedExecution(result);
|
|
@@ -406,35 +369,16 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
|
|
|
406
369
|
const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
|
|
407
370
|
? runtimeChildEnv({ env, cwd: projectRoot })
|
|
408
371
|
: env;
|
|
409
|
-
const capture = typeof lifecycle.capture === 'function' ? lifecycle.capture : managedProgressionCapture;
|
|
410
|
-
const beforeCapture = await capture({ executable, argv, projectRoot, env: childEnv, event: 'PreToolUse' });
|
|
411
|
-
if (beforeCapture?.error || (beforeCapture?.adopted && beforeCapture.progressionCaptured !== true
|
|
412
|
-
&& !isAutomaticProgressionSuspension(beforeCapture))) {
|
|
413
|
-
const reason = beforeCapture.error || beforeCapture.skipped || 'pre-execution progression was not durably captured';
|
|
414
|
-
return { content: [{ type: 'text', text: `managed execution refused: ${reason}` }], isError: true };
|
|
415
|
-
}
|
|
416
372
|
if (lifecycle && typeof lifecycle.beforeExecute === 'function') {
|
|
417
373
|
await lifecycle.beforeExecute({ executable, argv, projectRoot, env: childEnv });
|
|
418
374
|
}
|
|
419
375
|
const execution = await execute(executable, argv, childEnv);
|
|
420
376
|
const normalized = normalizeManagedExecution(execution);
|
|
421
|
-
const afterCapture = await capture({ executable, argv, projectRoot, env: childEnv, event: 'PostToolUse', execution, normalized });
|
|
422
377
|
if (lifecycle && typeof lifecycle.afterExecute === 'function') {
|
|
423
378
|
await lifecycle.afterExecute({ executable, argv, projectRoot, env: childEnv, execution, normalized });
|
|
424
379
|
}
|
|
425
380
|
recordManagedCliObservation({ toolName, executable, argv, execution, env });
|
|
426
|
-
if (afterCapture?.adopted && afterCapture.progressionCaptured !== true
|
|
427
|
-
&& !isAutomaticProgressionSuspension(afterCapture)) {
|
|
428
|
-
return {
|
|
429
|
-
content: [{ type: 'text', text: `managed command ${normalized.outcome}; continuity result was not durably captured: ${afterCapture.skipped || 'unknown reason'}` }],
|
|
430
|
-
isError: true,
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
381
|
const result = resultOf(executable, argv, execution);
|
|
434
|
-
if (isAutomaticProgressionSuspension(beforeCapture) || isAutomaticProgressionSuspension(afterCapture)) {
|
|
435
|
-
result.content.push({ type: 'text', text: 'Automatic project progression is operator-suspended; suspended captures produced no progression receipt. Ordinary AgentDB memory and explicit checkpoints remain available.' });
|
|
436
|
-
result.structuredContent.continuity = 'operator-suspended';
|
|
437
|
-
}
|
|
438
382
|
return result;
|
|
439
383
|
}
|
|
440
384
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
/** Bounded read-only projection of canonical Ruflo rows
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/** Bounded read-only projection of canonical Ruflo rows THROUGH THE RUFLO CLI — never direct SQLite
|
|
2
|
+
* (owner ruling 2026-10-12: hooks call rUv's tools, they do not reimplement them). Run as an owned child
|
|
3
|
+
* so a stalled CLI is retired at the recall deadline. Output contract unchanged: {ok, value}.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { resolveRuflo } from './ruflo-bin.mjs';
|
|
7
|
+
|
|
8
|
+
const call = (bin, args, deadline) => new Promise((resolve) => {
|
|
9
|
+
const timeout = Math.max(1, deadline - Date.now());
|
|
10
|
+
execFile(bin, args, { timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } },
|
|
11
|
+
(error, stdout) => resolve(error ? null : String(stdout || '')));
|
|
12
|
+
});
|
|
6
13
|
|
|
7
14
|
try {
|
|
8
15
|
const [storePath, input] = process.argv.slice(2);
|
|
@@ -12,11 +19,24 @@ try {
|
|
|
12
19
|
|| requests.some(row => typeof row?.namespace !== 'string' || typeof row?.key !== 'string' || !row.key))
|
|
13
20
|
|| namespaces && (!Array.isArray(namespaces) || namespaces.length > 8 || namespaces.some(value => typeof value !== 'string'))
|
|
14
21
|
|| Boolean(requests) === Boolean(namespaces)) throw new Error('invalid bounded read');
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const bin = resolveRuflo();
|
|
23
|
+
if (!bin) throw new Error('global Ruflo unavailable');
|
|
24
|
+
let value;
|
|
25
|
+
if (requests) {
|
|
26
|
+
value = await Promise.all(requests.map(async ({ namespace, key }) => {
|
|
27
|
+
const out = await call(bin, ['memory', 'retrieve', '--key', key, '--namespace', namespace, '--value-only', '--path', storePath], deadline);
|
|
28
|
+
return out === null ? null : out.trim();
|
|
29
|
+
}));
|
|
30
|
+
} else {
|
|
31
|
+
const lists = await Promise.all(namespaces.map(async (namespace) => {
|
|
32
|
+
const out = await call(bin, ['memory', 'list', '--namespace', namespace, '--limit', '100000', '--format', 'json', '--path', storePath], deadline);
|
|
33
|
+
if (out === null) throw new Error('list failed');
|
|
34
|
+
const at = out.indexOf('[');
|
|
35
|
+
return [namespace, at < 0 ? [] : JSON.parse(out.slice(at)).map((row) => row.key).filter((key) => typeof key === 'string')];
|
|
36
|
+
}));
|
|
37
|
+
value = Object.fromEntries(lists);
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(JSON.stringify({ ok: true, value }));
|
|
20
40
|
} catch {
|
|
21
41
|
process.stdout.write(JSON.stringify({ ok: false, fatal: true }));
|
|
22
42
|
}
|
|
@@ -12,12 +12,12 @@ import path from 'node:path';
|
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { resolveRuflo } from './ruflo-bin.mjs';
|
|
15
|
-
import { rufloCwdFor, rufloScratchRoot } from './
|
|
15
|
+
import { rufloCwdFor, rufloScratchRoot } from './ruflo-bin.mjs';
|
|
16
16
|
import { resolveProjectStore } from './project-store-resolver.mjs';
|
|
17
17
|
import { isHarnessGenerated, readStdinBounded } from './hook-input.mjs';
|
|
18
18
|
import { learningContext } from './runtime-preferences.mjs';
|
|
19
19
|
import { learningTarget, LEARNING_NAMESPACE } from './learning-store.mjs';
|
|
20
|
-
import { safeAction } from './
|
|
20
|
+
import { safeAction } from './learn-capture.mjs';
|
|
21
21
|
|
|
22
22
|
import { redactText } from './continuity-events.mjs';
|
|
23
23
|
import { resolveTurnDb } from './turn-outcome-capture.mjs';
|
|
@@ -25,11 +25,16 @@ import { maskExcludedPaths } from './turn-capture-privacy.mjs';
|
|
|
25
25
|
import { searchOnce, nativeRead, exactReads, searchJsonResult } from './agentdb-recall-process.mjs';
|
|
26
26
|
import { makeLesson } from './lesson-store.mjs';
|
|
27
27
|
import { supersededKeys, tombstonedKeys } from './decision-record.mjs';
|
|
28
|
+
import { checkpointMs, checkpointNamespaces, newestCheckpoint } from './checkpoint-keys.mjs';
|
|
28
29
|
export const STORE_FILES = Object.freeze(['memory.db']);
|
|
29
30
|
export const DEFAULT_DEADLINE_MS = 1900;
|
|
30
31
|
export const BLOCK_MAX_BYTES = 600;
|
|
31
32
|
export const CONSEQUENTIAL_BLOCK_MAX_BYTES = 1800;
|
|
32
33
|
export const MIN_RELEVANCE = 0.45;
|
|
34
|
+
/** Exact-read requests one bounded reader child accepts (agentdb-recall-reader.mjs). */
|
|
35
|
+
export const MAX_EXACT_READS = 32;
|
|
36
|
+
|
|
37
|
+
export { checkpointMs, checkpointNamespaces, newestCheckpoint };
|
|
33
38
|
|
|
34
39
|
export function recallBinding({ binding = {}, projectRoot, storePath, prompt } = {}) {
|
|
35
40
|
const allowed = ['projectRoot', 'storePath', 'sessionId', 'workflowId', 'phase', 'workerId', 'requestDigest'];
|
|
@@ -140,7 +145,7 @@ export function turnOutcomeExcerpt(value, prompt) {
|
|
|
140
145
|
/** Quote a substantive exact-value passage, including the remedy in structured lessons. */
|
|
141
146
|
export function evidenceExcerpt(value, key, prompt = '') {
|
|
142
147
|
const text = redactText(value);
|
|
143
|
-
if (/^project-state-current/i.test(key)) {
|
|
148
|
+
if (/^project-state-(?:current|auto)/i.test(key)) {
|
|
144
149
|
// Checkpoints often lead with version/hash metadata. Quote one actual field,
|
|
145
150
|
// selected by task terms, instead of spending the evidence budget on identity.
|
|
146
151
|
const first = text.indexOf('{'); const last = text.lastIndexOf('}');
|
|
@@ -190,6 +195,8 @@ export function learningObservationExcerpt(raw) {
|
|
|
190
195
|
export function pickRows(results, limit = 3) {
|
|
191
196
|
const candidates = results.flatMap((r) => r.rows.filter((p) => p.namespace === r.namespace && (!NOISE_KEY.test(p.key) || p.namespace === 'turns' && /^turn[-_]/i.test(p.key) || p.namespace === 'continuity-events' && /^cevt-.*-(?:decision|lesson|open-item)-/i.test(p.key))
|
|
192
197
|
&& (!r.family || p.key.toLowerCase().includes(r.family))
|
|
198
|
+
// State comes only from the newest exact checkpoint read; a semantic hit on an older one is never shown.
|
|
199
|
+
&& (r.family || !/^project-state-/i.test(p.key))
|
|
193
200
|
&& Number.isFinite(p.score) && p.score >= MIN_RELEVANCE).map((p) => ({ ...p, storePath: r.storePath, targeted: Boolean(r.family) })));
|
|
194
201
|
const signal = (p) => SIGNAL_NAMESPACES.has(p.namespace) ? 2 : p.namespace === 'turns' ? 1 : p.namespace === LEARNING_NAMESPACE ? -1 : 0;
|
|
195
202
|
const ranked = candidates.sort((a, b) => Number(b.targeted) - Number(a.targeted) || signal(b) - signal(a) || b.score - a.score);
|
|
@@ -227,13 +234,14 @@ export function searchDeadlineFor(started, deadline) { return Math.min(deadline,
|
|
|
227
234
|
async function consequentialRecall({ prompt, root, store, bin, scratch, env, deadline, binding, signal, enumerateKeys, exclusions, rankedSearch = true }) {
|
|
228
235
|
const searchDeadline = searchDeadlineFor(Date.now(), deadline);
|
|
229
236
|
const phaseBinding = recallBinding({ binding, projectRoot: root, storePath: store.path, prompt });
|
|
230
|
-
const
|
|
237
|
+
const stateNamespaces = checkpointNamespaces(root);
|
|
238
|
+
const namespaces = [...new Set([...stateNamespaces, 'lessons', 'continuity-events'])];
|
|
231
239
|
const enumeration = await enumerateKeys({ storePath: store.path, namespaces, deadline, signal, env, scratch });
|
|
232
240
|
if (!enumeration?.ok || signal?.aborted || Date.now() >= deadline) return { block: formatBlock({ picks: [], status: 'unavailable structural enumeration' }), picks: [], stores: [store],
|
|
233
241
|
status: { 'memory.db': 'unavailable' }, outcome: signal?.aborted ? 'unavailable' : Date.now() >= deadline ? 'timed-out' : 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
|
|
234
|
-
const keys = enumeration.value;
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
const keys = Object.fromEntries(namespaces.map(namespace => [namespace, Array.isArray(enumeration.value?.[namespace]) ? enumeration.value[namespace] : []]));
|
|
243
|
+
// Newest by the epoch-ms in the key, across every namespace spelling: never insertion order, score or one namespace.
|
|
244
|
+
const latestState = newestCheckpoint(keys, stateNamespaces);
|
|
237
245
|
const decisionNamespaces = namespaces.filter(namespace => namespace !== 'lessons');
|
|
238
246
|
const decisionKeys = decisionNamespaces.flatMap(namespace => keys[namespace].filter(key => /^decision[-_]|^cevt-.*-decision-/i.test(key)).map(key => ({ category: 'decisions', namespace, key })));
|
|
239
247
|
let relevantDecisions = decisionKeys;
|
|
@@ -255,13 +263,17 @@ async function consequentialRecall({ prompt, root, store, bin, scratch, env, dea
|
|
|
255
263
|
}
|
|
256
264
|
if (!relevantDecisions.length) decisionAvailability = 'unavailable'; // semantic miss is not absent history
|
|
257
265
|
}
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
const
|
|
266
|
+
const stateCandidates = latestState ? [{ category: 'state', namespace: latestState.namespace, key: latestState.key }] : [];
|
|
267
|
+
let lessonCandidates = keys.lessons.filter(key => /^lesson[-_]/i.test(key)).map(key => ({ category: 'lessons', namespace: 'lessons', key }));
|
|
268
|
+
// The exact-read budget is bounded. Too many lessons must never cost the state record: lessons alone become
|
|
269
|
+
// unavailable (an honest marker), the newest checkpoint and the decisions are still read.
|
|
270
|
+
let lessonAvailability = 'ok-empty';
|
|
271
|
+
if (stateCandidates.length + lessonCandidates.length + relevantDecisions.length > MAX_EXACT_READS) { lessonCandidates = []; lessonAvailability = 'unavailable'; }
|
|
272
|
+
const candidates = [...stateCandidates, ...lessonCandidates, ...relevantDecisions];
|
|
273
|
+
if (candidates.length > MAX_EXACT_READS) return { block: formatBlock({ picks: [], status: 'unavailable bounded category selection' }), picks: [], stores: [store], status: { 'memory.db': 'unavailable' }, outcome: 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
|
|
274
|
+
// Checkpoint keys that exist but none of which carries an orderable epoch-ms: history we cannot date, not absence.
|
|
275
|
+
const unorderedState = !latestState && stateNamespaces.some(namespace => keys[namespace].some(key => /^project-state-(?:current|auto)/.test(key)));
|
|
276
|
+
const categories = { state: unorderedState ? 'unavailable' : 'ok-empty', decisions: decisionAvailability, lessons: lessonAvailability };
|
|
265
277
|
const records = [];
|
|
266
278
|
candidates.forEach(candidate => { candidate.storePath = store.path; });
|
|
267
279
|
const exactResults = await exactReads({ candidates, bin, deadline, env, scratch, signal });
|
|
@@ -344,12 +356,9 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
344
356
|
const query = recallQuery(prompt);
|
|
345
357
|
const scratchFor = scratch || ((storePath) => rufloCwdFor(storePath, { root: rufloScratchRoot(env) }));
|
|
346
358
|
if (consequential) return await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal, enumerateKeys, exclusions });
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
: /where are we|status|catch me up/i.test(prompt) ? 'project-state-current'
|
|
351
|
-
: /releas|publish|workflow run|dispatch/i.test(prompt) ? 'release'
|
|
352
|
-
: /requirement|always|every prompt/i.test(prompt) ? 'decision-agentdb' : null;
|
|
359
|
+
// Only namespaces something below reads: curated lessons/decisions, the checkpoint namespaces (dirname, its
|
|
360
|
+
// lowercase form, default) and turns for the single ranked search. Each one is a `ruflo memory list` process.
|
|
361
|
+
const namespaces = [...new Set(['lessons', 'turns', 'continuity-events', ...checkpointNamespaces(root)])];
|
|
353
362
|
// This machine runs loaded (load average 30-60 measured 2026-10-10) and ranked search spawns one Ruflo process
|
|
354
363
|
// per namespace; run concurrently they starve every other read past the deadline. So: ONE native enumeration
|
|
355
364
|
// (reused for the curated pass and for pruning empty namespaces), then exact curated state/lessons/decisions,
|
|
@@ -362,13 +371,12 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
362
371
|
enumerateKeys: sharedKeys, exclusions, rankedSearch: false }).catch(() => null) : null;
|
|
363
372
|
const searchDeadline = searchDeadlineFor(Date.now(), deadline);
|
|
364
373
|
const activeNamespaces = presence?.ok ? namespaces.filter(namespace => presence.value[namespace]?.length) : namespaces;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
args: ['--format', 'json', '-q', workflowQuery, '-n', LEARNING_NAMESPACE, '-t', 'keyword', '--limit', '4'] });
|
|
374
|
+
// ONE supplementary search (owner ruling 2026-10-12). Measured on a live-size store at load 17: ~10
|
|
375
|
+
// concurrent `ruflo memory search` processes took 1.34-1.45 s each against a ~1.1 s window, so every
|
|
376
|
+
// ranked search timed out. Curated state/lessons/decisions already came from exact reads above.
|
|
377
|
+
const searchNamespace = [path.basename(root), 'default', 'turns'].find((namespace) => activeNamespaces.includes(namespace));
|
|
378
|
+
const jobs = store && searchNamespace ? [{ store, namespace: searchNamespace, family: null,
|
|
379
|
+
args: ['--format', 'json', '-q', query, '-n', searchNamespace, '--limit', '12'] }] : [];
|
|
372
380
|
const results = await Promise.all(jobs.map(async ({ store: jobStore, namespace, family: recordFamily, args }) => ({ namespace, family: recordFamily, storePath: jobStore.path,
|
|
373
381
|
...await searchOnce({ ruflo: bin, store: jobStore, deadline: searchDeadline, env, scratch: scratchFor, args, signal }) })));
|
|
374
382
|
let status = results.every((r) => r.state === 'ok') ? 'ok'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* automated-run.mjs — is this Stop hook running inside an automated, machine-read run?
|
|
3
|
+
*
|
|
4
|
+
* Automated runs keep their exact output (4.7.1, check 2f). Measured: `claude -p` returned a
|
|
5
|
+
* "Correction to my previous reply ... Verified:" paragraph and `codex exec` returned "UNVERIFIED: ..."
|
|
6
|
+
* because the continuation gate asked for a correction, which corrupted callers that parse the answer.
|
|
7
|
+
*
|
|
8
|
+
* The approach is copied from the owner's own Stop gate, ~/.claude/hooks/owner-review-gate.mjs:
|
|
9
|
+
* line 19 — `CLAUDE_CODE_ENTRYPOINT` starting with `sdk-` (scripted `claude -p` / Agent SDK runs);
|
|
10
|
+
* lines 20-24 — `/bin/ps -o args= -p <ppid>` and the regex `(^|\/)codex(\s+\S+)*\s+exec(\s|$)`;
|
|
11
|
+
* an unknown parent keeps gating.
|
|
12
|
+
* One difference, and why: that gate is registered directly, so Codex is its parent. This gate runs at
|
|
13
|
+
* the end of a chain (node -e wrapper -> codex-hook.mjs -> codex-hook-adapter.mjs -> hook-shim.mjs ->
|
|
14
|
+
* this), so the Codex check walks a bounded number of ancestors, and only when the hook host is Codex.
|
|
15
|
+
* Interactive Claude (`cli`, desktop, VS Code entrypoints) and interactive Codex stay gated.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const CODEX_EXEC = /(^|\/)codex(\s+\S+)*\s+exec(\s|$)/;
|
|
20
|
+
const MAX_ANCESTORS = 12;
|
|
21
|
+
|
|
22
|
+
function psRow(pid) {
|
|
23
|
+
const out = execFileSync('/bin/ps', ['-o', 'ppid=,command=', '-p', String(pid)], { encoding: 'utf8', timeout: 1000 });
|
|
24
|
+
const m = /^\s*(\d+)\s+(.*)$/s.exec(out.trim());
|
|
25
|
+
return m ? { ppid: Number(m[1]), args: m[2].trim() } : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True when the hook belongs to an automated run. `ps` is injectable for tests. */
|
|
29
|
+
export function isAutomatedRun({ env = process.env, ppid = process.ppid, ps = psRow } = {}) {
|
|
30
|
+
if (/^sdk-/.test(env.CLAUDE_CODE_ENTRYPOINT || '')) return true;
|
|
31
|
+
if (env.RUVNET_HOOK_HOST !== 'codex') return false;
|
|
32
|
+
try {
|
|
33
|
+
let pid = ppid;
|
|
34
|
+
for (let i = 0; i < MAX_ANCESTORS && pid > 1; i++) {
|
|
35
|
+
const row = ps(pid);
|
|
36
|
+
if (!row) return false;
|
|
37
|
+
if (CODEX_EXEC.test(row.args)) return true;
|
|
38
|
+
pid = row.ppid;
|
|
39
|
+
}
|
|
40
|
+
} catch { /* unknown parent: keep gating */ }
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
@@ -41,7 +41,7 @@ export const FOOTPRINT_POLICY = Object.freeze({
|
|
|
41
41
|
staleStageMs: 2 * 3_600_000, // an install stage nobody activated in 2h is debris
|
|
42
42
|
staleForgeCandidateMs: 24 * 3_600_000,
|
|
43
43
|
staleLeaseMs: 6 * 3_600_000, // plugin/scripts/update-apply.mjs LEASE_FRESH_MS
|
|
44
|
-
staleRufloRunMs: 3_600_000, // plugin/scripts/
|
|
44
|
+
staleRufloRunMs: 3_600_000, // plugin/scripts/ruflo-bin.mjs STALE_RUN_MS
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
/** Every full-KB copy name this product (or a recovery) has ever created beside the live KB. A superset of
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which project-state checkpoint is the newest — decided by the epoch-ms in the KEY, never by `ruflo memory list`
|
|
3
|
+
* order, createdAt/updatedAt, semantic score or a list limit. Shared by the per-prompt recall
|
|
4
|
+
* (agentdb-recall.mjs) and SessionStart restore (ruflo-memory-hooks.mjs). Pure: no I/O.
|
|
5
|
+
*/
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Namespaces that hold a project's checkpoints. `ruflo memory list -n` is case-sensitive, and owners write
|
|
10
|
+
* both the dirname and its lowercase form (measured 2026-10-13: AppealArmor's newest checkpoints are all in
|
|
11
|
+
* `appealarmor`), plus `default` when no -n was given.
|
|
12
|
+
*/
|
|
13
|
+
export function checkpointNamespaces(root) {
|
|
14
|
+
const base = path.basename(root);
|
|
15
|
+
return [...new Set([base, base.toLowerCase(), 'default'])];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CHECKPOINT_KEY = /^project-state-(current|auto)-(\d{13})(?:-[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
|
|
19
|
+
const MIN_CHECKPOINT_MS = Date.UTC(2020, 0, 1);
|
|
20
|
+
const FUTURE_SKEW_MS = 24 * 3600 * 1000;
|
|
21
|
+
|
|
22
|
+
/** The epoch-ms a checkpoint key carries ({family, ms}), or null for a key that cannot be ordered by time. */
|
|
23
|
+
export function checkpointMs(key, now = Date.now()) {
|
|
24
|
+
const match = CHECKPOINT_KEY.exec(String(key));
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
const ms = Number(match[2]);
|
|
27
|
+
return ms >= MIN_CHECKPOINT_MS && ms <= now + FUTURE_SKEW_MS ? { family: match[1], ms } : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Newest key of one family ('current' | 'auto') across {namespace: keys[]}; ties go to the earlier namespace. */
|
|
31
|
+
export function newestOfFamily(keysByNamespace, namespaces, family, now = Date.now()) {
|
|
32
|
+
let best = null;
|
|
33
|
+
namespaces.forEach((namespace, rank) => {
|
|
34
|
+
for (const key of keysByNamespace?.[namespace] ?? []) {
|
|
35
|
+
const parsed = checkpointMs(key, now);
|
|
36
|
+
if (!parsed || parsed.family !== family) continue;
|
|
37
|
+
if (!best || parsed.ms > best.ms || parsed.ms === best.ms && rank === best.rank && key > best.key) best = { namespace, key, ms: parsed.ms, rank };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return best && { namespace: best.namespace, key: best.key, ms: best.ms };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The newest curated project-state-current-*; an automatic project-state-auto-* only when no curated one exists. */
|
|
44
|
+
export function newestCheckpoint(keysByNamespace, namespaces, now = Date.now()) {
|
|
45
|
+
return newestOfFamily(keysByNamespace, namespaces, 'current', now) ?? newestOfFamily(keysByNamespace, namespaces, 'auto', now);
|
|
46
|
+
}
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
6
7
|
|
|
7
8
|
// New installs colocate the helper. Older standalone fixtures/installations can omit it;
|
|
8
9
|
// they retain their prior behavior until the installer upgrades both files.
|
|
@@ -44,8 +45,8 @@ const blockingHooks = new Set([
|
|
|
44
45
|
*
|
|
45
46
|
* warning: clamping SessionEnd hook timeout to 3s in .../hooks.json
|
|
46
47
|
*
|
|
47
|
-
* SessionEnd is HARD-CAPPED at 3 seconds and no registration can ask for more.
|
|
48
|
-
* 18s: measured with a real 10-entry queue and the real `ruflo`, 18342 / 18347 / 18384 ms, because
|
|
48
|
+
* SessionEnd is HARD-CAPPED at 3 seconds and no registration can ask for more. The former learning
|
|
49
|
+
* flusher needed 18s: measured with a real 10-entry queue and the real `ruflo`, 18342 / 18347 / 18384 ms, because
|
|
49
50
|
* one `ruflo hooks` call alone costs seconds and it self-bounds at LEARN_FLUSH_DEADLINE_MS.
|
|
50
51
|
*
|
|
51
52
|
* So the arithmetic never closes, and what shipped was the worst version of that: this wrapper
|
|
@@ -57,7 +58,8 @@ const blockingHooks = new Set([
|
|
|
57
58
|
* exactly — the hook returns immediately — while the flush runs to its own deadline and does its
|
|
58
59
|
* own write-back. Raising the number instead would have been a fiction the host silently clamps.
|
|
59
60
|
*/
|
|
60
|
-
|
|
61
|
+
// Empty since the learning flusher was deleted (2026-10-12): learning writes directly through ruflo.
|
|
62
|
+
const DETACHED_HOOKS = new Set([]);
|
|
61
63
|
|
|
62
64
|
/** THE BUDGET IS DERIVED FROM WHAT THE HOOK MEASURABLY COSTS, never from what looks tidy. */
|
|
63
65
|
function timeoutFor(hookId, event = '', sessionBudgetMs = 0) {
|
|
@@ -226,6 +228,14 @@ const result = spawnSync(process.execPath, [adapter, ...process.argv.slice(2)],
|
|
|
226
228
|
timeout: budgetMs,
|
|
227
229
|
killSignal: 'SIGKILL',
|
|
228
230
|
});
|
|
231
|
+
// Review N5: Codex has no shim watchdog in front of the body, so a kill here was silent. One ledger row,
|
|
232
|
+
// read by the next SessionStart (same writer and file as the shim's own rows). Never throws.
|
|
233
|
+
if (result.error?.code === 'ETIMEDOUT' || result.signal === 'SIGKILL') {
|
|
234
|
+
try {
|
|
235
|
+
const { recordHookFailure } = await import(pathToFileURL(path.join(root, 'scripts', 'hook-failures.mjs')).href);
|
|
236
|
+
recordHookFailure({ hook: hookId, event: process.argv[3] || '', error: 'killed-by-codex-wrapper', limitMs: budgetMs }, { brainHome });
|
|
237
|
+
} catch { /* an unrecordable failure must not become a second one */ }
|
|
238
|
+
}
|
|
229
239
|
if (hookId === 'session-start' && result.status !== 0) {
|
|
230
240
|
process.stderr.write('[RuvNet Brain — PROJECT CONTINUITY UNKNOWN] SessionStart did not complete inside its host deadline; no restoration success is claimed.\n');
|
|
231
241
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* HOSTS. Claude Code transcripts (`transcript_path`, JSONL) are parsed. Codex's Stop payload also
|
|
20
20
|
* carries `transcript_path`, but its rollout format is NOT parsed anywhere in this repository
|
|
21
|
-
* (
|
|
21
|
+
* (turn-outcome-capture.mjs declines it), so on Codex the
|
|
22
22
|
* transcript half is UNKNOWN and only the answer-side half (2)+(3) is enforced — disclosed in the
|
|
23
23
|
* correction text, never silently treated as proven.
|
|
24
24
|
*/
|
|
@@ -42,6 +42,7 @@ import path from 'node:path';
|
|
|
42
42
|
import os from 'node:os';
|
|
43
43
|
import crypto from 'node:crypto';
|
|
44
44
|
import { readStopHookInput } from './hook-input.mjs';
|
|
45
|
+
import { isAutomatedRun } from './automated-run.mjs';
|
|
45
46
|
import {
|
|
46
47
|
auditCapabilityClaims,
|
|
47
48
|
buildCapabilityInventoryReceipt,
|
|
@@ -336,6 +337,11 @@ if (has('--clear')) { save({ items: [] }); console.log('ledger cleared'); proces
|
|
|
336
337
|
*/
|
|
337
338
|
const hookInput = await readStopHookInput(); // shared with grounding-turn-gate: hook-input.mjs (ADR-043)
|
|
338
339
|
|
|
340
|
+
// AUTOMATED RUNS KEEP THEIR EXACT OUTPUT (4.7.1): scripted `claude -p` / SDK runs and `codex exec` are
|
|
341
|
+
// read by machines, and a correction request rewrote their answers. Same rule as the owner's
|
|
342
|
+
// owner-review-gate.mjs lines 15-24; see automated-run.mjs. Interactive sessions are still gated.
|
|
343
|
+
if (isAutomatedRun()) process.exit(EXIT_ALLOW);
|
|
344
|
+
|
|
339
345
|
// LOOP-SAFETY 1 (ADR-043 / Fable #1) — only an affirmatively-parsed hook payload may force. A 'tty' or
|
|
340
346
|
// 'unreadable' source cannot be confirmed a fresh stop, so it never forces.
|
|
341
347
|
if (hookInput.__source !== 'stdin') process.exit(EXIT_ALLOW);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* lesson an owner correction / standing rule, or explicit explicit only is authoritative
|
|
19
19
|
* open-item explicit only (the work ledger is read live, not copied) authoritative
|
|
20
20
|
*
|
|
21
|
-
* Each event is redacted (redactProgression), bounded (SUMMARY_LIMIT), and carries a content-derived
|
|
21
|
+
* Each event is redacted (redactProgression, below), bounded (SUMMARY_LIMIT), and carries a content-derived
|
|
22
22
|
* `id` so the same commit, decision or lesson is recorded once however many boundaries see it.
|
|
23
23
|
*
|
|
24
24
|
* PRIVACY. Turn capture never stores user text (a 2026-07-13 measurement: prompt echoes were 87% of a
|
|
@@ -31,7 +31,95 @@
|
|
|
31
31
|
import { nativeTurnLines } from './native-turn-transcript.mjs';
|
|
32
32
|
import crypto from 'node:crypto';
|
|
33
33
|
import { execFileSync } from 'node:child_process';
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
// Secret redaction for event payloads: pure functions, no storage.
|
|
36
|
+
function canonicalize(value) {
|
|
37
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
38
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
39
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
40
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
41
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
42
|
+
}
|
|
43
|
+
throw new TypeError('progression values must be JSON-compatible');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function secretKind(key) {
|
|
47
|
+
const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
48
|
+
if (normalized.includes('privatekey')) return 'private-key';
|
|
49
|
+
if (normalized.includes('apikey')) return 'api-key';
|
|
50
|
+
if (normalized.includes('authorization')) return 'authorization';
|
|
51
|
+
if (normalized.includes('password') || normalized.includes('passwd')) return 'password';
|
|
52
|
+
if (normalized.includes('token')) return 'token';
|
|
53
|
+
if (normalized.includes('secret')) return 'secret';
|
|
54
|
+
if (normalized.includes('credential')) return 'credential';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const INLINE_SECRETS = Object.freeze([
|
|
59
|
+
{
|
|
60
|
+
kind: 'private-key',
|
|
61
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
62
|
+
replace: '[REDACTED:private-key]',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
kind: 'bearer-token',
|
|
66
|
+
pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
67
|
+
replace: 'Bearer [REDACTED:bearer-token]',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: 'api-key',
|
|
71
|
+
pattern: /\b(?:sk|api-key|ghp|github_pat)[-_][A-Za-z0-9._-]{8,}/gi,
|
|
72
|
+
replace: '[REDACTED:api-key]',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
kind: 'password',
|
|
76
|
+
pattern: /\b(password|passwd)\s*[:=]\s*[^\s;,]+/gi,
|
|
77
|
+
replace: '$1=[REDACTED:password]',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: 'token',
|
|
81
|
+
pattern: /\b(token|secret|credential)\s*[:=]\s*[^\s;,]+/gi,
|
|
82
|
+
replace: '$1=[REDACTED:token]',
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
export function redactProgression(value) {
|
|
87
|
+
const redactions = [];
|
|
88
|
+
const mark = (path, kind) => {
|
|
89
|
+
if (!redactions.some((row) => row.path === path && row.kind === kind)) redactions.push({ path, kind });
|
|
90
|
+
};
|
|
91
|
+
const walk = (current, currentPath) => {
|
|
92
|
+
if (typeof current === 'string') {
|
|
93
|
+
let output = current;
|
|
94
|
+
for (const rule of INLINE_SECRETS) {
|
|
95
|
+
rule.pattern.lastIndex = 0;
|
|
96
|
+
const replaced = output.replace(rule.pattern, rule.replace);
|
|
97
|
+
if (replaced !== output) {
|
|
98
|
+
mark(currentPath, rule.kind);
|
|
99
|
+
output = replaced;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return output;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(current)) return current.map((item, index) => walk(item, `${currentPath}[${index}]`));
|
|
105
|
+
if (current && typeof current === 'object' && Object.getPrototypeOf(current) === Object.prototype) {
|
|
106
|
+
return Object.fromEntries(Object.keys(current).sort().map((key) => {
|
|
107
|
+
const path = `${currentPath}.${key}`;
|
|
108
|
+
const kind = secretKind(key);
|
|
109
|
+
if (kind && current[key] !== null && current[key] !== undefined) {
|
|
110
|
+
if (current[key] !== `[REDACTED:${kind}]`) mark(path, kind);
|
|
111
|
+
return [key, `[REDACTED:${kind}]`];
|
|
112
|
+
}
|
|
113
|
+
return [key, walk(current[key], path)];
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
return canonicalize(current);
|
|
117
|
+
};
|
|
118
|
+
const redacted = walk(value, '$');
|
|
119
|
+
redactions.sort((left, right) => left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind));
|
|
120
|
+
return { value: redacted, redactions };
|
|
121
|
+
}
|
|
122
|
+
|
|
35
123
|
|
|
36
124
|
export const CONTINUITY_NAMESPACE = 'continuity-events';
|
|
37
125
|
export const EVENT_SCHEMA = 'ruvnet-brain.continuity-event';
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* session-start continuity recovery at SessionStart claude, codex
|
|
21
21
|
* unprompted-speech advisory delivery at UserPromptSubmit claude, codex
|
|
22
22
|
* continuation-gate continuation nudge at turn end claude, codex
|
|
23
|
-
* session-snapshot
|
|
24
|
-
* session-snapshot
|
|
25
|
-
* session-snapshot
|
|
23
|
+
* session-snapshot turn outcome (ruflo) at Stop claude, codex
|
|
24
|
+
* session-snapshot checkpoint (ruflo) at PreCompact claude, codex
|
|
25
|
+
* session-snapshot checkpoint (ruflo) at SessionEnd claude, codex
|
|
26
|
+
* session-snapshot learning (opt-in) at PostToolUse claude, codex
|
|
26
27
|
* ground-ruvnet grounding injection at UserPromptSubmit claude, codex
|
|
27
28
|
* capacity-aware-parallel-work coordination guidance at UserPromptSubmit claude, codex
|
|
28
29
|
* decision-gate write authorization at PreToolUse (write) claude, codex
|
|
@@ -122,11 +123,9 @@ const registration = (id, matcher, hosts) => Object.freeze({ id, matcher, hosts:
|
|
|
122
123
|
*/
|
|
123
124
|
export const CONTINUITY_EVENTS = Object.freeze({
|
|
124
125
|
SessionStart: Object.freeze([
|
|
125
|
-
// learn-flush is no longer its own registration (ADR-106): session-start schedules it detached.
|
|
126
126
|
registration('session-start', 'startup|resume|clear|compact|fork', ['claude', 'codex']),
|
|
127
127
|
]),
|
|
128
128
|
UserPromptSubmit: Object.freeze([
|
|
129
|
-
registration('session-snapshot', '*', ['claude', 'codex']),
|
|
130
129
|
registration('unprompted-speech', '*', ['claude', 'codex']),
|
|
131
130
|
registration('ground-ruvnet', '*', ['claude', 'codex']),
|
|
132
131
|
// Context-only recommendation for clearly substantial, independently splittable work. The
|
|
@@ -144,7 +143,6 @@ export const CONTINUITY_EVENTS = Object.freeze({
|
|
|
144
143
|
// Claude's `Edit` shape for decision-gate.mjs's own policies (protect-state, hijack-ruvnet,
|
|
145
144
|
// ground-before-write, adr-currency all then see tool_name:"Edit" exactly as on Claude).
|
|
146
145
|
PreToolUse: Object.freeze([
|
|
147
|
-
registration('session-snapshot', '*', ['claude', 'codex']),
|
|
148
146
|
registration('decision-gate', '^(Write|Edit|MultiEdit|NotebookEdit|apply_patch)$', ['claude', 'codex']),
|
|
149
147
|
]),
|
|
150
148
|
// Extended to Codex 2026-09-12: a real MCP `search_ruvnet` call was measured to fire PostToolUse
|
|
@@ -154,16 +152,10 @@ export const CONTINUITY_EVENTS = Object.freeze({
|
|
|
154
152
|
// no change either (proven live: the real MCP tool_response's `content[0].text` carries the exact
|
|
155
153
|
// "Searched N RuvNet repos" banner and query text the script already looks for).
|
|
156
154
|
PostToolUse: Object.freeze([
|
|
157
|
-
//
|
|
155
|
+
// Learning observation (consent-gated `ruflo memory store`) runs inside session-snapshot's PostToolUse body.
|
|
158
156
|
registration('session-snapshot', '*', ['claude', 'codex']),
|
|
159
157
|
registration('grounding-stamp', '^(?:.*__)?search_ruvnet$', ['claude', 'codex']),
|
|
160
158
|
]),
|
|
161
|
-
PostToolUseFailure: Object.freeze([
|
|
162
|
-
registration('session-snapshot', '*', ['claude']),
|
|
163
|
-
]),
|
|
164
|
-
SubagentStop: Object.freeze([
|
|
165
|
-
registration('session-snapshot', '*', ['claude', 'codex']),
|
|
166
|
-
]),
|
|
167
159
|
Stop: Object.freeze([
|
|
168
160
|
registration('continuation-gate', '*', ['claude', 'codex']),
|
|
169
161
|
// Codex added 2026-09-29 (owner requirement: every turn's outcome recorded on BOTH hosts —
|
|
@@ -186,7 +178,6 @@ export const CONTINUITY_EVENTS = Object.freeze({
|
|
|
186
178
|
registration('session-snapshot', '*', ['claude', 'codex']),
|
|
187
179
|
]),
|
|
188
180
|
SessionEnd: Object.freeze([
|
|
189
|
-
// learn-flush is scheduled by session-snapshot's SessionEnd boundary (ADR-106).
|
|
190
181
|
registration('session-snapshot', '*', ['claude', 'codex']),
|
|
191
182
|
]),
|
|
192
183
|
});
|