ruvnet-brain 4.5.11 โ 4.5.13
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 +2 -2
- package/bin/install.mjs +15 -10
- package/config/model-router/policy.default.mjs +3 -3
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/codex-console-alias.mjs +88 -0
- package/plugin/scripts/session-start-budget.mjs +1 -1
- package/plugin/scripts/session-start-core.mjs +10 -2
- package/scripts/architecture-review-lock.mjs +44 -0
- package/scripts/candidate-ci-receipt.mjs +60 -0
- package/scripts/claude-controlled-terminal.mjs +68 -23
- package/scripts/codex-managed-terminal.mjs +113 -0
- package/scripts/grok-subscription-host.mjs +348 -0
- package/scripts/managed-terminal-input.mjs +152 -0
- package/scripts/model-managed-prompt.mjs +289 -0
- package/scripts/model-managed-workflow-service.mjs +327 -0
- package/scripts/model-routing-controller.mjs +368 -0
- package/scripts/model-routing-defence.mjs +44 -0
- package/scripts/model-routing-execution-adapters.mjs +318 -0
- package/scripts/model-routing-launchers.mjs +31 -0
- package/scripts/model-terminal-launchers.mjs +58 -14
- package/scripts/model-weekly-analyst.mjs +64 -28
- package/scripts/model-weekly-qualification.mjs +6 -1
- package/scripts/prepublication-evidence.mjs +7 -2
- package/scripts/qe/agentic-qe-4.3.mjs +12 -2
- package/scripts/release-qualification-contract.mjs +32 -3
- package/scripts/release-transaction-provider.mjs +10 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# ๐ง RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### ๐ง RuvNet Brain โ [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack โ delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
13
13
|
|
|
@@ -273,7 +273,7 @@ The honest limit: this is **3.5, not 4.0**. The advocacy surface is live and the
|
|
|
273
273
|
|
|
274
274
|
The 2.4 router was **hand-rolled**: our own heuristic with a placeholder policy, presented as "the MetaHarness router." rUv had already shipped the real thing. 2.5 replaces it with **[`@metaharness/router`](https://www.npmjs.com/package/@metaharness/router)** โ his actual learned cost-optimal router (k-NN over labelled embeddings, the productized DRACO Phase-2 finding, ADR-040/043).
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
Subscription membership is an eligibility constraint, not a measurement of allowance consumption. The managed terminal workflow uses the current per-user model and effort policy, supervised native workers, executable checks, independent review, and execution receipts. MetaHarness supplies routing and evaluation capabilities; registration or a price transform alone does not execute a prompt or prove a handoff. The supported entry paths and remaining boundaries are described in `docs/model-routing-operation.md`.
|
|
277
277
|
|
|
278
278
|
> **`npm run substitution:check`** โ a new CI gate that fails the build if any code implements a capability rUv already ships *and* wears his name without either using the real package or openly disclosing the hand-roll. **Verified to fire on the exact commit where we got this wrong.** You may hand-roll. You may never hand-roll *silently*.
|
|
279
279
|
|
package/bin/install.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { downloadFileWithRetry, httpsJsonWithRetry } from '../kb/download-retry.mjs';
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import os from 'node:os';
|
|
17
|
+
import { installCodexConsoleAlias } from '../plugin/scripts/codex-console-alias.mjs';
|
|
17
18
|
import path from 'node:path';
|
|
18
19
|
import { spawn, spawnSync } from 'node:child_process';
|
|
19
20
|
import { readAll as readAllReadiness, aggregate as aggregateReadiness } from '../plugin/scripts/mcp-readiness.mjs';
|
|
@@ -82,7 +83,7 @@ import { shellDiff as pluginShellDiff } from '../plugin/scripts/host-shell-bound
|
|
|
82
83
|
import { codexTrustChanges, CODEX_TRUST_ACTION } from '../scripts/codex-hook-trust.mjs';
|
|
83
84
|
import { probeFreshCodexDeclarations } from '../scripts/codex-fresh-host-proof.mjs';
|
|
84
85
|
import { repairSecurityGuidance } from '../scripts/security-guidance-codex-compat.mjs';
|
|
85
|
-
import { installNativeLaunchers, installRoutingRuntime } from '../scripts/model-routing-launchers.mjs';
|
|
86
|
+
import { installNativeLaunchers, installRoutingRuntime, runtimeSnapshot } from '../scripts/model-routing-launchers.mjs';
|
|
86
87
|
import { installTerminalLaunchers } from '../scripts/model-terminal-launchers.mjs';
|
|
87
88
|
import { readConsoleReceipts, replaceStaleConsoles } from '../scripts/console-instances.mjs';
|
|
88
89
|
import { moveBrain, MoveRefused } from '../scripts/move-brain.mjs';
|
|
@@ -2000,6 +2001,7 @@ export function wireCodexPlugin({
|
|
|
2000
2001
|
cwd = process.cwd(),
|
|
2001
2002
|
announce = true,
|
|
2002
2003
|
runJson = runCodexJson,
|
|
2004
|
+
aliasHome,
|
|
2003
2005
|
} = {}) {
|
|
2004
2006
|
if (!fs.existsSync(codexDir)) return { host: false, action: 'no-host' };
|
|
2005
2007
|
const options = { codexBin, codexHome, cwd };
|
|
@@ -2024,6 +2026,11 @@ export function wireCodexPlugin({
|
|
|
2024
2026
|
if (announce) warn(`Codex Brain plugin is installed but disabled by user or policy โ left disabled (${CODEX_PLUGIN_ID}).`);
|
|
2025
2027
|
return { host: true, action: 'disabled', ...before };
|
|
2026
2028
|
}
|
|
2029
|
+
// Explicit alternate homes support isolated host qualification; metadata-only injected probes
|
|
2030
|
+
// must not write the real user's skill directory.
|
|
2031
|
+
if (aliasHome !== undefined || (runJson === runCodexJson && codexHome === codexHomeDir())) {
|
|
2032
|
+
installCodexConsoleAlias({ home: aliasHome ?? os.homedir(), codexHome });
|
|
2033
|
+
}
|
|
2027
2034
|
// An existing Codex session can keep the plugin generation it loaded at boot. Compare the
|
|
2028
2035
|
// installed bytes with the source candidate before mutating the marketplace so body-only updates
|
|
2029
2036
|
// remain live while a changed/unknown boot surface gets one explicit restart request.
|
|
@@ -4931,7 +4938,7 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
|
|
|
4931
4938
|
const destination = path.join(routerDir, 'bin');
|
|
4932
4939
|
fs.mkdirSync(destination, { recursive: true });
|
|
4933
4940
|
let copied = 0;
|
|
4934
|
-
for (const name of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'subscription-hosts.mjs', 'review-model-defaults.mjs', 'dual-host-deliberation.mjs', 'dual-host-suggest.mjs', 'route-cheap.mjs', 'dispatch-receipt.mjs', 'metaharness-receipts.mjs', 'metaharness-router.mjs', 'model-router-dispatch.mjs', 'model-currency.mjs', 'model-currency-evidence.mjs', 'user-model-prompt-hook.mjs', 'model-router-agent-hook.mjs', 'model-weekly-assessment.mjs', 'model-weekly-analyst.mjs', 'model-weekly-cycle.mjs', 'model-weekly-qualification.mjs', 'model-native-qualification.mjs', 'model-routing-launchers.mjs', 'model-terminal-gateway.mjs', 'model-terminal-launchers.mjs', 'claude-terminal-mod.mjs', 'claude-controlled-terminal.mjs', 'model-native-catalog.mjs', 'model-analyst-sandbox.mjs', 'model-routing-policy-promotion.mjs', 'model-routing-gateway.mjs', 'native-subscription-usage.mjs', 'codex-routed.sh', 'goldie-weekly.sh']) {
|
|
4941
|
+
for (const name of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'subscription-hosts.mjs', 'review-model-defaults.mjs', 'dual-host-deliberation.mjs', 'dual-host-suggest.mjs', 'route-cheap.mjs', 'dispatch-receipt.mjs', 'metaharness-receipts.mjs', 'metaharness-router.mjs', 'model-router-dispatch.mjs', 'model-currency.mjs', 'model-currency-evidence.mjs', 'user-model-prompt-hook.mjs', 'model-router-agent-hook.mjs', 'model-weekly-assessment.mjs', 'model-weekly-analyst.mjs', 'model-weekly-cycle.mjs', 'model-weekly-qualification.mjs', 'model-native-qualification.mjs', 'model-routing-launchers.mjs', 'model-terminal-gateway.mjs', 'model-terminal-launchers.mjs', 'claude-terminal-mod.mjs', 'claude-controlled-terminal.mjs', 'model-managed-prompt.mjs', 'model-managed-workflow-service.mjs', 'model-routing-controller.mjs', 'model-routing-execution-adapters.mjs', 'codex-managed-terminal.mjs', 'grok-subscription-host.mjs', 'model-native-catalog.mjs', 'model-analyst-sandbox.mjs', 'model-routing-policy-promotion.mjs', 'model-routing-gateway.mjs', 'native-subscription-usage.mjs', 'codex-routed.sh', 'goldie-weekly.sh']) {
|
|
4935
4942
|
const source = path.join(packageRoot, 'scripts', name);
|
|
4936
4943
|
if (!fs.existsSync(source)) throw new Error(`Packaged router tool missing: ${name}`);
|
|
4937
4944
|
const target = path.join(destination, name);
|
|
@@ -4942,20 +4949,18 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
|
|
|
4942
4949
|
finally { fs.rmSync(temporary, { force: true }); }
|
|
4943
4950
|
copied++;
|
|
4944
4951
|
}
|
|
4945
|
-
//
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
if (!fs.existsSync(runtimeTarget) || !fs.readFileSync(runtimeTarget).equals(runtimeBytes)) {
|
|
4952
|
+
// Copy the same transitive dependencies used by immutable terminal runtimes.
|
|
4953
|
+
// Repository scripts live in bin here, preserving their relative sibling imports.
|
|
4954
|
+
for (const [relative, runtimeBytes] of runtimeSnapshot(packageRoot).files) {
|
|
4955
|
+
const mapped = relative.startsWith('scripts/') ? `bin/${relative.slice(8)}` : relative;
|
|
4956
|
+
const runtimeTarget = path.join(routerDir, mapped);
|
|
4957
|
+
if (fs.existsSync(runtimeTarget) && fs.readFileSync(runtimeTarget).equals(runtimeBytes)) continue;
|
|
4952
4958
|
fs.mkdirSync(path.dirname(runtimeTarget), { recursive: true });
|
|
4953
4959
|
const temporary = `${runtimeTarget}.${crypto.randomUUID()}.tmp`;
|
|
4954
4960
|
try { fs.writeFileSync(temporary, runtimeBytes, { mode: 0o600 }); fs.renameSync(temporary, runtimeTarget); }
|
|
4955
4961
|
finally { fs.rmSync(temporary, { force: true }); }
|
|
4956
4962
|
copied++;
|
|
4957
4963
|
}
|
|
4958
|
-
}
|
|
4959
4964
|
const qualificationSource = path.join(packageRoot, 'config', 'model-router', 'qualification-contract.json');
|
|
4960
4965
|
const qualificationTarget = path.join(routerDir, 'qualification-contract.json');
|
|
4961
4966
|
const qualificationBytes = fs.readFileSync(qualificationSource);
|
|
@@ -39,12 +39,12 @@ function assessmentText(text) {
|
|
|
39
39
|
export function validateTaskFacts(facts) {
|
|
40
40
|
if (facts === undefined) return undefined;
|
|
41
41
|
if (!facts || typeof facts !== 'object' || Array.isArray(facts)) throw new Error('taskFacts must be an object');
|
|
42
|
-
const keys = new Set(['taskType','scope','uncertainty','consequentialPlanning','finalSubstantiveReview','exceptionalReason']);
|
|
42
|
+
const keys = new Set(['taskType','scope','uncertainty','consequentialPlanning','finalSubstantiveReview','exceptionalReason','verifiedTaskQualityFailure']);
|
|
43
43
|
if (Object.keys(facts).some((key) => !keys.has(key))) throw new Error('Unknown taskFacts field');
|
|
44
44
|
if (facts.taskType !== undefined && !['mechanical','coding','research','planning','review'].includes(facts.taskType)) throw new Error('Invalid taskFacts taskType');
|
|
45
45
|
if (facts.scope !== undefined && !['routine','substantial'].includes(facts.scope)) throw new Error('Invalid taskFacts scope');
|
|
46
46
|
if (facts.uncertainty !== undefined && !['none','architecture','coupled-implementation','missing-information','environment'].includes(facts.uncertainty)) throw new Error('Invalid taskFacts uncertainty');
|
|
47
|
-
for (const key of ['consequentialPlanning','finalSubstantiveReview']) {
|
|
47
|
+
for (const key of ['consequentialPlanning','finalSubstantiveReview','verifiedTaskQualityFailure']) {
|
|
48
48
|
if (facts[key] !== undefined && typeof facts[key] !== 'boolean') throw new Error(`taskFacts ${key} must be boolean`);
|
|
49
49
|
}
|
|
50
50
|
if (facts.exceptionalReason !== undefined && !/^[a-z][a-z0-9-]{2,79}$/.test(facts.exceptionalReason)) {
|
|
@@ -68,7 +68,7 @@ export function classify(features, harness = features.harness || 'codex') {
|
|
|
68
68
|
const facts = validateTaskFacts(features.taskFacts);
|
|
69
69
|
// Partial caller metadata supplements the assessment; it cannot lower explicit high-consequence text.
|
|
70
70
|
if (facts?.exceptionalReason) return harness === 'claude-code' ? 'hard' : 'exceptional';
|
|
71
|
-
if (hardText) return 'hard';
|
|
71
|
+
if (hardText || facts?.verifiedTaskQualityFailure) return 'hard';
|
|
72
72
|
if (facts && (['architecture','coupled-implementation'].includes(facts.uncertainty) ||
|
|
73
73
|
facts.consequentialPlanning || facts.finalSubstantiveReview ||
|
|
74
74
|
((facts.scope === 'substantial' || substantialText) && ['planning','review'].includes(facts.taskType)))) return 'hard';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.5.
|
|
4
|
-
"description": "One-command installer for RuvNet Brain
|
|
3
|
+
"version": "4.5.13",
|
|
4
|
+
"description": "One-command installer for RuvNet Brain โ a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ruvnet-brain": "bin/install.mjs"
|
|
@@ -149,8 +149,10 @@
|
|
|
149
149
|
"vitest": "^4.1.10"
|
|
150
150
|
},
|
|
151
151
|
"dependencies": {
|
|
152
|
+
"@claude-flow/aidefence": "^3.0.3",
|
|
152
153
|
"@metaharness/flywheel": "^0.1.7",
|
|
153
154
|
"@metaharness/router": "^0.4.0",
|
|
155
|
+
"@pacphi/agentic-kit": "next",
|
|
154
156
|
"ws": "^8.22.0"
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code โ grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.5.
|
|
4
|
+
"version": "4.5.13",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export const ALIAS_MARKER = '<!-- managed-by: ruvnet-brain; short-console-alias-v1 -->';
|
|
7
|
+
export const ALIAS_NOTICE = 'RuvNet Brain is loaded. Configure it with $rvbc.';
|
|
8
|
+
const aliasPath = (home) => path.join(home, '.agents', 'skills', 'rvbc', 'SKILL.md');
|
|
9
|
+
const aliasBody = `---\nname: rvbc\ndescription: Open the RuvNet Brain configuration console when the user invokes $rvbc or says Configure RuvNet Brain.\n---\n\n${ALIAS_MARKER}\nOpen the installed RuvNet Brain Console. Resolve \`\${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs\`; do not guess a developer checkout. Run \`node <resolved-script> --serve --open\` in the background. Preserve the user's configuration. Report startup failures honestly. Verify the rendered console and client accessibility before claiming it opened or presenting its URL.\n`;
|
|
10
|
+
|
|
11
|
+
export function ownedCodexConsoleAlias(home = os.homedir()) {
|
|
12
|
+
try {
|
|
13
|
+
const file = aliasPath(home);
|
|
14
|
+
if (fs.lstatSync(path.dirname(file)).isSymbolicLink() || !fs.lstatSync(file).isFile()
|
|
15
|
+
|| fs.lstatSync(file).isSymbolicLink()) return false;
|
|
16
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
17
|
+
return text.includes(ALIAS_MARKER) && /^name: rvbc$/m.test(text);
|
|
18
|
+
} catch { return false; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function installCodexConsoleAlias({ home = os.homedir(), codexHome = path.join(home, '.codex'), enabled = true } = {}) {
|
|
22
|
+
if (!enabled) return { action: 'disabled' };
|
|
23
|
+
const file = aliasPath(home);
|
|
24
|
+
try {
|
|
25
|
+
const config = fs.existsSync(path.join(codexHome, 'config.toml')) ? fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8') : '';
|
|
26
|
+
for (const section of config.split(/(?=^\[)/m)) {
|
|
27
|
+
if (!/^\[\[skills\.config\]\]/.test(section) || !/^\s*enabled\s*=\s*false\s*(?:#.*)?$/m.test(section)) continue;
|
|
28
|
+
const disabled = section.match(/^\s*path\s*=\s*(['"])(.*?)\1\s*(?:#.*)?$/m)?.[2];
|
|
29
|
+
if (disabled && [file, path.dirname(file)].includes(path.resolve(disabled))) return { action: 'disabled' };
|
|
30
|
+
}
|
|
31
|
+
if (fs.existsSync(path.dirname(file)) && !ownedCodexConsoleAlias(home)) return { action: 'user-owned' };
|
|
32
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
34
|
+
if (existing === aliasBody) return { action: 'unchanged', path: file };
|
|
35
|
+
fs.writeFileSync(file, aliasBody);
|
|
36
|
+
return { action: existing === null ? 'installed' : 'updated', path: file };
|
|
37
|
+
} catch (error) { return { action: 'unavailable', reason: error.message }; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fresh native metadata only: no inference or plugin mutation requested.
|
|
41
|
+
export function probeCodexConsoleAlias({ home = os.homedir(), codexHome = path.join(home, '.codex'),
|
|
42
|
+
cwd = process.cwd(), binary = 'codex', timeoutMs = 150, spawnChild = spawn } = {}) {
|
|
43
|
+
if (!ownedCodexConsoleAlias(home)) return Promise.resolve(false);
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
let child; let timer; let buffer = ''; let settled = false;
|
|
46
|
+
const finish = (ready) => { if (settled) return; settled = true; clearTimeout(timer); try { child?.kill(); } catch {} resolve(ready); };
|
|
47
|
+
try { child = spawnChild(binary, ['app-server'], { cwd, env: { ...process.env, HOME: home, CODEX_HOME: codexHome }, stdio: ['pipe', 'pipe', 'ignore'] }); }
|
|
48
|
+
catch { finish(false); return; }
|
|
49
|
+
timer = setTimeout(() => finish(false), timeoutMs);
|
|
50
|
+
child.on('error', () => finish(false)); child.on('exit', () => finish(false));
|
|
51
|
+
child.stdin.on('error', () => finish(false));
|
|
52
|
+
child.stdout.on('data', (chunk) => {
|
|
53
|
+
buffer += String(chunk);
|
|
54
|
+
if (buffer.length > 8 * 1024 * 1024) { finish(false); return; }
|
|
55
|
+
let newline;
|
|
56
|
+
while ((newline = buffer.indexOf('\n')) >= 0) {
|
|
57
|
+
let message; try { message = JSON.parse(buffer.slice(0, newline)); } catch {}
|
|
58
|
+
buffer = buffer.slice(newline + 1);
|
|
59
|
+
if (message?.id === 1 && !message.error) {
|
|
60
|
+
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
|
61
|
+
child.stdin.write(`${JSON.stringify({ id: 2, method: 'skills/list', params: { cwds: [cwd], forceReload: true } })}\n`);
|
|
62
|
+
} else if (message?.id === 2) {
|
|
63
|
+
const groups = message.result?.data;
|
|
64
|
+
const ready = !message.error && Array.isArray(groups) && groups.length === 1 && groups[0].cwd === cwd
|
|
65
|
+
&& Array.isArray(groups[0].skills) && !(groups[0].errors?.length)
|
|
66
|
+
&& groups[0].skills.some((row) => row.name === 'rvbc' && row.path === aliasPath(home) && row.enabled === true);
|
|
67
|
+
finish(Boolean(ready && ownedCodexConsoleAlias(home)));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
child.stdin.write(`${JSON.stringify({ id: 1, method: 'initialize', params: { clientInfo: { name: 'rnb_short_console_alias', version: '1' } } })}\n`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readSessionSource(input = process.stdin) {
|
|
76
|
+
if (input.isTTY) return Promise.resolve(null);
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let raw = ''; let settled = false;
|
|
79
|
+
const finish = () => {
|
|
80
|
+
if (settled) return; settled = true; clearTimeout(timer); input.pause();
|
|
81
|
+
input.removeListener('data', onData); input.removeListener('end', finish); input.removeListener('error', finish);
|
|
82
|
+
try { resolve(JSON.parse(raw).source ?? null); } catch { resolve(null); }
|
|
83
|
+
};
|
|
84
|
+
const onData = (chunk) => { raw += String(chunk); if (raw.length > 32768) { raw = ''; finish(); } };
|
|
85
|
+
const timer = setTimeout(finish, 100);
|
|
86
|
+
input.on('data', onData); input.once('end', finish); input.once('error', finish); input.resume();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -47,7 +47,7 @@ export const STAGE_BUDGETS_MS = {
|
|
|
47
47
|
'stable-spine': 300, // seed-dispatch decision + a single detach launch
|
|
48
48
|
heartbeat: 300, // update-check dispatch launch
|
|
49
49
|
'ascii-drift': 300, // optional ascii->svg drift advisory, already spawnSync-timeout bounded
|
|
50
|
-
banner: 200, //
|
|
50
|
+
banner: 200, // banner assembly, including a bounded native skill metadata probe
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export function sumBudgetsMs(budgets = STAGE_BUDGETS_MS) {
|
|
@@ -39,6 +39,7 @@ import { FOOTPRINT_LINE_PREFIX } from './brain-confirmation.mjs';
|
|
|
39
39
|
import { describeLifecycleHooks, readHookContracts } from './session-start-hook-description.mjs';
|
|
40
40
|
import { createStageTracer } from './session-start-trace.mjs';
|
|
41
41
|
import { sessionStartProofRecorder } from './session-start-proof.mjs';
|
|
42
|
+
import { ALIAS_NOTICE, probeCodexConsoleAlias, readSessionSource } from './codex-console-alias.mjs';
|
|
42
43
|
|
|
43
44
|
// Re-exported for callers/tests that import the entitlement check directly from this file's own
|
|
44
45
|
// long-standing public surface (tests/unit/session-start-core-parity.test.mjs).
|
|
@@ -90,6 +91,8 @@ export async function runSessionStart({
|
|
|
90
91
|
platform = process.platform,
|
|
91
92
|
restoreContinuity = restoreWithBrief,
|
|
92
93
|
runHeartbeat = true,
|
|
94
|
+
sessionSource = 'startup',
|
|
95
|
+
probeConsoleAlias = probeCodexConsoleAlias,
|
|
93
96
|
} = {}) {
|
|
94
97
|
const lines = [];
|
|
95
98
|
// SessionStart is context plumbing, not an instruction channel. Keep factual, actionable
|
|
@@ -99,6 +102,7 @@ export async function runSessionStart({
|
|
|
99
102
|
const isSafeStatus = (line) => {
|
|
100
103
|
const s = String(line);
|
|
101
104
|
return s.startsWith('๐จ')
|
|
105
|
+
|| s === ALIAS_NOTICE
|
|
102
106
|
|| s.startsWith('[RuvNet Brain โ HEALTH ALARM')
|
|
103
107
|
|| s.startsWith('[RuvNet Brain โ INSTALL ALARM')
|
|
104
108
|
|| s.startsWith('[RuvNet Brain โ NIGHTLY FAILED')
|
|
@@ -332,7 +336,7 @@ export async function runSessionStart({
|
|
|
332
336
|
}
|
|
333
337
|
});
|
|
334
338
|
|
|
335
|
-
tracer.
|
|
339
|
+
await tracer.stageAsync('banner', async () => {
|
|
336
340
|
const bannerVersion = effectiveVersion || 'unknown';
|
|
337
341
|
if (pluginRoot.startsWith(path.join(home, '.claude', 'plugins') + path.sep)) {
|
|
338
342
|
if (running) write(path.join(stateDir, '.running-version'), `${running}\n`);
|
|
@@ -368,6 +372,9 @@ export async function runSessionStart({
|
|
|
368
372
|
// correction it is delivered to everyone, worded for the user, through the same always-shown
|
|
369
373
|
// alarm mechanism as HEALTH ALARM above (never gated by maintainerIssueEntitlement).
|
|
370
374
|
emit(`[RuvNet Brain v${bannerVersion} โ active this session${updated ? ` ยท updated ${updated}` : ''}]`);
|
|
375
|
+
if (env.RUVNET_HOOK_HOST === 'codex' && sessionSource === 'startup'
|
|
376
|
+
&& await probeConsoleAlias({ home, codexHome: env.CODEX_HOME || path.join(home, '.codex'),
|
|
377
|
+
cwd, binary: env.CODEX_BIN || 'codex' })) emit(ALIAS_NOTICE);
|
|
371
378
|
bannerEmitted = true;
|
|
372
379
|
// S2 (ONE CURRENCY VERDICT): this no longer compares the KB's own SOURCE.json releaseTag to the
|
|
373
380
|
// plugin version โ that heuristic fires a FALSE POSITIVE for the entire (normal, expected)
|
|
@@ -430,7 +437,8 @@ function isDirectInvocation() {
|
|
|
430
437
|
|
|
431
438
|
const direct = isDirectInvocation();
|
|
432
439
|
if (direct) {
|
|
433
|
-
|
|
440
|
+
const sessionSource = await readSessionSource();
|
|
441
|
+
runSessionStart({ sessionSource }).catch((error) => {
|
|
434
442
|
if (process.env.RUVNET_SESSION_TRACE === '1') {
|
|
435
443
|
process.stderr.write(`SESSION_TRACE native-fail-open ${error?.message || error}\n`);
|
|
436
444
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A release adapter over doc-currency, not a second review engine.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { REPO_ROOT, evaluateDoc, blockingFindings } from './doc-currency.mjs';
|
|
6
|
+
|
|
7
|
+
export const GOVERNING_ADR = 'docs/adr/0103-routing-architecture-review-lock.md';
|
|
8
|
+
|
|
9
|
+
export function evaluateArchitectureReview(root = REPO_ROOT) {
|
|
10
|
+
try {
|
|
11
|
+
const doc = evaluateDoc(root, GOVERNING_ADR);
|
|
12
|
+
const findings = blockingFindings([doc], { strict: true });
|
|
13
|
+
const block = (code, message) => findings.push({ file: GOVERNING_ADR, level: 'block', code, message });
|
|
14
|
+
if (doc.id !== 'ADR-103' || doc.status !== 'Accepted') {
|
|
15
|
+
block('architecture-decision-not-accepted', 'The finite governing ADR-103 must be Accepted.');
|
|
16
|
+
}
|
|
17
|
+
if (!doc.governsDeclared.length || doc.governsDeclared.some((entry) => /[*?\[\]{}]/.test(entry))
|
|
18
|
+
|| !doc.governed?.length || doc.governed.some((entry) => !entry.resolved)) {
|
|
19
|
+
block('architecture-scope-incomplete', 'The governing set must name existing tracked files explicitly, without directories or globs.');
|
|
20
|
+
}
|
|
21
|
+
if (doc.review?.current !== true) {
|
|
22
|
+
block('architecture-review-not-current', 'Update the architecture/test mapping and record a source-bound review of the final governed bytes before qualification.');
|
|
23
|
+
}
|
|
24
|
+
return { schemaVersion: 1, pass: findings.length === 0, file: GOVERNING_ADR,
|
|
25
|
+
review: doc.review ?? null, findings };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { schemaVersion: 1, pass: false, file: GOVERNING_ADR, review: null,
|
|
28
|
+
findings: [{ file: GOVERNING_ADR, level: 'block', code: 'architecture-review-unreadable', message: error.message }] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function main(argv = process.argv.slice(2)) {
|
|
33
|
+
if (argv.length && (argv.length !== 2 || argv[0] !== '--root')) {
|
|
34
|
+
console.error('Usage: node scripts/architecture-review-lock.mjs [--root PATH]');
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
const result = evaluateArchitectureReview(argv.length ? path.resolve(argv[1]) : REPO_ROOT);
|
|
38
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39
|
+
return result.pass ? 0 : 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
43
|
+
process.exitCode = main();
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { verifyPayloadMembers } from './release-payload.mjs';
|
|
7
|
+
import { validateQualificationReceipt } from './release-qualification.mjs';
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_CI_JOBS = ['candidate-preflight', 'release-acceptance-linux',
|
|
10
|
+
'release-acceptance-windows', 'release-acceptance-macos', 'release-qe'];
|
|
11
|
+
const sha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
|
|
13
|
+
export function createCandidateCiReceipt({ sha, runId, runAttempt, results, ciResult, manifestFile,
|
|
14
|
+
candidateFile, acceptanceDir }) {
|
|
15
|
+
if (!/^[a-f0-9]{40}$/.test(sha || '') || !Number.isSafeInteger(runId) || runId <= 0
|
|
16
|
+
|| !Number.isSafeInteger(runAttempt) || runAttempt <= 0) throw new Error('candidate CI run identity is invalid');
|
|
17
|
+
if (ciResult !== 'success') throw new Error('candidate CI reusable workflow did not succeed');
|
|
18
|
+
if (!results || Object.keys(results).sort().join(',') !== [...REQUIRED_CI_JOBS].sort().join(',')
|
|
19
|
+
|| REQUIRED_CI_JOBS.some(name => results[name] !== 'success')) {
|
|
20
|
+
throw new Error('candidate CI contains a missing or non-success dependency');
|
|
21
|
+
}
|
|
22
|
+
const manifestBytes = fs.readFileSync(manifestFile);
|
|
23
|
+
const manifest = JSON.parse(manifestBytes);
|
|
24
|
+
const candidate = JSON.parse(fs.readFileSync(candidateFile));
|
|
25
|
+
if (manifest.candidateSha !== sha || candidate.sha !== sha || manifest.version !== candidate.version
|
|
26
|
+
|| !/^\d+\.\d+\.\d+$/.test(manifest.version || '') || manifest.tag !== `v${manifest.version}`) {
|
|
27
|
+
throw new Error('candidate CI receipt identity differs from checked-out source');
|
|
28
|
+
}
|
|
29
|
+
const members = manifest.members;
|
|
30
|
+
if (!Array.isArray(members) || !members.length
|
|
31
|
+
|| new Set(members.map(row => row.name)).size !== members.length
|
|
32
|
+
|| members.filter(row => row.role === 'npm').length !== 1) throw new Error('candidate CI payload members are invalid');
|
|
33
|
+
const payload = verifyPayloadMembers({ manifest, root: path.dirname(manifestFile) });
|
|
34
|
+
const npm = members.find(row => row.role === 'npm');
|
|
35
|
+
if (String(candidate.artifact?.sha256 || '').replace(/^sha256:/, '') !== npm.sha256) {
|
|
36
|
+
throw new Error('candidate package digest differs from payload');
|
|
37
|
+
}
|
|
38
|
+
const acceptanceReceipts = ['linux', 'macos', 'windows'].map(platform => {
|
|
39
|
+
const bytes = fs.readFileSync(path.join(acceptanceDir, `release-acceptance-${platform}.json`));
|
|
40
|
+
const report = JSON.parse(bytes);
|
|
41
|
+
const tests = validateQualificationReceipt(report, { suite: 'source', platform, sourceSha: sha });
|
|
42
|
+
return { platform, sourceSha: report.source.sha, passed: tests.passed, receiptSha256: sha256(bytes) };
|
|
43
|
+
});
|
|
44
|
+
return { acceptanceReceipts, schemaVersion: 1, kind: 'ruvnet-brain-candidate-ci-evidence',
|
|
45
|
+
sourceSha: sha, version: manifest.version, payloadId: payload.payloadId,
|
|
46
|
+
payloadManifestSha256: sha256(manifestBytes), producerWorkflow: 'release-candidate-preflight',
|
|
47
|
+
producerJob: 'aggregate', summarizedWorkflow: 'ci', runId, runAttempt,
|
|
48
|
+
jobs: REQUIRED_CI_JOBS.map(name => ({ name, conclusion: results[name],
|
|
49
|
+
workflow: name === 'candidate-preflight' ? 'release-candidate-preflight' : 'ci' })),
|
|
50
|
+
verdict: 'PASS', skipped: 0, unknown: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
54
|
+
const receipt = createCandidateCiReceipt({ sha: process.env.CANDIDATE_SHA,
|
|
55
|
+
runId: Number(process.env.GITHUB_RUN_ID), runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
|
|
56
|
+
ciResult: process.env.CI_RESULT,
|
|
57
|
+
results: { ...JSON.parse(process.env.CI_RESULTS_JSON), 'candidate-preflight': process.env.PREFLIGHT_RESULT }, manifestFile: 'release-evidence/payload-manifest.json',
|
|
58
|
+
candidateFile: 'release-evidence/candidate-receipt.json', acceptanceDir: 'acceptance-reports' });
|
|
59
|
+
fs.writeFileSync('lane-evidence/ci/candidate-ci-evidence.json', JSON.stringify(receipt, null, 2) + '\n', { flag: 'wx' });
|
|
60
|
+
}
|