ruvnet-brain 4.0.1 → 4.0.2
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/.claude-plugin/marketplace.json +1 -0
- package/README.md +4 -4
- package/bin/install.mjs +100 -5
- package/console/CONTRACT.md +172 -0
- package/console/activity.js +753 -0
- package/console/app.js +4189 -0
- package/console/architecture.html +1221 -0
- package/console/assets/depth-1.webp +0 -0
- package/console/assets/depth-2.webp +0 -0
- package/console/assets/depth-3.webp +0 -0
- package/console/assets/harness-vs-plain.svg +259 -0
- package/console/assets/hero.webp +0 -0
- package/console/assets/memory.webp +0 -0
- package/console/assets/metaharness.svg +247 -0
- package/console/index.html +777 -0
- package/console/install-architecture.html +162 -0
- package/console/install-mockup.html +543 -0
- package/console/style.css +2144 -0
- package/console/tips.css +926 -0
- package/console/tips.html +858 -0
- package/console/tips.js +128 -0
- package/docs/RELEASE-NOTES-4.0.md +88 -0
- package/kb/model-requirements.mjs +37 -6
- package/keys/ruvnet-brain-signing.pub.pem +3 -0
- package/package.json +8 -22
- package/plugin/.claude-plugin/marketplace.json +1 -0
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +2 -2
- package/plugin/commands/configure.md +3 -2
- package/plugin/commands/rvbc.md +4 -3
- package/plugin/commands/rvcb.md +2 -2
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +21 -0
- package/plugin/scripts/detach.mjs +14 -0
- package/plugin/scripts/first-session-worker.mjs +38 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -6
- package/plugin/scripts/hook-shim.mjs +7 -7
- package/plugin/scripts/learn-capture.sh +22 -3
- package/plugin/scripts/learn-flush.mjs +21 -4
- package/plugin/scripts/runtime-preferences.mjs +269 -0
- package/plugin/scripts/session-start-core.mjs +477 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +81 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/scripts/adr-backfill.mjs +107 -0
- package/scripts/advocacy-outcomes.mjs +808 -0
- package/scripts/agentdb-context.mjs +216 -0
- package/scripts/agentdb-fleet-doctor.mjs +101 -0
- package/scripts/ascii-drift.mjs +236 -0
- package/scripts/behavioral-l1-l4.mjs +210 -0
- package/scripts/brain-capability-check.mjs +72 -0
- package/scripts/brain-grade-groundtruth.mjs +100 -0
- package/scripts/brain-latency-50.mjs +227 -0
- package/scripts/brain-novice-50.mjs +189 -0
- package/scripts/brain-stamp.mjs +94 -0
- package/scripts/brain-state.mjs +212 -0
- package/scripts/build-bundle.mjs +522 -0
- package/scripts/build-concepts.mjs +132 -0
- package/scripts/build-l2.mjs +71 -0
- package/scripts/build-primer.mjs +73 -0
- package/scripts/build-symbols.mjs +68 -0
- package/scripts/calibrate-router.mjs +97 -0
- package/scripts/capability-audit.mjs +321 -0
- package/scripts/capability-registry.mjs +876 -0
- package/scripts/check-indexation.mjs +108 -0
- package/scripts/check-legibility.mjs +189 -0
- package/scripts/ci/build-fixture-kb.mjs +67 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
- package/scripts/ci/learning-replay-recorder.mjs +59 -0
- package/scripts/ci/mutate-hook-timeout.mjs +70 -0
- package/scripts/ci/stranger-fixture-stage.mjs +17 -0
- package/scripts/ci/stranger-scenario.mjs +228 -0
- package/scripts/ci/stranger-timeout.mjs +25 -0
- package/scripts/ci-verdict.mjs +29 -0
- package/scripts/claims-verify.mjs +710 -0
- package/scripts/clear-claude-tmp.sh +31 -0
- package/scripts/console-engine.mjs +434 -0
- package/scripts/console-engine.test.mjs +125 -0
- package/scripts/corpus-qa.mjs +250 -0
- package/scripts/correction-detect-embed.mjs +346 -0
- package/scripts/correction-detect-measure.mjs +270 -0
- package/scripts/correction-detect.mjs +686 -0
- package/scripts/count-chunks.mjs +54 -0
- package/scripts/described-questions.json +30 -0
- package/scripts/design-grade.mjs +58 -0
- package/scripts/dev-plugin-link.sh +105 -0
- package/scripts/distill-project.mjs +200 -0
- package/scripts/doc-currency.mjs +801 -0
- package/scripts/eval-brain.mjs +244 -0
- package/scripts/fix-metaharness-memretrieve.mjs +121 -0
- package/scripts/full-hints.mjs +87 -0
- package/scripts/gate.sh +39 -0
- package/scripts/gates.mjs +146 -0
- package/scripts/gen-console-images.mjs +54 -0
- package/scripts/gen-images.mjs +47 -0
- package/scripts/git-clone-refresh.mjs +52 -0
- package/scripts/git-hooks/pre-push +126 -0
- package/scripts/goal-match.mjs +398 -0
- package/scripts/goldie-research.mjs +223 -0
- package/scripts/goldie-weekly.sh +67 -0
- package/scripts/health-repair.mjs +250 -0
- package/scripts/helix-scenario-questions.json +10 -0
- package/scripts/ingest-gists.mjs +230 -0
- package/scripts/ingest-meeting.mjs +115 -0
- package/scripts/ingest-repo.mjs +79 -0
- package/scripts/install-npx-witness.sh +49 -0
- package/scripts/issue-fix.mjs +639 -0
- package/scripts/issue-watch.mjs +276 -0
- package/scripts/issue4-close-note.md +31 -0
- package/scripts/key-canary.mjs +91 -0
- package/scripts/latency-to-surface.mjs +233 -0
- package/scripts/learning-enable.mjs +380 -0
- package/scripts/learning-replay.mjs +1570 -0
- package/scripts/learnings.mjs +62 -0
- package/scripts/lesson-gate.mjs +680 -0
- package/scripts/lesson-lifecycle.mjs +449 -0
- package/scripts/lesson-promote.mjs +262 -0
- package/scripts/lesson-ratify.mjs +98 -0
- package/scripts/lesson-seed.mjs +252 -0
- package/scripts/lesson-store.mjs +447 -0
- package/scripts/loop-checkpoint.mjs +86 -0
- package/scripts/memdb-health.sh +14 -0
- package/scripts/memory-doctor.mjs +271 -0
- package/scripts/model-catalog.mjs +79 -0
- package/scripts/nightly-controller.mjs +66 -0
- package/scripts/nightly-gists.sh +72 -0
- package/scripts/nightly-wrapper.sh +180 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2749 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/prove.mjs +95 -0
- package/scripts/proxy/claude-proxied.sh +57 -0
- package/scripts/proxy/proxy-revert.sh +59 -0
- package/scripts/proxy/proxy-up.sh +60 -0
- package/scripts/proxy/proxy-verify.mjs +142 -0
- package/scripts/published-surface-probe.mjs +241 -0
- package/scripts/qe/card-lane-gate.mjs +162 -0
- package/scripts/qe/session-start-gate.mjs +229 -0
- package/scripts/qe/ux-suite.mjs +323 -0
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/record-lesson.mjs +113 -0
- package/scripts/refresh-model-catalog.mjs +99 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +395 -0
- package/scripts/remedy-registry.mjs +247 -0
- package/scripts/rerank-cap-eval.mjs +265 -0
- package/scripts/rerank-cap-warm-ab.mjs +129 -0
- package/scripts/route-cheap.mjs +20 -15
- package/scripts/router-utilization.mjs +182 -0
- package/scripts/routing-flywheel.mjs +596 -0
- package/scripts/rvf-generation.mjs +104 -0
- package/scripts/rvf-index-audit.mjs +138 -0
- package/scripts/self-update.mjs +508 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stack-sync.mjs +469 -0
- package/scripts/stamp-existing-rvf-generations.mjs +53 -0
- package/scripts/stamp-sweep.mjs +144 -0
- package/scripts/status-honesty.mjs +102 -0
- package/scripts/sync-version.mjs +217 -0
- package/scripts/token-report.mjs +102 -0
- package/scripts/top100-benchmark.mjs +479 -0
- package/scripts/top100-corpus.mjs +112 -0
- package/scripts/top100-semantic-assertions.mjs +449 -0
- package/scripts/update-apply.mjs +9 -0
- package/scripts/upgrade-notice.mjs +14 -0
- package/scripts/verify-bundle.mjs +51 -0
- package/scripts/verify-channels.mjs +184 -0
- package/scripts/verify-model-catalog.mjs +104 -0
- package/scripts/verify-nightly-close-issue4.sh +31 -0
- package/scripts/version.mjs +40 -0
- package/scripts/wired-check.mjs +864 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// gates.mjs — what stands between the model and your machine, and what it has caught.
|
|
2
|
+
//
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// WHY (2026-07-17). Stuart, looking at the console's wiring card: "I have no idea what message it's
|
|
5
|
+
// supposed to tell me… it seems to be facts without purpose." The card counted launch sites. Nobody
|
|
6
|
+
// wants a census. The question worth answering is the one the harness exists for: WHAT STOPS CLAUDE
|
|
7
|
+
// FROM BEING WRONG, AND HAS IT EVER ACTUALLY STOPPED IT?
|
|
8
|
+
//
|
|
9
|
+
// Two things had to be true before that card could be honest, and neither was:
|
|
10
|
+
//
|
|
11
|
+
// 1. The console never read the gates. wiringSurvey() walks ~/Code project settings only, so the
|
|
12
|
+
// 12 machine-wide hooks in ~/.claude/settings.json and the 9 in the plugin's own hooks.json —
|
|
13
|
+
// including the design wall that blocks commits — were invisible to the page bragging about them.
|
|
14
|
+
//
|
|
15
|
+
// 2. The gates never recorded a block. Only successes were logged, so "the harness caught Claude"
|
|
16
|
+
// had no receipt. gate-receipt.sh now writes one at the moment of refusal.
|
|
17
|
+
//
|
|
18
|
+
// THE HONEST DISTINCTION this module exists to draw: a hook wired with `|| true` CANNOT block —
|
|
19
|
+
// it injects context and the tool call proceeds regardless. Only a gate that can exit non-zero
|
|
20
|
+
// stops anything. Counting all hooks as "protection" would be the same inflation as counting
|
|
21
|
+
// cloned upstream repos as your own wiring. Advisory and blocking are different claims.
|
|
22
|
+
//
|
|
23
|
+
// Reads only. Never asserts a count it cannot source from a file on this machine.
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import os from 'node:os';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
|
|
30
|
+
const HOME = os.homedir();
|
|
31
|
+
const BLOCKS = path.join(HOME, '.cache/ruvnet-brain/gate-blocks.jsonl');
|
|
32
|
+
|
|
33
|
+
function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } }
|
|
34
|
+
|
|
35
|
+
// Two independent things must BOTH be true for a hook to stop anything, and conflating them is how
|
|
36
|
+
// a census gets sold as protection:
|
|
37
|
+
// 1. The EVENT must be one that runs before the thing it would stop. PreToolUse gates a tool call;
|
|
38
|
+
// UserPromptSubmit gates a prompt. SessionStart has nothing to refuse yet, and PostToolUse /
|
|
39
|
+
// PreCompact / SessionEnd arrive after the fact — those inject or record, they never block.
|
|
40
|
+
// 2. The COMMAND must not end in `|| true`, which swallows the exit code the gate would refuse with.
|
|
41
|
+
const BLOCKING_EVENTS = new Set(['PreToolUse', 'UserPromptSubmit']);
|
|
42
|
+
const swallowsExit = (cmd) => /\|\|\s*true\s*$/.test(String(cmd || '').trim());
|
|
43
|
+
const canBlock = (event, cmd) => BLOCKING_EVENTS.has(event) && !swallowsExit(cmd);
|
|
44
|
+
|
|
45
|
+
const NAME = (cmd) => {
|
|
46
|
+
const m = String(cmd || '').match(/([\w-]+)\.(sh|mjs|js)/);
|
|
47
|
+
return m ? m[1] : String(cmd || '').split(/\s+/).filter((t) => !t.startsWith('-')).pop()?.slice(0, 28) || 'hook';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function collect(hooksObj, source) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const [event, groups] of Object.entries(hooksObj || {})) {
|
|
53
|
+
const list = Array.isArray(groups) ? groups : [groups];
|
|
54
|
+
for (const g of list) {
|
|
55
|
+
const hooks = Array.isArray(g?.hooks) ? g.hooks : (g?.command ? [g] : []);
|
|
56
|
+
for (const h of hooks) {
|
|
57
|
+
if (!h?.command) continue;
|
|
58
|
+
out.push({ event, matcher: g?.matcher ?? '*', name: NAME(h.command), blocking: canBlock(event, h.command), source });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Every catch the gates have recorded. This file only exists once a gate has actually refused
|
|
66
|
+
// something — an empty ledger is an honest "nothing caught yet", never a failure.
|
|
67
|
+
export function gateBlocks() {
|
|
68
|
+
try {
|
|
69
|
+
return fs.readFileSync(BLOCKS, 'utf8').trim().split('\n')
|
|
70
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
} catch { return []; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function gatesSurvey({ repo } = {}) {
|
|
76
|
+
const machine = collect(readJSON(path.join(HOME, '.claude/settings.json'))?.hooks, 'machine');
|
|
77
|
+
const pluginCfg = repo ? readJSON(path.join(repo, 'plugin/hooks/hooks.json')) : null;
|
|
78
|
+
const plugin = collect(pluginCfg?.hooks || pluginCfg, 'plugin');
|
|
79
|
+
|
|
80
|
+
const all = [...machine, ...plugin];
|
|
81
|
+
const blocking = all.filter((g) => g.blocking);
|
|
82
|
+
|
|
83
|
+
// THE LEDGER IS MACHINE-WIDE; THIS SURVEY IS ABOUT ONE PROJECT. Every catch ever recorded on the
|
|
84
|
+
// machine used to be counted here, so standing in an empty folder produced "203 refusals have been
|
|
85
|
+
// recorded" — this repo's history, attributed to a project that has never run a gate. Each record
|
|
86
|
+
// carries the `cwd` it was caught in, so when a project is named, only its own catches count.
|
|
87
|
+
//
|
|
88
|
+
// KNOWN LIMIT, stated rather than hidden: `cwd` is a basename, so two projects sharing a folder
|
|
89
|
+
// name share a count. That is a real ambiguity and it is narrow; attributing the whole machine's
|
|
90
|
+
// history to whichever directory you happen to be standing in was neither.
|
|
91
|
+
const here = repo ? path.basename(path.resolve(repo)) : null;
|
|
92
|
+
const allBlocks = gateBlocks();
|
|
93
|
+
const blocks = here ? allBlocks.filter((b) => b.cwd === here) : allBlocks;
|
|
94
|
+
|
|
95
|
+
// Same gate, same event, wired both machine-wide AND by the plugin — it runs twice on every
|
|
96
|
+
// matching call. Harmless to correctness (these gates are idempotent) but it is real duplicated
|
|
97
|
+
// work, and counting it as two protections would inflate the only number on the card that matters.
|
|
98
|
+
const seen = new Map();
|
|
99
|
+
const duplicated = [];
|
|
100
|
+
for (const g of all) {
|
|
101
|
+
const k = `${g.event}:${g.name}`;
|
|
102
|
+
if (seen.has(k) && seen.get(k) !== g.source) { if (!duplicated.includes(g.name)) duplicated.push(g.name); }
|
|
103
|
+
seen.set(k, g.source);
|
|
104
|
+
}
|
|
105
|
+
const uniqueBlocking = new Set(blocking.map((g) => `${g.event}:${g.name}`)).size;
|
|
106
|
+
|
|
107
|
+
// Group the catches by gate so the card can say WHAT was caught, not just how many times.
|
|
108
|
+
const byGate = {};
|
|
109
|
+
for (const b of blocks) (byGate[b.gate] ||= []).push(b);
|
|
110
|
+
|
|
111
|
+
const weekAgo = Date.now() - 7 * 864e5;
|
|
112
|
+
const recent = blocks.filter((b) => Date.parse(b.at || 0) >= weekAgo);
|
|
113
|
+
|
|
114
|
+
// THREE NUMBERS, ONE UNIT. This block used to mix two: `blocking` was DEDUPLICATED
|
|
115
|
+
// (distinct event:name) while `advisory` was `all.length - blocking.length`, computed from the
|
|
116
|
+
// RAW array. So `blocking + advisory` fell short of `armed` by exactly the number of duplicated
|
|
117
|
+
// blocking wirings, and the console printed "N gates armed — B can stop a call. The other A add
|
|
118
|
+
// context" where B + A ≠ N. On a machine with 4 duplicate blocking gates the sentence silently
|
|
119
|
+
// lost four of them — in the one sentence whose entire job is to account for all of them.
|
|
120
|
+
//
|
|
121
|
+
// It summed correctly on any machine with no duplicates, which is why it survived: the defect was
|
|
122
|
+
// invisible exactly where it was most often looked at. Found by Fable 5, 2026-07-24, by adding up
|
|
123
|
+
// the numbers on the owner's own console.
|
|
124
|
+
//
|
|
125
|
+
// Fixed by reporting all three in WIRED-ENTRY units, so armed = blocking + advisory holds by
|
|
126
|
+
// construction. The distinct-gate count is still exported — it is genuinely the more meaningful
|
|
127
|
+
// number for "how many different things can refuse" — but under its own name, where it cannot be
|
|
128
|
+
// mistaken for a term in that sum.
|
|
129
|
+
const blockingWired = blocking.length;
|
|
130
|
+
return {
|
|
131
|
+
summary: {
|
|
132
|
+
armed: all.length,
|
|
133
|
+
blocking: blockingWired, // wired entries that can refuse — same unit as `armed`
|
|
134
|
+
advisory: all.length - blockingWired,
|
|
135
|
+
blockingDistinct: uniqueBlocking, // distinct gates that can refuse; ≤ blocking when wired twice
|
|
136
|
+
duplicated, // wired twice; runs twice
|
|
137
|
+
caughtTotal: blocks.length,
|
|
138
|
+
caughtThisWeek: recent.length,
|
|
139
|
+
everRecorded: blocks.length > 0,
|
|
140
|
+
},
|
|
141
|
+
gates: all.sort((a, b) => Number(b.blocking) - Number(a.blocking)),
|
|
142
|
+
// Newest first — the most recent catch is the one worth reading.
|
|
143
|
+
catches: blocks.slice(-12).reverse(),
|
|
144
|
+
byGate: Object.fromEntries(Object.entries(byGate).map(([k, v]) => [k, v.length])),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gen-console-images.mjs — imagery for the Onboarding Console. Same proven path as gen-images.mjs
|
|
3
|
+
// (OpenAI gpt-image-1, fallback dall-e-3), same brand style, output into console/assets/.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const OUT = path.join(ROOT, 'console/assets');
|
|
10
|
+
fs.mkdirSync(OUT, { recursive: true });
|
|
11
|
+
|
|
12
|
+
// Load the key the same way gen-images.mjs does, but tolerate a few env-file locations.
|
|
13
|
+
function loadKey() {
|
|
14
|
+
if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY;
|
|
15
|
+
const candidates = [process.env.RUVNET_ENV_FILE, path.join(ROOT, '.env'), path.join(process.env.HOME, 'Code/Ask-Ruvnet/.env')].filter(Boolean);
|
|
16
|
+
for (const f of candidates) { try { const m = fs.readFileSync(f, 'utf8').match(/^OPENAI_API_KEY=(.+)$/m); if (m) return m[1]; } catch { /* next */ } }
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
const KEY = (loadKey().match(/sk-[A-Za-z0-9_\-]+/) || [''])[0];
|
|
20
|
+
if (!KEY) { console.error('no OPENAI_API_KEY (env or .env)'); process.exit(2); }
|
|
21
|
+
const redact = (s) => String(s).replace(/sk-[A-Za-z0-9_\-]+/g, 'sk-***');
|
|
22
|
+
|
|
23
|
+
const STYLE = ' — Style: deep near-black background (#0a0c10), sophisticated premium editorial-tech aesthetic, restrained warm amber (#f0a830) and warm gold (#ffce6a) glow with cool cyan (#5ad6ff) and calm green (#5fd38a) accents only, cinematic soft volumetric light, generous negative space, refined, calming, screenshot-worthy, high craft. Absolutely NO text, NO words, NO letters, NO numbers, NO UI chrome, NO logos, NO circuit-board cliché.';
|
|
24
|
+
|
|
25
|
+
const IMAGES = [
|
|
26
|
+
{ slug: 'hero', size: '1536x1024', p: 'A warm amber intelligence gently understanding a computer: soft glowing amber and gold neural filaments and threads of light weaving and resolving out of a faint tangle on the left into an elegant, orderly, translucent crystalline lattice of floating glass panels and cards on the right — the feeling of messy machine settings being calmly brought into clear, beautiful order. Lots of soft dark negative space on the right for text.' },
|
|
27
|
+
{ slug: 'memory', size: '1024x1024', p: 'A single luminous softly-glowing sphere of warm amber and cyan light, made of countless fine interwoven filaments, holding its shape calmly in dark space — an abstract emblem of a mind that remembers; serene, alive, precise.' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
async function gen(model, prompt, size) {
|
|
31
|
+
const body = model === 'gpt-image-1'
|
|
32
|
+
? { model, prompt, size, quality: 'high', n: 1 }
|
|
33
|
+
: { model, prompt, size: size === '1536x1024' ? '1792x1024' : '1024x1024', response_format: 'b64_json', n: 1 };
|
|
34
|
+
const r = await fetch('https://api.openai.com/v1/images/generations', {
|
|
35
|
+
method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
if (!r.ok) throw new Error(`${model} HTTP ${r.status}: ${(await r.text()).slice(0, 160)}`);
|
|
38
|
+
return (await r.json()).data[0].b64_json;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const only = process.argv.slice(2).filter((a) => !a.startsWith('-'));
|
|
42
|
+
for (const im of IMAGES) {
|
|
43
|
+
if (only.length && !only.includes(im.slug)) continue;
|
|
44
|
+
const prompt = im.p + STYLE;
|
|
45
|
+
let b64;
|
|
46
|
+
try { b64 = await gen('gpt-image-1', prompt, im.size); console.log(`✓ ${im.slug} (gpt-image-1)`); }
|
|
47
|
+
catch (e) {
|
|
48
|
+
console.log(` gpt-image-1 failed for ${im.slug}: ${redact(e.message)} — trying dall-e-3`);
|
|
49
|
+
try { b64 = await gen('dall-e-3', prompt, im.size); console.log(`✓ ${im.slug} (dall-e-3)`); }
|
|
50
|
+
catch (e2) { console.error(`✗ ${im.slug}: ${redact(e2.message)}`); continue; }
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(path.join(OUT, `${im.slug}.png`), Buffer.from(b64, 'base64'));
|
|
53
|
+
}
|
|
54
|
+
console.log('done →', OUT);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gen-images.mjs — generate a cohesive set of explainer images (OpenAI gpt-image-1, fallback dall-e-3).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const OUT = path.join(ROOT, 'explainer/assets/img');
|
|
8
|
+
fs.mkdirSync(OUT, { recursive: true });
|
|
9
|
+
const RAWKEY = process.env.OPENAI_API_KEY || (fs.readFileSync((process.env.RUVNET_ENV_FILE || '.env'), 'utf8').match(/^OPENAI_API_KEY=(.+)$/m) || [])[1] || '';
|
|
10
|
+
const KEY = (RAWKEY.match(/sk-[A-Za-z0-9_\-]+/) || [''])[0]; // first valid token only — handles dup lines / whitespace
|
|
11
|
+
if (!KEY) { console.error('no OPENAI_API_KEY'); process.exit(2); }
|
|
12
|
+
const redact = (s) => String(s).replace(/sk-[A-Za-z0-9_\-]+/g, 'sk-***'); // never let a key reach logs
|
|
13
|
+
|
|
14
|
+
const STYLE = ' — Style: dark near-black background (#0b0d0f) with a faint blueprint grid, sophisticated premium editorial-tech aesthetic, restrained warm amber (#f0a830) and cool cyan (#5ad6ff) accent light only, cinematic volumetric glow, fine detail, high craft, elegant and confident. Absolutely NO text, NO words, NO letters, NO numbers, NO UI chrome, NO logos.';
|
|
15
|
+
|
|
16
|
+
const IMAGES = [
|
|
17
|
+
{ slug: 'hero', size: '1536x1024', p: 'A luminous intricate three-dimensional structure resembling a brain fused with a vast interconnected codebase: thousands of glowing amber and cyan filaments forming one elegant organized sphere of intelligence, floating in dark space, a sense of all knowledge made orderly and alive.' },
|
|
18
|
+
{ slug: 'problem-skim', size: '1536x1024', p: 'A vast deep canyon made of densely stacked layers of code and documents descending far into darkness; a single small fragile light hovers at the very top only grazing the surface, never reaching the immense depth below. The feeling of skimming and missing everything underneath.' },
|
|
19
|
+
{ slug: 'point-deeper', size: '1536x1024', p: 'One precise clean beam of warm amber light cutting straight down through many deep translucent strata of a vast structure to perfectly illuminate a single exact point far below; surgical precision locating the one true answer in the depths.' },
|
|
20
|
+
{ slug: 'architecture', size: '1536x1024', p: 'An elegant isometric exploded view of five translucent glass layers floating one above another in dark space, each a slightly different luminous tone, joined by thin vertical conduits of light; a refined premium product render of a clean layered system.' },
|
|
21
|
+
{ slug: 'proof', size: '1536x1024', p: 'Three distinct elegant luminous measuring instruments aim converging beams of light onto a single crystalline object at center that glows confident green, while one beam exposes a hidden flaw glowing warning red; independent rigorous verification against a single source of truth.' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async function gen(model, prompt, size) {
|
|
25
|
+
const body = model === 'gpt-image-1'
|
|
26
|
+
? { model, prompt, size, quality: 'high', n: 1 }
|
|
27
|
+
: { model, prompt, size: size === '1536x1024' ? '1792x1024' : '1024x1024', response_format: 'b64_json', n: 1 };
|
|
28
|
+
const r = await fetch('https://api.openai.com/v1/images/generations', {
|
|
29
|
+
method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
30
|
+
});
|
|
31
|
+
if (!r.ok) throw new Error(`${model} HTTP ${r.status}: ${(await r.text()).slice(0, 160)}`);
|
|
32
|
+
const j = await r.json();
|
|
33
|
+
return j.data[0].b64_json;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const im of IMAGES) {
|
|
37
|
+
const prompt = im.p + STYLE;
|
|
38
|
+
let b64;
|
|
39
|
+
try { b64 = await gen('gpt-image-1', prompt, im.size); console.log(`✓ ${im.slug} (gpt-image-1)`); }
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.log(` gpt-image-1 failed for ${im.slug}: ${redact(e.message)} — trying dall-e-3`);
|
|
42
|
+
try { b64 = await gen('dall-e-3', prompt, im.size); console.log(`✓ ${im.slug} (dall-e-3)`); }
|
|
43
|
+
catch (e2) { console.error(`✗ ${im.slug}: ${redact(e2.message)}`); continue; }
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(path.join(OUT, `${im.slug}.png`), Buffer.from(b64, 'base64'));
|
|
46
|
+
}
|
|
47
|
+
console.log('done →', OUT);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
function submodulePaths(repoDir) {
|
|
6
|
+
const gitmodules = path.join(repoDir, '.gitmodules');
|
|
7
|
+
if (!fs.existsSync(gitmodules)) return [];
|
|
8
|
+
let output = '';
|
|
9
|
+
try {
|
|
10
|
+
output = execFileSync(
|
|
11
|
+
'git',
|
|
12
|
+
['config', '-f', gitmodules, '--get-regexp', '^submodule\\..*\\.path$'],
|
|
13
|
+
{ encoding: 'utf8' },
|
|
14
|
+
);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.status !== 1) throw error;
|
|
17
|
+
}
|
|
18
|
+
return output.trim().split('\n').filter(Boolean).map((line) => line.replace(/^\S+\s+/, ''));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function withSubmoduleSymlinksDetached(repoDir, operation) {
|
|
22
|
+
const detached = [];
|
|
23
|
+
for (const relativePath of submodulePaths(repoDir)) {
|
|
24
|
+
const absolutePath = path.join(repoDir, relativePath);
|
|
25
|
+
let stat;
|
|
26
|
+
try {
|
|
27
|
+
stat = fs.lstatSync(absolutePath);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT') continue;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (!stat.isSymbolicLink()) continue;
|
|
33
|
+
detached.push({ absolutePath, target: fs.readlinkSync(absolutePath) });
|
|
34
|
+
fs.unlinkSync(absolutePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return operation();
|
|
39
|
+
} finally {
|
|
40
|
+
for (const { absolutePath, target } of detached) {
|
|
41
|
+
if (fs.existsSync(absolutePath)) {
|
|
42
|
+
const stat = fs.lstatSync(absolutePath);
|
|
43
|
+
if (!stat.isDirectory() || fs.readdirSync(absolutePath).length > 0) {
|
|
44
|
+
throw new Error(`cannot restore submodule symlink over non-empty path: ${absolutePath}`);
|
|
45
|
+
}
|
|
46
|
+
fs.rmdirSync(absolutePath);
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
49
|
+
fs.symlinkSync(target, absolutePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# RuvNet Brain — pre-push gate. NON-OPTIONAL. Runs on every `git push`.
|
|
3
|
+
#
|
|
4
|
+
# WHY (2026-07-17): a real client (Jan) hit npx ruvnet-brain@latest with a stale package whose
|
|
5
|
+
# self-update manifest 404'd. The root cause wasn't the bug — it was that the CHECK was optional
|
|
6
|
+
# and I skipped it. This hook removes the discretion: it refuses the push if the version surfaces
|
|
7
|
+
# disagree or the self-update manifest 404s. It does not care whether I remembered, or was in a
|
|
8
|
+
# hurry, or "was pretty sure it was fine." Pass or the push does not happen.
|
|
9
|
+
#
|
|
10
|
+
# It blocks ONLY on what is knowable before a push. npm-latest and the live explainer are verified
|
|
11
|
+
# post-publish by `node scripts/release.mjs --publish` (the only sanctioned ship path).
|
|
12
|
+
#
|
|
13
|
+
# Escape hatch is `git push --no-verify` — but if you use it, you are consciously overriding a gate
|
|
14
|
+
# that exists because of a specific failure that cost real user trust. Own that out loud.
|
|
15
|
+
#
|
|
16
|
+
# SECRET SCAN (2026-07-21): a buggy diagnostic echoed live OPENAI and OPENROUTER keys into a
|
|
17
|
+
# terminal, and AgentDB's session autocapture wrote them to .swarm/. Nothing reached git — but the
|
|
18
|
+
# only thing that stopped it was a GLOBAL gitignore that does not travel to a clone or another
|
|
19
|
+
# machine. Relying on "the file happened to be ignored" is not a control. This is: a real key
|
|
20
|
+
# pattern in anything being pushed refuses the push. Placeholders (your-key, abc123, xxxx,
|
|
21
|
+
# deadbeef, example) are excluded, because rUv's own docs are full of them and a gate that cries
|
|
22
|
+
# wolf on documentation is a gate people learn to bypass.
|
|
23
|
+
#
|
|
24
|
+
# SCOPE FIX (2026-07-22): this scanned `git diff --cached HEAD` — the STAGED INDEX. But a push ships
|
|
25
|
+
# COMMITS, and at push time the index is normally clean, so the scan read nothing. Proven both ways
|
|
26
|
+
# in a scratch repo: a key COMMITTED then pushed sailed through, while the same key merely STAGED was
|
|
27
|
+
# blocked. The gate could only catch the one case that never happens. It is the same defect class the
|
|
28
|
+
# 7e715bb commit fixed in the *test* — a check that cannot fail in the situation it was written for —
|
|
29
|
+
# which survived here in the *scan*. Now it reads the actual push range from the hook's stdin.
|
|
30
|
+
RANGE_DIFF=""
|
|
31
|
+
CURRENCY_BASE=""
|
|
32
|
+
while read -r _local_ref local_sha _remote_ref remote_sha; do
|
|
33
|
+
[ -z "$local_sha" ] && continue
|
|
34
|
+
case "$local_sha" in *[!0]*) ;; *) continue ;; esac # all-zeros = branch deletion, nothing to scan
|
|
35
|
+
case "$remote_sha" in
|
|
36
|
+
""|*[!0-9a-f]*|0000000000000000000000000000000000000000) # new branch: scan its whole history vs other refs
|
|
37
|
+
RANGE_DIFF="$RANGE_DIFF$(git diff --no-color -U0 "$(git merge-base HEAD origin/main 2>/dev/null || echo '')" "$local_sha" 2>/dev/null)"
|
|
38
|
+
# ADR-056 §5: a new branch has no remote counterpart, so the merge-base with main is the only
|
|
39
|
+
# honest base. If even that cannot resolve, CURRENCY_BASE stays empty and the currency check is
|
|
40
|
+
# SKIPPED rather than run against a guess — never a scope invented from nothing.
|
|
41
|
+
[ -z "$CURRENCY_BASE" ] && CURRENCY_BASE="$(git merge-base HEAD origin/main 2>/dev/null || true)" ;;
|
|
42
|
+
*)
|
|
43
|
+
RANGE_DIFF="$RANGE_DIFF$(git diff --no-color -U0 "$remote_sha" "$local_sha" 2>/dev/null)"
|
|
44
|
+
# ADR-056: the remote tip is only a valid "what did I change" base when it is an ANCESTOR of
|
|
45
|
+
# what we are pushing. After a rebase or squash it is NOT — the range then spans the entire
|
|
46
|
+
# rewrite and drags in every document the upstream branch already carried, red ones included.
|
|
47
|
+
# Measured 2026-07-27: a force-push after rebasing onto main was refused for ADR-054 and
|
|
48
|
+
# ADR-055 — two documents on main that this branch never touched. GPT-5.6 predicted exactly
|
|
49
|
+
# this in the ADR-056 duel ("rebase or squash changes the touched-file set without changing
|
|
50
|
+
# whether a violation is genuinely new"). So: ancestor => use it; otherwise fall back to the
|
|
51
|
+
# merge-base with main, which is the honest answer to "what is mine".
|
|
52
|
+
if [ -z "$CURRENCY_BASE" ]; then
|
|
53
|
+
if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
|
|
54
|
+
CURRENCY_BASE="$remote_sha"
|
|
55
|
+
else
|
|
56
|
+
CURRENCY_BASE="$(git merge-base "$local_sha" origin/main 2>/dev/null || true)"
|
|
57
|
+
fi
|
|
58
|
+
fi ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
# Fall back to the staged index too, so a pre-commit-style invocation still gets covered.
|
|
62
|
+
RANGE_DIFF="$RANGE_DIFF
|
|
63
|
+
$(git diff --cached --no-color -U0 HEAD 2>/dev/null)"
|
|
64
|
+
|
|
65
|
+
SECRET_HITS=$(printf '%s' "$RANGE_DIFF" | grep -E "^\+" \
|
|
66
|
+
| grep -oE "sk-(proj|ant|or-v1)-[A-Za-z0-9_-]{20,}" \
|
|
67
|
+
| grep -vEi "your-key|abc123|xxxx+|deadbeef|example|placeholder|REDACTED" | head -3)
|
|
68
|
+
if [ -n "$SECRET_HITS" ]; then
|
|
69
|
+
echo ""
|
|
70
|
+
echo " ⛔ PUSH BLOCKED — what looks like a REAL API key is in this push."
|
|
71
|
+
echo " matched (prefix only): $(echo "$SECRET_HITS" | cut -c1-14 | tr '\n' ' ')"
|
|
72
|
+
echo " Remove it, rotate the key, and push again. Never --no-verify past this one:"
|
|
73
|
+
echo " a key that reaches a remote is compromised the moment it lands."
|
|
74
|
+
echo ""
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Hooks may be shared by several git worktrees. Resolving from this script's own path is wrong when
|
|
79
|
+
# core.hooksPath names another checkout: the hook then validates that checkout while pushing the
|
|
80
|
+
# current one (measured 2026-07-28: announced 3.9.128, pushed 3.9.129). Git already knows the
|
|
81
|
+
# worktree whose refs are being pushed, so derive the shipping root from that live repository.
|
|
82
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
83
|
+
if [ -z "$ROOT" ] || [ ! -f "$ROOT/scripts/verify-channels.mjs" ]; then
|
|
84
|
+
echo " ⛔ pre-push gate cannot resolve the worktree being pushed."
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
node "$ROOT/scripts/verify-channels.mjs" --pre-push
|
|
88
|
+
status=$?
|
|
89
|
+
if [ "$status" -ne 0 ]; then
|
|
90
|
+
echo ""
|
|
91
|
+
echo " ⛔ pre-push gate refused this push. A user on some path would get a broken build."
|
|
92
|
+
echo " Fix the channel(s) above, then push again. Do not route around this."
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── DOCUMENT CURRENCY (ADR-056 §5) ─────────────────────────────────────────────────────────────────
|
|
97
|
+
# The tool was written and tested on 2026-07-22 and then invoked by NOTHING for five days — absent
|
|
98
|
+
# from this hook, gate.sh, gates.mjs and every workflow, while `wired-check` reported it "wired"
|
|
99
|
+
# because package.json DEFINED an npm alias for it. This line is the whole fix.
|
|
100
|
+
#
|
|
101
|
+
# NO RATCHET, DELIBERATELY. The first draft of ADR-056 proposed a stateful new-vs-pre-existing
|
|
102
|
+
# violation tracker so this gate could coexist with 32 pre-existing BLOCKs forever. Two independent
|
|
103
|
+
# adversarial reviews killed it: a committed baseline is gamed by the same commit that adds the
|
|
104
|
+
# violation, re-deriving at the remote SHA needs a second worktree, and "report the rest forever"
|
|
105
|
+
# is the wolf-cry doc-currency.mjs's own header names as its designed failure mode. The debt was
|
|
106
|
+
# PAID instead (32 BLOCKs -> 4, all four genuine re-reads), which makes the plain gate safe.
|
|
107
|
+
#
|
|
108
|
+
# SCOPED to what this push touches. A pre-existing red document must not block someone who merely
|
|
109
|
+
# walked past it — that teaches people never to touch the red documents, preserving exactly the drift
|
|
110
|
+
# this exists to end. Scope is empty => nothing to check => pass; never a scope invented from nothing.
|
|
111
|
+
if [ -n "$CURRENCY_BASE" ]; then
|
|
112
|
+
node "$ROOT/scripts/doc-currency.mjs" --check --changed "$CURRENCY_BASE"
|
|
113
|
+
dc_status=$?
|
|
114
|
+
if [ "$dc_status" -ne 0 ]; then
|
|
115
|
+
echo ""
|
|
116
|
+
echo " ⛔ pre-push gate refused this push — a document you touched disagrees with its code."
|
|
117
|
+
echo " An ADR that describes a world the code left is worse than no ADR."
|
|
118
|
+
echo " \`node scripts/doc-currency.mjs --fix\` backfills every date git can prove;"
|
|
119
|
+
echo " everything else is a claim only you can make."
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
echo ""
|
|
124
|
+
echo " ✓ pre-push gate passed — version, channels, secrets, and document currency are clean."
|
|
125
|
+
echo ""
|
|
126
|
+
exit 0
|