ruvnet-brain 4.0.5 → 4.0.7
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 +3 -3
- package/bin/install.mjs +112 -7
- package/config/model-router/catalog.template.json +17 -2
- package/console/app.js +32 -14
- package/console/architecture.html +1 -1
- package/data/model-catalog.json +54 -0
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +17 -17
- package/plugin/hooks/hooks.json +34 -1
- package/plugin/scripts/codex-hook-wrapper.mjs +36 -4
- package/plugin/scripts/ground-ruvnet.sh +2 -1
- package/plugin/scripts/hook-shim.mjs +13 -6
- package/plugin/scripts/lesson-provenance.mjs +21 -0
- package/plugin/scripts/lesson-store.mjs +33 -3
- package/plugin/scripts/route-dispatch.sh +23 -49
- package/plugin/scripts/session-snapshot-contract.mjs +100 -0
- package/plugin/scripts/session-snapshot-hook.mjs +34 -0
- package/plugin/scripts/session-start-core.mjs +25 -7
- package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
- package/plugin/skills/release-proof/SKILL.md +4 -4
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
- package/scripts/capability-registry.mjs +3 -3
- package/scripts/lesson-lifecycle.mjs +2 -1
- package/scripts/lesson-ratify.mjs +6 -3
- package/scripts/lesson-seed.mjs +21 -16
- package/scripts/model-router-catalog.mjs +16 -0
- package/scripts/model-router-engine.mjs +4 -1
- package/scripts/onboarding-console.mjs +52 -14
- package/scripts/provider-availability.mjs +12 -0
- package/scripts/release-authority.mjs +14 -2
- package/scripts/release-transaction-provider.mjs +236 -0
- package/scripts/release-transaction.mjs +214 -0
- package/scripts/release.mjs +67 -279
- package/scripts/session-snapshot-contract.mjs +1 -0
- package/scripts/stabilization-receipt.mjs +1 -1
- package/scripts/staged-host-verifier.mjs +106 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const SNAPSHOT_SCHEMA = 'ruvnet-brain.session-snapshot';
|
|
5
|
+
export const SNAPSHOT_VERSION = 1;
|
|
6
|
+
export const SNAPSHOT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
const EVENTS = new Set(['PreCompact', 'PostCompact', 'SessionEnd']);
|
|
9
|
+
|
|
10
|
+
export function createSessionSnapshot({ event, capturedAt = new Date().toISOString() }) {
|
|
11
|
+
if (!EVENTS.has(event)) throw new Error(`unsupported snapshot event: ${event}`);
|
|
12
|
+
if (!Number.isFinite(Date.parse(capturedAt))) throw new Error('capturedAt must be an ISO-8601 timestamp');
|
|
13
|
+
return {
|
|
14
|
+
schema: SNAPSHOT_SCHEMA,
|
|
15
|
+
version: SNAPSHOT_VERSION,
|
|
16
|
+
capturedAt,
|
|
17
|
+
boundary: { event },
|
|
18
|
+
privacy: { rawTranscriptStored: false, credentialValuesStored: false },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateSessionSnapshot(value) {
|
|
23
|
+
return Boolean(value)
|
|
24
|
+
&& typeof value === 'object'
|
|
25
|
+
&& !Array.isArray(value)
|
|
26
|
+
&& value.schema === SNAPSHOT_SCHEMA
|
|
27
|
+
&& value.version === SNAPSHOT_VERSION
|
|
28
|
+
&& EVENTS.has(value.boundary?.event)
|
|
29
|
+
&& Number.isFinite(Date.parse(value.capturedAt))
|
|
30
|
+
&& value.privacy?.rawTranscriptStored === false
|
|
31
|
+
&& value.privacy?.credentialValuesStored === false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function freshness(capturedAt, now) {
|
|
35
|
+
const age = now - Date.parse(capturedAt);
|
|
36
|
+
return age >= 0 && age <= SNAPSHOT_MAX_AGE_MS;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function canonical(projectDir, now) {
|
|
40
|
+
const file = path.join(projectDir, '.swarm', 'agentdb-sessions.jsonl');
|
|
41
|
+
if (!fs.existsSync(file)) return { values: [], malformed: false };
|
|
42
|
+
try {
|
|
43
|
+
const stat = fs.lstatSync(file);
|
|
44
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4 * 1024 * 1024) {
|
|
45
|
+
return { values: [], malformed: true };
|
|
46
|
+
}
|
|
47
|
+
const lines = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).slice(-128);
|
|
48
|
+
const values = [];
|
|
49
|
+
let malformed = false;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
try {
|
|
52
|
+
const value = JSON.parse(line);
|
|
53
|
+
if (validateSessionSnapshot(value)) values.push({ kind: 'canonical', fresh: freshness(value.capturedAt, now), capturedAt: value.capturedAt });
|
|
54
|
+
else malformed = true;
|
|
55
|
+
} catch { malformed = true; }
|
|
56
|
+
}
|
|
57
|
+
return { values, malformed };
|
|
58
|
+
} catch { return { values: [], malformed: true }; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function legacy(projectDir, now) {
|
|
62
|
+
const values = [];
|
|
63
|
+
let malformed = false;
|
|
64
|
+
for (const root of ['.claude', '.claude-flow']) {
|
|
65
|
+
const directory = path.join(projectDir, root, 'sessions');
|
|
66
|
+
if (!fs.existsSync(directory)) continue;
|
|
67
|
+
let names;
|
|
68
|
+
try {
|
|
69
|
+
const stat = fs.lstatSync(directory);
|
|
70
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return { values: [], malformed: true };
|
|
71
|
+
names = fs.readdirSync(directory).filter((name) => /^session-.*\.json$/.test(name)).slice(-64);
|
|
72
|
+
} catch { malformed = true; continue; }
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
try {
|
|
75
|
+
const file = path.join(directory, name);
|
|
76
|
+
const stat = fs.lstatSync(file);
|
|
77
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) { malformed = true; continue; }
|
|
78
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
79
|
+
const capturedAt = value.endedAt || value.startedAt;
|
|
80
|
+
if (typeof value.id !== 'string' || !value.id || !value.context || !value.metrics || !Number.isFinite(Date.parse(capturedAt))) {
|
|
81
|
+
malformed = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
values.push({ kind: 'legacy', fresh: freshness(capturedAt, now), capturedAt });
|
|
85
|
+
} catch { malformed = true; }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { values, malformed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function inspectSessionSnapshots(projectDir, { now = Date.now() } = {}) {
|
|
92
|
+
const results = [canonical(projectDir, now), legacy(projectDir, now)];
|
|
93
|
+
const values = results.flatMap((result) => result.values);
|
|
94
|
+
const priority = { canonical: 0, legacy: 1 };
|
|
95
|
+
const fresh = values.filter((value) => value.fresh).sort((a, b) => priority[a.kind] - priority[b.kind] || Date.parse(b.capturedAt) - Date.parse(a.capturedAt))[0];
|
|
96
|
+
if (fresh) return fresh;
|
|
97
|
+
if (values.length) return values.sort((a, b) => priority[a.kind] - priority[b.kind] || Date.parse(b.capturedAt) - Date.parse(a.capturedAt))[0];
|
|
98
|
+
if (results.some((result) => result.malformed)) return { kind: 'malformed', fresh: false };
|
|
99
|
+
return { kind: 'absent', fresh: false };
|
|
100
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createSessionSnapshot } from './session-snapshot-contract.mjs';
|
|
4
|
+
|
|
5
|
+
function regularOrAbsent(file) {
|
|
6
|
+
try {
|
|
7
|
+
const stat = fs.lstatSync(file);
|
|
8
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return error?.code === 'ENOENT';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeSessionSnapshot(projectDir, event) {
|
|
15
|
+
const swarm = path.join(projectDir, '.swarm');
|
|
16
|
+
const target = path.join(swarm, 'agentdb-sessions.jsonl');
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(swarm)) {
|
|
19
|
+
const stat = fs.lstatSync(swarm);
|
|
20
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
21
|
+
} else {
|
|
22
|
+
fs.mkdirSync(swarm, { recursive: false, mode: 0o700 });
|
|
23
|
+
}
|
|
24
|
+
if (!regularOrAbsent(target)) return false;
|
|
25
|
+
fs.appendFileSync(target, `${JSON.stringify(createSessionSnapshot({ event }))}\n`, { mode: 0o600 });
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.argv[1] && path.resolve(process.argv[1]).endsWith('session-snapshot-hook.mjs')) {
|
|
33
|
+
writeSessionSnapshot(process.env.CLAUDE_PROJECT_DIR || process.cwd(), process.argv[2] || 'SessionEnd');
|
|
34
|
+
}
|
|
@@ -62,9 +62,30 @@ const dispatchDetached = (hookDir, ttl, log, command, args = [], env = process.e
|
|
|
62
62
|
String(ttl), log, command, ...args,
|
|
63
63
|
], { env, stdio: 'ignore', timeout: 2000 })?.status === 0;
|
|
64
64
|
|
|
65
|
-
const
|
|
65
|
+
export const maintainerIssueEntitlement = (env, home, repo, platform = process.platform) => {
|
|
66
|
+
const file = env.RUVNET_BRAIN_MAINTAINER_ISSUES_FILE
|
|
67
|
+
|| path.join(home, '.config', 'ruvnet-brain', 'maintainer-issues.json');
|
|
68
|
+
// Windows ACL ownership is not available through this dependency-free hot path. Fail closed
|
|
69
|
+
// instead of weakening an owner-only promise into "any local user who can write the file".
|
|
70
|
+
if (platform === 'win32') return false;
|
|
71
|
+
let stat;
|
|
72
|
+
try { stat = fs.lstatSync(file); } catch { return false; }
|
|
73
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return false;
|
|
74
|
+
// This is maintainer-only operational data. Refuse group/world-readable opt-ins on POSIX so a
|
|
75
|
+
// shared machine cannot turn a private maintainer signal into a terminal banner for other users.
|
|
76
|
+
if ((stat.mode & 0o077) !== 0) return false;
|
|
77
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) return false;
|
|
78
|
+
const entitlement = json(file);
|
|
79
|
+
return entitlement?.enabled === true
|
|
80
|
+
&& Array.isArray(entitlement.repos)
|
|
81
|
+
&& entitlement.repos.includes(repo);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const surfaceIssues = (stateDir, emit, now, env, home, platform) => {
|
|
66
85
|
const status = json(path.join(stateDir, 'open-issues.json'));
|
|
67
|
-
|
|
86
|
+
const observedAt = Date.parse(status?.at || '');
|
|
87
|
+
if (!Number.isFinite(observedAt) || observedAt > now + 5 * 60_000 || now - observedAt > 6 * 3600_000) return;
|
|
88
|
+
if (!maintainerIssueEntitlement(env, home, status.repo, platform)) return;
|
|
68
89
|
const open = Array.isArray(status.issues) ? status.issues : [];
|
|
69
90
|
if (!open.length) return;
|
|
70
91
|
const breaches = open.filter((issue) => issue.breach).sort((a, b) => b.ageHours - a.ageHours);
|
|
@@ -383,7 +404,7 @@ export async function runSessionStart({
|
|
|
383
404
|
emit(`Offer ONCE: "Want to see your whole RuvNet stack on one page?" — installed parts, learned project knowledge and reversible fixes, read-only until clicked; later it's ${consoleInvoke}. On yes invoke ${consoleInvoke}; on no, don't re-offer.`);
|
|
384
405
|
}
|
|
385
406
|
|
|
386
|
-
surfaceIssues(stateDir, emit, now);
|
|
407
|
+
surfaceIssues(stateDir, emit, now, env, home, platform);
|
|
387
408
|
surfaceSignals({ env, cwd, stateDir, hookDir, emit, now });
|
|
388
409
|
|
|
389
410
|
const routerProfile = path.join(home, '.claude', 'model-router', 'profile.json');
|
|
@@ -476,10 +497,7 @@ export async function runSessionStart({
|
|
|
476
497
|
|
|
477
498
|
const playbook = `${hookDir}${path.sep}..${path.sep}skills${path.sep}ruvnet-brain${path.sep}PLAYBOOK.md`;
|
|
478
499
|
emit('[RuvNet Brain — standing build playbook for this session (referenced by later turns as THE PLAYBOOK)]');
|
|
479
|
-
emit(`
|
|
480
|
-
emit('Every build/change request: take the wheel. FIRST, silently — read the files this touches in THEIR repo; search_ruvnet what the feature technically DOES; check project memory.');
|
|
481
|
-
emit('⛔ NO SILENT SUBSTITUTION (#1 trust-killer): never hand-roll, or aim a generic Task subagent at, work a RuvNet tool owns — QE=agentic-qe, swarms=ruflo, routing=agentic-flow, vectors=RuVector, memory=AgentDB, red/blue=@metaharness/redblue. Use the real one; if absent offer the exact install; if unusable say so out loud, every time. Never give your own code its name.');
|
|
482
|
-
emit('Beats A-D are in that file, in full. In short: A RESPOND in one voice (hear them; THE ATTACK as one lettered plan over their real files; why it holds; what you checked; "Build it now?") · B ON A YES EXECUTE END-TO-END (SPARC with a QA gate per phase, DDD, ADRs, PARALLEL Ruflo swarm work, AgentDB persistence, frontend-design + real image generation, a PROVEN result scored to >=98, ONE ask for a missing API key) · C TAKE OVER what you do well · D keep them oriented. RUN THE PROCESS.');
|
|
500
|
+
emit(`Read ${playbook} before the first build response. It requires source inspection, search_ruvnet grounding, project-memory recall, and the real owning rUv tool—never a silent hand-roll or generic substitute.`);
|
|
483
501
|
}
|
|
484
502
|
} catch (error) {
|
|
485
503
|
if (env.RUVNET_SESSION_TRACE === '1') stderr.write(`SESSION_TRACE native-fail-open ${error?.message || error}\n`);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// swarm-slot-recycler.mjs — Claude Code TeammateIdle recycling boundary.
|
|
3
|
+
//
|
|
4
|
+
// Claude owns the shared task files and their claim locks. This hook never edits them. It reads the
|
|
5
|
+
// team ledger at the synchronous TeammateIdle boundary and refuses idling only when it can prove an
|
|
6
|
+
// unassigned pending task is ready. Claude then performs the normal locked TaskUpdate claim. Missing,
|
|
7
|
+
// malformed, dependency-blocked, or ambiguous state fails open: a scheduler must not invent work.
|
|
8
|
+
//
|
|
9
|
+
// Host boundary: Claude Code exposes TeammateIdle; Codex 0.146.0 exposes SubagentStop but no
|
|
10
|
+
// TeammateIdle/TaskCompleted event or equivalent shared-task ledger. Do not register this body in the
|
|
11
|
+
// Codex manifest and do not claim Codex recycling is hook-enforced.
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { readStdinBounded } from './hook-input.mjs';
|
|
17
|
+
|
|
18
|
+
const MAX_INPUT_BYTES = 64 * 1024;
|
|
19
|
+
const MAX_TASK_BYTES = 256 * 1024;
|
|
20
|
+
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
21
|
+
|
|
22
|
+
function allowIdle() { process.exit(0); }
|
|
23
|
+
|
|
24
|
+
let raw = Buffer.alloc(0);
|
|
25
|
+
try {
|
|
26
|
+
raw = await readStdinBounded({ maxBytes: MAX_INPUT_BYTES, idleMs: 50, emptyMs: 250 });
|
|
27
|
+
} catch { allowIdle(); }
|
|
28
|
+
|
|
29
|
+
let event;
|
|
30
|
+
try {
|
|
31
|
+
event = JSON.parse(raw.toString('utf8'));
|
|
32
|
+
} catch { allowIdle(); }
|
|
33
|
+
|
|
34
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) allowIdle();
|
|
35
|
+
if (event.hook_event_name !== 'TeammateIdle') allowIdle();
|
|
36
|
+
|
|
37
|
+
const teamName = String(event.team_name || '');
|
|
38
|
+
const teammateName = String(event.teammate_name || '');
|
|
39
|
+
if (!SAFE_NAME.test(teamName) || !SAFE_NAME.test(teammateName)) allowIdle();
|
|
40
|
+
|
|
41
|
+
const tasksRoot = process.env.RUVNET_CLAUDE_TASKS_DIR
|
|
42
|
+
|| path.join(os.homedir(), '.claude', 'tasks');
|
|
43
|
+
const teamDir = path.resolve(tasksRoot, teamName);
|
|
44
|
+
const root = path.resolve(tasksRoot);
|
|
45
|
+
if (path.dirname(teamDir) !== root) allowIdle();
|
|
46
|
+
|
|
47
|
+
let files;
|
|
48
|
+
try {
|
|
49
|
+
files = fs.readdirSync(teamDir, { withFileTypes: true })
|
|
50
|
+
.filter((entry) => entry.isFile() && /^\d+\.json$/.test(entry.name))
|
|
51
|
+
.map((entry) => entry.name);
|
|
52
|
+
} catch { allowIdle(); }
|
|
53
|
+
|
|
54
|
+
const tasks = new Map();
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
try {
|
|
57
|
+
const taskPath = path.join(teamDir, file);
|
|
58
|
+
const stat = fs.statSync(taskPath);
|
|
59
|
+
if (!stat.isFile() || stat.size > MAX_TASK_BYTES) continue;
|
|
60
|
+
const task = JSON.parse(fs.readFileSync(taskPath, 'utf8'));
|
|
61
|
+
if (!task || typeof task !== 'object' || Array.isArray(task)) continue;
|
|
62
|
+
const id = String(task.id || path.basename(file, '.json'));
|
|
63
|
+
if (!/^\d+$/.test(id)) continue;
|
|
64
|
+
tasks.set(id, task);
|
|
65
|
+
} catch { /* one bad task cannot make another look ready */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const completed = new Set(
|
|
69
|
+
[...tasks].filter(([, task]) => task.status === 'completed').map(([id]) => id),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const ready = [...tasks]
|
|
73
|
+
.filter(([, task]) => task.status === 'pending')
|
|
74
|
+
.filter(([, task]) => !String(task.owner || '').trim())
|
|
75
|
+
.filter(([, task]) => {
|
|
76
|
+
if (!Array.isArray(task.blockedBy)) return false;
|
|
77
|
+
return task.blockedBy.every((id) => completed.has(String(id)));
|
|
78
|
+
})
|
|
79
|
+
.sort(([left], [right]) => Number(left) - Number(right));
|
|
80
|
+
|
|
81
|
+
if (!ready.length) allowIdle();
|
|
82
|
+
|
|
83
|
+
const [id, task] = ready[0];
|
|
84
|
+
const subject = String(task.subject || 'untitled task').replace(/[\r\n]+/g, ' ').slice(0, 160);
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
`Ready work remains. Claim task ${id} (${subject}) now with TaskUpdate owner=${teammateName} `
|
|
87
|
+
+ 'and status=in_progress, then execute it. Do not go idle while an unassigned, unblocked pending task exists.\n',
|
|
88
|
+
);
|
|
89
|
+
process.exit(2);
|
|
@@ -30,10 +30,10 @@ green.
|
|
|
30
30
|
## Candidate seal
|
|
31
31
|
|
|
32
32
|
Generate the receipt from commands in the protected candidate workflow. Do not hand-author it.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
creating its
|
|
33
|
+
Dispatch `.github/workflows/protected-release.yml` only with the full candidate SHA, the exact
|
|
34
|
+
current version, and the successful exact-SHA CI run ID whose named `release-qe` job produced
|
|
35
|
+
`release-evidence-<sha>`. The workflow derives the artifact digest from those sealed bytes and checks
|
|
36
|
+
every binding before creating its handoff and again at the Production boundary. Missing artifacts,
|
|
37
37
|
pending/red jobs, malformed inputs, version splits, and byte mismatches stop before the publisher.
|
|
38
38
|
Validate it from the repository with:
|
|
39
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# THE PLAYBOOK — the standing build playbook, in full
|
|
2
2
|
|
|
3
|
-
Updated: 2026-
|
|
3
|
+
Updated: 2026-08-02 | Version 1.1.0
|
|
4
4
|
Created: 2026-07-27
|
|
5
5
|
|
|
6
6
|
**Read this before your first build response in a session.** `plugin/scripts/session-start.sh`
|
|
@@ -87,10 +87,25 @@ rule-compliance, cite a source the tools didn't return, or claim a check that di
|
|
|
87
87
|
Completion, with a QA gate between phases.
|
|
88
88
|
- For a non-trivial domain, model it first (DDD: bounded contexts, aggregates, domain events) and
|
|
89
89
|
capture key decisions as ADRs — design before code.
|
|
90
|
-
-
|
|
90
|
+
- **Parallel-by-default state machine for multi-part work.** Before the first spawn, decompose the
|
|
91
|
+
whole request, create the complete shared task list/ledger, record dependencies and give every
|
|
92
|
+
writing task its own worktree. Ruflo coordinates roles and state; the native host executes. Fill
|
|
93
|
+
available executor slots with independent ready work immediately, without asking, up to the
|
|
94
|
+
host's configured capacity; never oversubscribe and never put more than one writer in a worktree.
|
|
95
|
+
A completion moves that task to completed, unblocks its dependents, and the freed slot claims the
|
|
96
|
+
first unassigned, unblocked pending task immediately. Only allow a slot to idle when no such task
|
|
97
|
+
exists. Keep dependent integration with the designated integration owner.
|
|
98
|
+
- **Claude Code:** its shared task ledger and `TeammateIdle` hook make recycling enforceable: the
|
|
99
|
+
shipped recycler refuses idle while a ready unassigned task exists, then Claude's locked
|
|
100
|
+
`TaskUpdate` claim performs the transition.
|
|
101
|
+
- **Codex:** Codex 0.146.0 exposes no `TeammateIdle` or `TaskCompleted` hook and no equivalent
|
|
102
|
+
shared-task hook ledger. Initial fan-out and completion-notification recycling are guidance,
|
|
103
|
+
not hook enforcement: the lead must immediately dispatch the next ready ledger item when a
|
|
104
|
+
collaboration slot completes. State this degraded boundary if it affects the run; never call it
|
|
105
|
+
enforced.
|
|
91
106
|
If Ruflo / RuVector MCP tools aren't available in this environment, DON'T block or stall — degrade
|
|
92
|
-
gracefully to
|
|
93
|
-
|
|
107
|
+
gracefully to the native host's agents and local .rvf, and briefly note the tool that would make
|
|
108
|
+
it better + how to add it. Never demand a tool the user doesn't have.
|
|
94
109
|
- Persist decisions + state to AgentDB memory so nothing is lost across sessions or compaction.
|
|
95
110
|
- If it has a UI, treat design as a BUILD STEP, not a coat of paint: apply the frontend-design
|
|
96
111
|
discipline and GENERATE the visuals (AI image generation for UI mockups / diagrams / the explainer
|
|
@@ -515,9 +515,9 @@ export const CAPABILITIES = [
|
|
|
515
515
|
// router was in fact never being consulted at all. A quiet week and a severed wire look identical
|
|
516
516
|
// from the receipt file, so read the wire directly.
|
|
517
517
|
//
|
|
518
|
-
// Two things must both be true for
|
|
519
|
-
//
|
|
520
|
-
//
|
|
518
|
+
// Two things must both be true for the host-limited dispatch audit to record anything: a
|
|
519
|
+
// PreToolUse hook on subagent dispatch and the opt-in profile it refuses to act without
|
|
520
|
+
// (route-dispatch.sh exits 0 when
|
|
521
521
|
// profile.json is absent). Either missing ⇒ the router cannot fire, regardless of how healthy
|
|
522
522
|
// the receipt ledger looks.
|
|
523
523
|
const profile = fs.existsSync(path.join(HOME, '.claude/model-router/profile.json'));
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import fs from 'node:fs';
|
|
44
44
|
import path from 'node:path';
|
|
45
45
|
import crypto from 'node:crypto';
|
|
46
|
-
import { ORIGIN, STATUS, ENFORCEMENT, loadLessons } from './lesson-store.mjs';
|
|
46
|
+
import { ORIGIN, SOURCE_CLASS, STATUS, ENFORCEMENT, loadLessons } from './lesson-store.mjs';
|
|
47
47
|
|
|
48
48
|
// ── The bars, in one place, as numbers a human can argue with ────────────────────────────────────
|
|
49
49
|
export const RETIREMENT = Object.freeze({
|
|
@@ -385,6 +385,7 @@ export function explainGeneralization(lesson, otherProjects = [], { minProjects
|
|
|
385
385
|
// `{...lesson, ...proposal}`, so the result cannot block even if the source lesson was ratified
|
|
386
386
|
// and blocking — the trust boundary is enforced by the data, not by the caller remembering.
|
|
387
387
|
origin: ORIGIN.MODEL_INFERRED,
|
|
388
|
+
sourceClass: SOURCE_CLASS.MODEL_INFERRED,
|
|
388
389
|
status: STATUS.CANDIDATE,
|
|
389
390
|
enforcement: ENFORCEMENT.CHECKLIST,
|
|
390
391
|
intendedEnforcement: null,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* correctly, stops using it.
|
|
27
27
|
*/
|
|
28
28
|
import os from 'node:os';
|
|
29
|
-
import { loadLessons, saveLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, TRIGGERS } from './lesson-store.mjs';
|
|
29
|
+
import { loadLessons, saveLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, SOURCE_CLASS, TRIGGERS } from './lesson-store.mjs';
|
|
30
30
|
|
|
31
31
|
const argv = process.argv.slice(2);
|
|
32
32
|
const arg = (f) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; };
|
|
@@ -34,7 +34,7 @@ const has = (f) => argv.includes(f);
|
|
|
34
34
|
|
|
35
35
|
const lessons = loadLessons();
|
|
36
36
|
if (!lessons.length) {
|
|
37
|
-
console.log('\n No lessons stored yet.
|
|
37
|
+
console.log('\n No personal lessons stored yet. They appear after an explicit user correction is captured.\n');
|
|
38
38
|
process.exit(0);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -86,7 +86,10 @@ if (has('--ratify')) {
|
|
|
86
86
|
// Bulk convenience, deliberately scoped: it can only touch lessons the USER stated. Model-inferred
|
|
87
87
|
// lessons are never swept up by a bulk action — that would be exactly the hole the boundary closes.
|
|
88
88
|
let next = lessons;
|
|
89
|
-
const targets = lessons.filter((l) => l.origin === ORIGIN.USER_STATED
|
|
89
|
+
const targets = lessons.filter((l) => l.origin === ORIGIN.USER_STATED
|
|
90
|
+
&& l.sourceClass === SOURCE_CLASS.CURRENT_USER
|
|
91
|
+
&& l.status === STATUS.CANDIDATE
|
|
92
|
+
&& !l.demoted);
|
|
90
93
|
for (const l of targets) next = ratify(l.id, next);
|
|
91
94
|
saveLessons(next);
|
|
92
95
|
const nowBlocking = next.filter((l) => l.enforcement === ENFORCEMENT.BLOCK).length;
|
package/scripts/lesson-seed.mjs
CHANGED
|
@@ -17,17 +17,22 @@
|
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import {
|
|
19
19
|
makeLesson, saveLessons, loadLessons, lessonsFor, unenforceable, pending, weightOf,
|
|
20
|
-
TRIGGERS as T, ENFORCEMENT as E, ORIGIN as O,
|
|
20
|
+
TRIGGERS as T, ENFORCEMENT as E, ORIGIN as O, SOURCE_CLASS,
|
|
21
21
|
} from './lesson-store.mjs';
|
|
22
22
|
|
|
23
23
|
// Shipped at CHECKLIST; `intendedEnforcement` records what it becomes once a human ratifies it.
|
|
24
24
|
const blocking = { enforcement: E.CHECKLIST, intendedEnforcement: E.BLOCK };
|
|
25
|
+
const ownerImport = {
|
|
26
|
+
origin: O.IMPORTED,
|
|
27
|
+
sourceClass: SOURCE_CLASS.IMPORTED_OWNER,
|
|
28
|
+
demoted: true,
|
|
29
|
+
};
|
|
25
30
|
|
|
26
31
|
export const SEED = [
|
|
27
32
|
makeLesson({
|
|
28
33
|
id: 'L01-verify-with-a-capable-channel',
|
|
29
34
|
...blocking,
|
|
30
|
-
|
|
35
|
+
...ownerImport,
|
|
31
36
|
severity: 'high',
|
|
32
37
|
statement: 'Before claiming something works, verify through a channel CAPABLE of observing the change — an independent tool, a re-measurement, a read-write connection. Never the exit code of the thing being tested.',
|
|
33
38
|
trigger: T.CLAIM_DONE.key,
|
|
@@ -46,7 +51,7 @@ export const SEED = [
|
|
|
46
51
|
makeLesson({
|
|
47
52
|
id: 'L02-check-before-you-assert',
|
|
48
53
|
...blocking,
|
|
49
|
-
|
|
54
|
+
...ownerImport,
|
|
50
55
|
severity: 'high',
|
|
51
56
|
statement: 'Before stating any fact about the world — a version, an API, what a tool does, how an architecture works — read a live source THIS TURN and name it. Recalling is not checking. The urge to skip the check IS the signal you are about to be wrong.',
|
|
52
57
|
trigger: T.ASSERT_FACT.key,
|
|
@@ -64,7 +69,7 @@ export const SEED = [
|
|
|
64
69
|
makeLesson({
|
|
65
70
|
id: 'L03-research-before-recommending',
|
|
66
71
|
enforcement: E.CHECKLIST,
|
|
67
|
-
|
|
72
|
+
...ownerImport,
|
|
68
73
|
statement: 'Before recommending an architecture, research it: compare at least three real options with tradeoffs, and check whether the ecosystem already ships it. Pattern-matching from training data is not a recommendation.',
|
|
69
74
|
trigger: T.RECOMMEND_ARCH.key,
|
|
70
75
|
evidence: [
|
|
@@ -78,7 +83,7 @@ export const SEED = [
|
|
|
78
83
|
makeLesson({
|
|
79
84
|
id: 'L04-never-relay-a-number',
|
|
80
85
|
enforcement: E.CHECKLIST,
|
|
81
|
-
|
|
86
|
+
...ownerImport,
|
|
82
87
|
severity: 'high',
|
|
83
88
|
statement: 'Never repeat a score, benchmark, or subagent result without re-checking the underlying artifact yourself. A number you did not measure is a claim you cannot defend.',
|
|
84
89
|
trigger: T.RELAY_NUMBER.key,
|
|
@@ -93,7 +98,7 @@ export const SEED = [
|
|
|
93
98
|
makeLesson({
|
|
94
99
|
id: 'L05-version-is-the-update-signal',
|
|
95
100
|
...blocking,
|
|
96
|
-
|
|
101
|
+
...ownerImport,
|
|
97
102
|
severity: 'high',
|
|
98
103
|
statement: 'Any behaviour-changing push bumps the version IN THE SAME COMMIT, and the release narrative is updated to match. A fix label on a new subsystem is a lie about what changed.',
|
|
99
104
|
trigger: T.SHIP.key,
|
|
@@ -109,7 +114,7 @@ export const SEED = [
|
|
|
109
114
|
makeLesson({
|
|
110
115
|
id: 'L06-use-the-real-tool',
|
|
111
116
|
...blocking,
|
|
112
|
-
|
|
117
|
+
...ownerImport,
|
|
113
118
|
severity: 'high',
|
|
114
119
|
statement: 'Before writing code in the RuvNet domain, search for the tool that already implements it. If you still disagree after genuinely looking, say so OUT LOUD, cite the source path, and name the hand-roll as a hand-roll. Never silently.',
|
|
115
120
|
trigger: T.WRITE_CODE.key,
|
|
@@ -127,7 +132,7 @@ export const SEED = [
|
|
|
127
132
|
makeLesson({
|
|
128
133
|
id: 'L07-blast-radius-not-social-comfort',
|
|
129
134
|
...blocking,
|
|
130
|
-
|
|
135
|
+
...ownerImport,
|
|
131
136
|
severity: 'high',
|
|
132
137
|
statement: 'Gate on blast radius, not on how awkward an action feels. Ask: is it reversible, and is it outward-facing? A silent irreversible change is worse than an awkward reversible one.',
|
|
133
138
|
trigger: T.MUTATE_MACHINE.key,
|
|
@@ -143,7 +148,7 @@ export const SEED = [
|
|
|
143
148
|
makeLesson({
|
|
144
149
|
id: 'L08-status-is-a-table',
|
|
145
150
|
enforcement: E.CHECKLIST,
|
|
146
|
-
|
|
151
|
+
...ownerImport,
|
|
147
152
|
statement: 'Report status as a structured table with an explicit shipped/tested column, never as narrative. Prose lets unfinished work live inside sentences about progress.',
|
|
148
153
|
trigger: T.REPORT_STATUS.key,
|
|
149
154
|
evidence: [
|
|
@@ -159,7 +164,7 @@ export const SEED = [
|
|
|
159
164
|
makeLesson({
|
|
160
165
|
id: 'L09-gradeable-is-not-valuable',
|
|
161
166
|
enforcement: E.CHECKLIST,
|
|
162
|
-
|
|
167
|
+
...ownerImport,
|
|
163
168
|
statement: 'When choosing what to work on, name the ungradeable items explicitly and commit to them. The instinct to pick the task with a green test routes systematically away from the user\'s actual value.',
|
|
164
169
|
trigger: T.CHOOSE_WORK.key,
|
|
165
170
|
evidence: [
|
|
@@ -172,7 +177,7 @@ export const SEED = [
|
|
|
172
177
|
makeLesson({
|
|
173
178
|
id: 'L10-under-enumeration-is-a-tell',
|
|
174
179
|
enforcement: E.CHECKLIST,
|
|
175
|
-
|
|
180
|
+
...ownerImport,
|
|
176
181
|
statement: 'When producing a list of failure modes, options, or requirements, state what was left out and why. A satisfyingly round number is evidence of rounding, not of completeness.',
|
|
177
182
|
trigger: T.REPORT_STATUS.key,
|
|
178
183
|
evidence: [
|
|
@@ -185,7 +190,7 @@ export const SEED = [
|
|
|
185
190
|
makeLesson({
|
|
186
191
|
id: 'L11-retrieval-without-volition-is-broken',
|
|
187
192
|
enforcement: E.REVIEW,
|
|
188
|
-
|
|
193
|
+
...ownerImport,
|
|
189
194
|
statement: 'A surface that CAN detect something useful and stays silent is broken. Judge every feature by whether it volunteers what it knows, not by whether it can answer when asked.',
|
|
190
195
|
trigger: T.CHOOSE_WORK.key,
|
|
191
196
|
evidence: [
|
|
@@ -199,7 +204,7 @@ export const SEED = [
|
|
|
199
204
|
makeLesson({
|
|
200
205
|
id: 'L12-efficiency-seeking-is-the-tell',
|
|
201
206
|
enforcement: E.REVIEW,
|
|
202
|
-
|
|
207
|
+
...ownerImport,
|
|
203
208
|
statement: 'Treat the impulse to save a step as a defect signal, not a virtue. Skipping a check to save a turn is the specific mechanism that produces wrong answers — effectiveness first, always.',
|
|
204
209
|
trigger: T.CHOOSE_WORK.key,
|
|
205
210
|
evidence: [
|
|
@@ -228,14 +233,14 @@ if (invokedDirectly) {
|
|
|
228
233
|
for (const l of ls) {
|
|
229
234
|
const shown = l.intendedEnforcement ? `${l.enforcement}→${l.intendedEnforcement}` : l.enforcement;
|
|
230
235
|
console.log(` ${shown.padEnd(18)} ${l.id}`);
|
|
231
|
-
console.log(` ${''.padEnd(18)}
|
|
236
|
+
console.log(` ${''.padEnd(18)} IMPORTED MAINTAINER HISTORY (quarantined — never personal policy) · observed ${l.repeatCount}× · weight ${weightOf(l)}`);
|
|
232
237
|
}
|
|
233
238
|
console.log('');
|
|
234
239
|
}
|
|
235
240
|
const un = unenforceable(SEED);
|
|
236
241
|
const pend = pending(SEED);
|
|
237
|
-
console.log(` ${pend.length} awaiting
|
|
238
|
-
console.log(` the
|
|
242
|
+
console.log(` ${pend.length} awaiting ratification. Bundled maintainer history is quarantined and cannot`);
|
|
243
|
+
console.log(` become the installing user's personal policy.`);
|
|
239
244
|
if (un.length) console.log(` ${un.length} are declared unenforceable (no hook can observe them); checked at review instead.`);
|
|
240
245
|
console.log('');
|
|
241
246
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Managed facts are additive for existing users. Their existing row is the user overlay: it wins
|
|
2
|
+
// byte-for-byte, including disablement, tier/priority changes, and local candidates. Newly shipped
|
|
3
|
+
// metered rows are not auto-added; that would silently expand spend authority.
|
|
4
|
+
export function mergeManagedCatalog(existing, managed) {
|
|
5
|
+
const current = Array.isArray(existing?.candidates) ? existing.candidates : [];
|
|
6
|
+
const seen = new Set(current.map((candidate) => candidate?.id).filter(Boolean));
|
|
7
|
+
const additions = (managed?.candidates || []).filter((candidate) => candidate?.id
|
|
8
|
+
&& !seen.has(candidate.id)
|
|
9
|
+
&& ((candidate.subscription || []).length > 0 || candidate.provider === 'local'));
|
|
10
|
+
const next = {
|
|
11
|
+
...existing,
|
|
12
|
+
managedVersion: managed?.managedVersion || managed?.updated || null,
|
|
13
|
+
candidates: [...current, ...additions],
|
|
14
|
+
};
|
|
15
|
+
return JSON.stringify(next) === JSON.stringify(existing) ? existing : next;
|
|
16
|
+
}
|
|
@@ -113,10 +113,13 @@ export function loadCatalog() {
|
|
|
113
113
|
} catch {
|
|
114
114
|
/* fall through to a minimal built-in so the engine still answers */
|
|
115
115
|
}
|
|
116
|
-
// Built-in fallback
|
|
116
|
+
// Built-in fallback. Claude launchability was verified against Claude Code 2.1.220 on 2026-08-02;
|
|
117
|
+
// prices remain null where the subscription host, rather than a metered API, is authoritative.
|
|
117
118
|
return [
|
|
118
119
|
{ id: 'deepseek/deepseek-chat', provider: 'openrouter', harness: ['claude-code', 'codex'], tier: 'cheap', costPerMTok: { in: 0.2, out: 0.8 }, verified: '2026-07-07' },
|
|
119
120
|
{ id: 'claude-opus-4-8', provider: 'anthropic', harness: ['claude-code'], tier: 'frontier', costPerMTok: { in: 5.0, out: 25.0 }, verified: '2026-07-07' },
|
|
121
|
+
{ id: 'claude-opus-5', provider: 'anthropic', harness: ['claude-code'], subscription: ['claude-code'], tier: 'frontier', costPerMTok: null, verified: '2026-08-02 Claude Code 2.1.220 launch' },
|
|
122
|
+
{ id: 'claude-fable-5', provider: 'anthropic', harness: ['claude-code'], subscription: ['claude-code'], tier: 'frontier', costPerMTok: null, verified: '2026-08-02 Claude Code 2.1.220 launch' },
|
|
120
123
|
{ id: 'gpt-5.5', provider: 'openai', harness: ['codex'], tier: 'frontier', costPerMTok: null, verified: null },
|
|
121
124
|
];
|
|
122
125
|
}
|