ruvnet-brain 4.0.28 โ 4.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/install.mjs +27 -6
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/capability-registry.mjs +20 -4
- package/plugin/scripts/update-apply.mjs +27 -2
- package/scripts/github-health-watch.mjs +126 -0
- package/scripts/metaharness-receipts.mjs +26 -0
- package/scripts/nightly-wrapper.sh +12 -0
- package/scripts/onboarding-console.mjs +36 -2
- package/scripts/release-transaction.mjs +17 -1
- package/scripts/route-cheap.mjs +14 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# ๐ง RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### ๐ง RuvNet Brain โ [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
8
8
|
|
|
9
9
|
**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.**
|
|
10
10
|
|
package/bin/install.mjs
CHANGED
|
@@ -25,6 +25,24 @@ import {
|
|
|
25
25
|
missingEmbedderModels,
|
|
26
26
|
} from '../kb/model-requirements.mjs';
|
|
27
27
|
import { applyManagedCatalogUpdate } from '../scripts/model-router-catalog.mjs';
|
|
28
|
+
import { cmpVersion } from '../scripts/stack-sync.mjs';
|
|
29
|
+
|
|
30
|
+
// ISSUE #123 โ AN INSTALL THAT IS AHEAD OF THE PUBLISHED RELEASE IS NOT A BROKEN INSTALL.
|
|
31
|
+
// Host convergence compared installed === PACKAGE_VERSION, so 4.0.29-dev against a published
|
|
32
|
+
// 4.0.28 read EXACTLY like being behind: --update exited 1 with 'host synchronization is
|
|
33
|
+
// incomplete' and --doctor printed 'โ FAILING' while every displayed check was green (121 repos
|
|
34
|
+
// indexed, grounding proven, Codex MCP live, 17 hooks enabled). The prescribed repair โ
|
|
35
|
+
// reinstalling the plugin extras โ was a no-op, because the extras were present and firing.
|
|
36
|
+
// Telling someone their working install is broken, then handing them a fix that changes nothing,
|
|
37
|
+
// is the worst kind of false alarm: it burns trust AND time.
|
|
38
|
+
// Convergence means NOT BEHIND. Ordering comes from cmpVersion, which is prerelease-aware and is
|
|
39
|
+
// documented as the only place ordering is decided in this system.
|
|
40
|
+
const versionSatisfies = (installed, expected) => {
|
|
41
|
+
if (!expected) return true;
|
|
42
|
+
if (!installed) return false;
|
|
43
|
+
if (installed === expected) return true;
|
|
44
|
+
try { return cmpVersion(installed, expected) >= 0; } catch { return installed === expected; }
|
|
45
|
+
};
|
|
28
46
|
import {
|
|
29
47
|
CONSOLE_RUNTIME_SURFACE, CONSOLE_RUNTIME_IDENTITY_FILE, consoleRuntimeDigest,
|
|
30
48
|
} from '../scripts/console-runtime-identity.mjs';
|
|
@@ -847,7 +865,7 @@ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false
|
|
|
847
865
|
// code says the command ran, not that the plugin is usable; the commands either exist on disk or
|
|
848
866
|
// they do not. This is the difference between `/rvbc` working and "Unknown command: /rvbc".
|
|
849
867
|
const installed = claudePluginStatus();
|
|
850
|
-
if (installed.installed && (
|
|
868
|
+
if (installed.installed && versionSatisfies(installed.version, expectedVersion)) {
|
|
851
869
|
ok(`plugin installed at user scope (global, alongside Ruflo / RuVector) โ exact version ${installed.version}`);
|
|
852
870
|
info(` commands available after a restart: ${c.bold('/rvbc')}, ${c.bold('/ruvnet-brain:configure')}`);
|
|
853
871
|
return { host: true, wired: true, version: installed.version, manualMarketplace, manualInstall };
|
|
@@ -855,7 +873,7 @@ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false
|
|
|
855
873
|
|
|
856
874
|
// The honest failure. The brain still WORKS โ this is the difference between a broken install and
|
|
857
875
|
// a partial one, and the user is told exactly which they have instead of being congratulated.
|
|
858
|
-
const mismatch = installed.installed &&
|
|
876
|
+
const mismatch = installed.installed && !versionSatisfies(installed.version, expectedVersion);
|
|
859
877
|
warn(mismatch
|
|
860
878
|
? `Claude installed ${installed.version || 'an unknown version'}, not required ${expectedVersion}; refusing to call this host converged.`
|
|
861
879
|
: `the plugin did NOT land โ so slash commands like ${c.bold('/rvbc')} will not exist yet.`);
|
|
@@ -1259,7 +1277,7 @@ export function wireCodexPlugin({
|
|
|
1259
1277
|
if (announce) warn(`Codex Brain plugin is installed but disabled by user or policy โ left disabled (${CODEX_PLUGIN_ID}).`);
|
|
1260
1278
|
return { host: true, action: 'disabled', ...before };
|
|
1261
1279
|
}
|
|
1262
|
-
if (before.installed && before.enabled && (
|
|
1280
|
+
if (before.installed && before.enabled && versionSatisfies(before.version, expectedVersion)) {
|
|
1263
1281
|
if (announce) ok(`Codex Brain plugin already installed and enabled (${before.version || 'version unknown'}) โ no changes.`);
|
|
1264
1282
|
return { host: true, action: 'unchanged', ...before };
|
|
1265
1283
|
}
|
|
@@ -1297,7 +1315,7 @@ export function wireCodexPlugin({
|
|
|
1297
1315
|
return { host: true, action: 'plugin-failed', error: added.error };
|
|
1298
1316
|
}
|
|
1299
1317
|
const after = codexPluginStatus({ ...options, runJson });
|
|
1300
|
-
if (!after.installed || !after.enabled || (
|
|
1318
|
+
if (!after.installed || !after.enabled || !versionSatisfies(after.version, expectedVersion)) {
|
|
1301
1319
|
if (announce) warn('Codex accepted the install command but the Brain plugin is not installed and enabled.');
|
|
1302
1320
|
return { host: true, action: 'verification-failed', expectedVersion, ...after };
|
|
1303
1321
|
}
|
|
@@ -2363,12 +2381,15 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
2363
2381
|
}
|
|
2364
2382
|
|
|
2365
2383
|
export function classifyHostConvergence(receipt, expectedVersion = PACKAGE_VERSION) {
|
|
2366
|
-
|
|
2384
|
+
// Same 'ahead is not behind' rule as host convergence above (#123). A receipt written by a
|
|
2385
|
+
// NEWER generation than the installer currently running is not a version mismatch โ it is a
|
|
2386
|
+
// machine that is further along. Only a receipt for an OLDER generation is stale.
|
|
2387
|
+
if (!receipt || !versionSatisfies(receipt.desiredVersion, expectedVersion)) {
|
|
2367
2388
|
return { healthy: false, state: 'version-mismatch', action: `required version ${expectedVersion}` };
|
|
2368
2389
|
}
|
|
2369
2390
|
const hostStates = Object.values(receipt.hosts || {});
|
|
2370
2391
|
const badHost = hostStates.find((host) => !['ready', 'disabled', 'absent'].includes(host?.state)
|
|
2371
|
-
|| (host.state === 'ready' && host.version
|
|
2392
|
+
|| (host.state === 'ready' && !versionSatisfies(host.version, expectedVersion)));
|
|
2372
2393
|
if (badHost) return { healthy: false, state: 'host-pending', action: 're-run host synchronization' };
|
|
2373
2394
|
if (receipt.consoleRuntime?.state !== 'ready') {
|
|
2374
2395
|
return { healthy: false, state: receipt.consoleRuntime?.state || 'console-unproven', action: 'restart Console, then re-run --doctor' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.35",
|
|
4
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": {
|
|
@@ -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 71 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 an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.0.35",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -709,11 +709,27 @@ export const CAPABILITIES = [
|
|
|
709
709
|
|
|
710
710
|
const S = helpers.lessonStore.STATUS || {};
|
|
711
711
|
const inForce = lessons.filter((l) => l?.status === S.RATIFIED || l?.status === S.ACTIVE).length;
|
|
712
|
-
|
|
713
|
-
//
|
|
714
|
-
//
|
|
712
|
+
// AWAITING YOU means RATIFIABLE BY YOU (issue #125). This counted every CANDIDATE, including
|
|
713
|
+
// the seeded imported-owner lessons โ which `ratify()` refuses by design and `pending()`
|
|
714
|
+
// excludes, so `lesson-ratify --list` correctly reported "0 awaiting your decision" while this
|
|
715
|
+
// card said 12 were awaiting ratification. Two surfaces, two answers, and the card's was the
|
|
716
|
+
// one asking the user to act on something that cannot be acted on. The quarantine is right;
|
|
717
|
+
// describing it as a pending decision is not.
|
|
718
|
+
//
|
|
719
|
+
// Delegated to the store's own pending(), rather than re-deriving the predicate here โ a
|
|
720
|
+
// second copy is exactly how these two numbers drifted apart in the first place.
|
|
721
|
+
const ratifiable = typeof helpers.lessonStore.pending === 'function'
|
|
722
|
+
? helpers.lessonStore.pending(lessons).length
|
|
723
|
+
: lessons.filter((l) => l?.status === S.CANDIDATE && !l?.demoted).length;
|
|
724
|
+
const quarantined = lessons.filter((l) => l?.status === S.CANDIDATE).length - ratifiable;
|
|
715
725
|
if (inForce > 0) return row(STATE.ON, `${inForce} of ${lessons.length} lessons are ratified and can affect what your AI does`);
|
|
716
|
-
|
|
726
|
+
if (ratifiable > 0) {
|
|
727
|
+
return row(STATE.OFF, `${ratifiable} recorded lesson(s) are candidates awaiting your ratification โ none of them can influence anything yet`);
|
|
728
|
+
}
|
|
729
|
+
// Nothing is actually waiting on the user. Say what IS true instead of inventing a decision.
|
|
730
|
+
return row(STATE.OFF, quarantined > 0
|
|
731
|
+
? `${quarantined} bundled maintainer lesson(s) are quarantined and cannot be ratified โ nothing is awaiting your decision`
|
|
732
|
+
: 'no lessons are in force yet, and none are awaiting your decision');
|
|
717
733
|
},
|
|
718
734
|
},
|
|
719
735
|
|
|
@@ -316,10 +316,14 @@ function gc() {
|
|
|
316
316
|
console.log(`gc: kept ${[...keep].join(', ') || '(none)'} ยท removed ${removed}`);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// Version ordering, hoisted to module scope so BOTH the exact payload selector and the
|
|
320
|
+
// already-converged check below can use one comparator (#126). Two copies is how the
|
|
321
|
+
// selection rule and the convergence rule would drift apart.
|
|
322
|
+
const semverish = (v) => String(v).split(/[.-]/).map((x) => (Number.isFinite(+x) ? +x : x));
|
|
323
|
+
const cmp = (a, b) => { const A = semverish(a), B = semverish(b); for (let i = 0; i < Math.max(A.length, B.length); i++) { if ((A[i] ?? 0) === (B[i] ?? 0)) continue; if (typeof A[i] === 'number' && typeof B[i] === 'number') return A[i] - B[i]; return String(A[i] ?? '') < String(B[i] ?? '') ? -1 : 1; } return 0; };
|
|
324
|
+
|
|
319
325
|
// โโ sources โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
320
326
|
function newestStagedCC(expectedVersion = null) {
|
|
321
|
-
const semverish = (v) => v.split(/[.-]/).map((x) => (Number.isFinite(+x) ? +x : x));
|
|
322
|
-
const cmp = (a, b) => { const A = semverish(a), B = semverish(b); for (let i = 0; i < Math.max(A.length, B.length); i++) { if ((A[i] ?? 0) === (B[i] ?? 0)) continue; if (typeof A[i] === 'number' && typeof B[i] === 'number') return A[i] - B[i]; return String(A[i] ?? '') < String(B[i] ?? '') ? -1 : 1; } return 0; };
|
|
323
327
|
const candidates = [];
|
|
324
328
|
for (const cache of PLUGIN_CACHES) {
|
|
325
329
|
if (!fs.existsSync(cache)) continue;
|
|
@@ -400,6 +404,27 @@ function main() {
|
|
|
400
404
|
const active = readJSON(ACTIVE);
|
|
401
405
|
if (!staged) {
|
|
402
406
|
if (expectedVersion) {
|
|
407
|
+
// ALREADY AHEAD IS ALREADY CONVERGED (issue #126) โ but the SELECTION above stays exact.
|
|
408
|
+
//
|
|
409
|
+
// Issue #64 requires that we never apply "a different newer payload in another host cache",
|
|
410
|
+
// and that requirement is right: the caches are per-host, so grabbing a newer payload from
|
|
411
|
+
// .codex to satisfy a .claude request installs something nobody asked for. My first attempt
|
|
412
|
+
// at #126 loosened newestStagedCC() to "newest >= expected" and reintroduced exactly that
|
|
413
|
+
// bug โ tests/qe/release/issue-64-host-convergence.test.mjs caught it.
|
|
414
|
+
//
|
|
415
|
+
// The real defect is one layer up: after a release the plugin is bumped to the next -dev
|
|
416
|
+
// generation, so the ACTIVE host is routinely ahead of the published version the updater
|
|
417
|
+
// requests. No payload matched, this returned 1, host sync reported incomplete, and the
|
|
418
|
+
// Console runtime refresh inside that sync was rolled back โ so the header's
|
|
419
|
+
// "โณ update available โ click to update" ran clean and changed nothing, forever.
|
|
420
|
+
//
|
|
421
|
+
// Nothing to apply because you are already past it is SUCCESS, not failure. This asserts
|
|
422
|
+
// that about the ACTIVE spine only; it never selects an unrequested payload.
|
|
423
|
+
const activeNow = readJSON(ACTIVE);
|
|
424
|
+
if (activeNow?.version && cmp(activeNow.version, expectedVersion) >= 0) {
|
|
425
|
+
console.log(`already on ${activeNow.version}, at or above requested ${expectedVersion} โ nothing to apply.`);
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
403
428
|
console.error(`โ no staged host payload exactly matches expected version ${expectedVersion} โ spine unchanged`);
|
|
404
429
|
return 1;
|
|
405
430
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GITHUB HEALTH WATCH โ one place that notices GitHub is unhappy, before the maintainer does.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. On 2026-08-08 the maintainer said: "You should never ever let it be in a
|
|
6
|
+
* situation where it's got failed pushes. Your job is to always be looking out for GitHub to make
|
|
7
|
+
* sure if there's any problem that you're on it immediately." They were right, and the reason it
|
|
8
|
+
* kept happening is structural, not attentional: every existing signal watched ONE thing.
|
|
9
|
+
*
|
|
10
|
+
* ยท signal-watch โ CI verdicts, per push
|
|
11
|
+
* ยท issue-watch โ open issues and their SLA
|
|
12
|
+
* ยท published-surface โ npm vs GitHub, nightly
|
|
13
|
+
*
|
|
14
|
+
* Nothing watched the states that actually stall work: a red default branch, a PR that has silently
|
|
15
|
+
* gone CONFLICTING, a branch left behind after a merge, a stuck queue, or a release that published
|
|
16
|
+
* and then failed its own bookkeeping. Each was found by a human noticing something felt wrong.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT DOES NOT DO. It does not push, merge, close, publish, or edit anything. It REPORTS. A
|
|
19
|
+
* watcher that also acts is a watcher whose failures are hard to reason about, and this repo has
|
|
20
|
+
* already been bitten by automation that satisfied its own success predicate (ADR-050). Exit code
|
|
21
|
+
* is the contract: 0 = healthy, 1 = something needs attention, and the reasons print in full.
|
|
22
|
+
*
|
|
23
|
+
* node scripts/github-health-watch.mjs # human-readable
|
|
24
|
+
* node scripts/github-health-watch.mjs --json # machine-readable, for hooks/CI
|
|
25
|
+
*/
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
|
|
28
|
+
const REPO = 'stuinfla/ruvnet-brain';
|
|
29
|
+
const JSON_OUT = process.argv.includes('--json');
|
|
30
|
+
const findings = [];
|
|
31
|
+
const note = (level, area, detail, action) => findings.push({ level, area, detail, action });
|
|
32
|
+
|
|
33
|
+
const gh = (args) => {
|
|
34
|
+
try { return execFileSync('gh', args, { encoding: 'utf8', timeout: 60_000 }).trim(); }
|
|
35
|
+
catch (error) { return { __error: String(error.stderr || error.message).slice(0, 200) }; }
|
|
36
|
+
};
|
|
37
|
+
const ghJson = (args) => {
|
|
38
|
+
const out = gh(args);
|
|
39
|
+
if (out && out.__error) return out;
|
|
40
|
+
try { return JSON.parse(out); } catch { return { __error: 'unparseable gh output' }; }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// โโ 1. Is the default branch green? A red main blocks every release and every merge. โโโโโโโโโโโโโ
|
|
44
|
+
const runs = ghJson(['run', 'list', '--repo', REPO, '--branch', 'main', '--workflow', 'ci',
|
|
45
|
+
'--limit', '1', '--json', 'status,conclusion,headSha,url']);
|
|
46
|
+
if (runs.__error) note('warn', 'ci', `could not read CI status: ${runs.__error}`, 'check gh auth');
|
|
47
|
+
else if (!runs.length) note('warn', 'ci', 'no CI runs found on main', 'verify the workflow is enabled');
|
|
48
|
+
else {
|
|
49
|
+
const [run] = runs;
|
|
50
|
+
if (run.status === 'completed' && run.conclusion !== 'success') {
|
|
51
|
+
note('fail', 'ci', `main is RED at ${run.headSha.slice(0, 7)} (${run.conclusion})`, `read ${run.url}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// โโ 2. Queue health. A stalled queue looks identical to slow CI until someone waits an hour. โโโโโ
|
|
56
|
+
const queued = ghJson(['run', 'list', '--repo', REPO, '--limit', '60', '--json', 'status']);
|
|
57
|
+
if (!queued.__error && Array.isArray(queued)) {
|
|
58
|
+
const q = queued.filter((r) => r.status === 'queued').length;
|
|
59
|
+
const running = queued.filter((r) => r.status === 'in_progress').length;
|
|
60
|
+
// Many queued with NOTHING executing is the shape of a stuck plane or an exhausted runner pool โ
|
|
61
|
+
// measured on 2026-08-06, when two hung notifier runs held both slots and starved 21 real runs.
|
|
62
|
+
if (running === 0 && q >= 5) {
|
|
63
|
+
note('fail', 'queue', `${q} runs queued, 0 executing โ the Actions plane may be stalled`,
|
|
64
|
+
'check githubstatus.com, then cancel any hung run holding a concurrency slot');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// โโ 3. PRs that have gone unmergeable. These rot silently; nobody is notified when main moves. โโโ
|
|
69
|
+
const prs = ghJson(['pr', 'list', '--repo', REPO, '--state', 'open', '--json',
|
|
70
|
+
'number,title,mergeable,isDraft,updatedAt']);
|
|
71
|
+
if (!prs.__error && Array.isArray(prs)) {
|
|
72
|
+
for (const pr of prs) {
|
|
73
|
+
if (pr.mergeable === 'CONFLICTING') {
|
|
74
|
+
note('fail', 'pr', `#${pr.number} is CONFLICTING โ "${pr.title.slice(0, 60)}"`,
|
|
75
|
+
'rebase or resolve against main; a conflicting PR is invisible work');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// โโ 4. Branches left behind after a merge. The maintainer's standing rule is a few, resolved fast.
|
|
81
|
+
const branches = gh(['api', `repos/${REPO}/branches?per_page=100`, '--jq', '.[].name']);
|
|
82
|
+
if (typeof branches === 'string') {
|
|
83
|
+
const list = branches.split('\n').filter(Boolean).filter((b) => b !== 'main');
|
|
84
|
+
if (list.length > 3) {
|
|
85
|
+
note('warn', 'branches', `${list.length} non-main branches: ${list.slice(0, 6).join(', ')}`,
|
|
86
|
+
'delete merged branches; resolve the rest back to main');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// โโ 5. Published surfaces naming different generations โ the whole of issue #77. โโโโโโโโโโโโโโโโโ
|
|
91
|
+
const npmLatest = (() => {
|
|
92
|
+
try { return execFileSync('npm', ['view', 'ruvnet-brain', 'dist-tags.latest'], { encoding: 'utf8', timeout: 60_000 }).trim(); }
|
|
93
|
+
catch { return null; }
|
|
94
|
+
})();
|
|
95
|
+
const release = ghJson(['api', `repos/${REPO}/releases/latest`, '--jq', '.tag_name']);
|
|
96
|
+
if (npmLatest && typeof release === 'string' && release) {
|
|
97
|
+
if (npmLatest !== release.replace(/^v/, '')) {
|
|
98
|
+
note('fail', 'surfaces', `npm ${npmLatest} != GitHub ${release}`,
|
|
99
|
+
'run scripts/published-surface-probe.mjs; this is issue #77 recurring');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// โโ 6. Issues past their response SLA. โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
104
|
+
const issues = ghJson(['issue', 'list', '--repo', REPO, '--state', 'open', '--json',
|
|
105
|
+
'number,title,createdAt,comments']);
|
|
106
|
+
if (!issues.__error && Array.isArray(issues)) {
|
|
107
|
+
for (const issue of issues) {
|
|
108
|
+
const ageH = (Date.now() - Date.parse(issue.createdAt)) / 3_600_000;
|
|
109
|
+
if (ageH > 4 && (issue.comments ?? 0) === 0) {
|
|
110
|
+
note('fail', 'issues', `#${issue.number} is ${Math.round(ageH)}h old with NO response`,
|
|
111
|
+
'respond; the 4h SLA is the promise, not the target');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const failed = findings.filter((f) => f.level === 'fail');
|
|
117
|
+
if (JSON_OUT) {
|
|
118
|
+
process.stdout.write(`${JSON.stringify({ healthy: failed.length === 0, findings }, null, 2)}\n`);
|
|
119
|
+
} else if (!findings.length) {
|
|
120
|
+
process.stdout.write('[github-health] โ clean โ main green, no conflicting PRs, surfaces coherent, no unanswered issues.\n');
|
|
121
|
+
} else {
|
|
122
|
+
for (const f of findings) {
|
|
123
|
+
process.stdout.write(`[github-health] ${f.level === 'fail' ? 'โ' : 'โ '} ${f.area}: ${f.detail}\n โ ${f.action}\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
process.exit(failed.length ? 1 : 0);
|
|
@@ -105,8 +105,34 @@ export function formatTable(rows) {
|
|
|
105
105
|
const withBar = totalFrontier > 0 ? Math.min(BAR, Math.max(1, Math.round(BAR * (totalCost / totalFrontier)))) : BAR;
|
|
106
106
|
const drawBar = (n) => 'โ'.repeat(n) + 'โ'.repeat(BAR - n);
|
|
107
107
|
const rule = 'โ'.repeat(70);
|
|
108
|
+
// โโ IS THIS STILL HAPPENING, OR IS IT A SOUVENIR? (added 2026-08-08) โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
109
|
+
//
|
|
110
|
+
// This card reported "SAVED ~61% ยท 43 routed tasks" for FOURTEEN DAYS after routing had entirely
|
|
111
|
+
// stopped. Every number was true and every number was history: the newest receipt was 2026-07-24,
|
|
112
|
+
// and in the fortnight since โ including three days of heavy work โ not one task was routed. The
|
|
113
|
+
// card looked like a live dashboard and was actually a museum plaque.
|
|
114
|
+
//
|
|
115
|
+
// SKILL.md already records this exact failure once ("after two days, the receipts log held 3
|
|
116
|
+
// entries, all test pings, $0.018 saved โ while real work was done inline in the most expensive
|
|
117
|
+
// model") and answered it by making the routing rule a floor rather than advice. That did not
|
|
118
|
+
// help, because nothing MEASURED whether the floor was being honoured. A silent zero reads
|
|
119
|
+
// identically to a healthy system, which is the same disease as every other defect fixed this
|
|
120
|
+
// week: a surface reporting something other than what it measured.
|
|
121
|
+
//
|
|
122
|
+
// So the card now leads with recency. Savings you earned last month are not savings you are
|
|
123
|
+
// earning, and a stale ledger must say so before it says anything else.
|
|
124
|
+
const newest = rows.map((r) => Date.parse(r.ts || r.date || r.at || 0)).filter(Boolean).sort().at(-1);
|
|
125
|
+
const idleDays = newest ? (Date.now() - newest) / 86_400_000 : Infinity;
|
|
126
|
+
const staleness = !Number.isFinite(idleDays)
|
|
127
|
+
? ' โ NO ROUTING RECEIPTS AT ALL โ these numbers describe nothing that has happened.'
|
|
128
|
+
: idleDays >= 3
|
|
129
|
+
? ` โ STALE: nothing has been routed in ${Math.floor(idleDays)} days (newest receipt ${new Date(newest).toISOString().slice(0, 10)}).\n`
|
|
130
|
+
+ ' The figures below are HISTORY, not current performance โ mechanical work is running unrouted.'
|
|
131
|
+
: null;
|
|
132
|
+
|
|
108
133
|
return [
|
|
109
134
|
rule,
|
|
135
|
+
...(staleness ? [staleness, ''] : []),
|
|
110
136
|
` ๐ฐ SAVED ~${pct}%${speedBadge} ยท ~${ratio}ร cheaper ยท ${rows.length} routed task(s) (${subagents} subagent, ${rows.length - subagents} openrouter/calibration)`,
|
|
111
137
|
'',
|
|
112
138
|
` without routing ${drawBar(BAR)} ${fmt$(totalFrontier)}`,
|
|
@@ -119,6 +119,18 @@ sh scripts/memdb-health.sh .swarm/memory.db >> "$LOG" 2>&1 \
|
|
|
119
119
|
# host verification, post-publication seal) still runs exactly as designed. It stands down on any
|
|
120
120
|
# unexpected state โ dirty tree, open PRs, no green exact-SHA evidence, a -dev version, a stalled
|
|
121
121
|
# Actions plane โ because a watchdog acting on a partial picture is worse than no watchdog.
|
|
122
|
+
# โโ GITHUB HEALTH (2026-08-08). Stuart: "You should never ever let it be in a situation where
|
|
123
|
+
# it's got failed pushes. Your job is to always be looking out for GitHub." Every prior signal
|
|
124
|
+
# watched exactly ONE thing โ signal-watch CI verdicts, issue-watch the SLA, published-surface
|
|
125
|
+
# npm-vs-GitHub โ and nothing watched the states that actually stall work: a red main, a PR that
|
|
126
|
+
# silently went CONFLICTING, a stalled Actions queue, branches left behind after a merge.
|
|
127
|
+
#
|
|
128
|
+
# REPORTS ONLY. It cannot push, merge, close or publish (asserted by test). Exit 1 means a human
|
|
129
|
+
# needs to look, and the reasons print in full rather than as a count.
|
|
130
|
+
echo "===== GITHUB-HEALTH watch โ $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
131
|
+
/usr/local/bin/node scripts/github-health-watch.mjs >> "$LOG" 2>&1 \
|
|
132
|
+
|| echo "[github-health] findings above need attention" >> "$LOG"
|
|
133
|
+
|
|
122
134
|
echo "===== RELEASE-CONVERGENCE watchdog โ $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
123
135
|
/usr/local/bin/node scripts/release-convergence-watchdog.mjs --dispatch >> "$LOG" 2>&1 \
|
|
124
136
|
|| echo "[release-watchdog] exited non-zero โ see above; nightly continues" >> "$LOG"
|
|
@@ -428,8 +428,39 @@ function wiringSurvey() {
|
|
|
428
428
|
function sessionHookExists() {
|
|
429
429
|
return fs.existsSync(path.join(CONSOLE_ROOT, '.claude/hooks/agentdb-ensure.sh')) || fs.existsSync(path.join(CONSOLE_ROOT, '.claude/hooks'));
|
|
430
430
|
}
|
|
431
|
+
// WHICH FILE IS THE PROJECT'S MEMORY STORE IS NOT A CONSTANT (issue #127).
|
|
432
|
+
//
|
|
433
|
+
// This probed only `.swarm/memory.db`. A reporter on ruflo 3.34.0 measured their live store โ 31MB
|
|
434
|
+
// with a fresh project-state checkpoint โ sitting in `.swarm/agentdb-memory.db`, so the card scored
|
|
435
|
+
// the project on an empty file and reported "no project-state checkpoint found" and "liveness: 1
|
|
436
|
+
// entries" about a store that had neither problem. The card's own remedy could not fix it, because
|
|
437
|
+
// the data was never missing; the probe was looking elsewhere.
|
|
438
|
+
//
|
|
439
|
+
// I could NOT reproduce their attribution on this machine: same ruflo 3.34.0, both with and without
|
|
440
|
+
// an explicit --path, a fresh `ruflo memory store` lands in `.swarm/memory.db` (proven by exact-key
|
|
441
|
+
// SQL against both files). So which name the CLI picks is evidently config- or environment-
|
|
442
|
+
// dependent, and hard-coding EITHER name is how this breaks again on the next machine.
|
|
443
|
+
//
|
|
444
|
+
// So the probe stops guessing and asks the filesystem: consider both known names, and use whichever
|
|
445
|
+
// actually holds rows. That is correct on their machine and on mine without knowing why they differ.
|
|
446
|
+
const MEMORY_DB_NAMES = ['memory.db', 'agentdb-memory.db'];
|
|
447
|
+
export function resolveMemoryDb(projectDir) {
|
|
448
|
+
const candidates = MEMORY_DB_NAMES
|
|
449
|
+
.map((name) => path.join(projectDir, '.swarm', name))
|
|
450
|
+
.filter((file) => fs.existsSync(file));
|
|
451
|
+
if (!candidates.length) return path.join(projectDir, '.swarm/memory.db'); // canonical name for the "absent" message
|
|
452
|
+
if (candidates.length === 1) return candidates[0];
|
|
453
|
+
// Both present: prefer the one with real content. Size is a proxy for rows that costs no query and
|
|
454
|
+
// cannot throw on a locked or WAL-mode database โ this runs inside a UI probe that must never hang.
|
|
455
|
+
return candidates.sort((a, b) => {
|
|
456
|
+
const sa = (() => { try { return fs.statSync(a).size; } catch { return 0; } })();
|
|
457
|
+
const sb = (() => { try { return fs.statSync(b).size; } catch { return 0; } })();
|
|
458
|
+
return sb - sa;
|
|
459
|
+
})[0];
|
|
460
|
+
}
|
|
461
|
+
|
|
431
462
|
function probeMemory(projectDir) {
|
|
432
|
-
const db =
|
|
463
|
+
const db = resolveMemoryDb(projectDir);
|
|
433
464
|
const probes = {};
|
|
434
465
|
// compaction survival + session surfacing are filesystem facts, always checkable
|
|
435
466
|
const snapshot = inspectSessionSnapshots(projectDir);
|
|
@@ -1633,7 +1664,10 @@ function findMemoryStores(root) {
|
|
|
1633
1664
|
const p = path.join(dir, e.name);
|
|
1634
1665
|
if (VENDOR.some((m) => (p + '/').includes(m))) continue;
|
|
1635
1666
|
if (e.name === '.swarm') {
|
|
1636
|
-
|
|
1667
|
+
// Same resolution as probeMemory โ the fleet walk had the identical hardcoded assumption (#127).
|
|
1668
|
+
const resolved = ['memory.db', 'agentdb-memory.db'].map((n) => path.join(p, n)).filter((f) => fs.existsSync(f))
|
|
1669
|
+
.sort((a, b) => { const sz = (f) => { try { return fs.statSync(f).size; } catch { return 0; } }; return sz(b) - sz(a); })[0];
|
|
1670
|
+
if (resolved) out.push({ project: dir, db: resolved });
|
|
1637
1671
|
continue;
|
|
1638
1672
|
}
|
|
1639
1673
|
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
@@ -416,7 +416,23 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
416
416
|
}
|
|
417
417
|
await transition('finalize-intent');
|
|
418
418
|
const final = await adapter.finalize(identity, current, hostVerifier);
|
|
419
|
-
|
|
419
|
+
// SAY WHICH CONVERGENCE FAILED, AND WHY (2026-08-07). This threw the bare sentence
|
|
420
|
+
// `final release convergence failed` while `finalize` had already returned exactly the reason
|
|
421
|
+
// โ hostVerifierError, hosts.verifier.error, publicationError or sealError โ and the throw
|
|
422
|
+
// discarded every one of them. That is the same defect that let `spawnSync ENOBUFS` masquerade
|
|
423
|
+
// as `staged GitHub payload mismatch` for three days: an error that reports its category and
|
|
424
|
+
// withholds its evidence. Publication had ALREADY succeeded on both channels here, so the
|
|
425
|
+
// operator is reading this line while npm and GitHub are correct, with nothing to act on.
|
|
426
|
+
if (final.verdict !== 'PASS') {
|
|
427
|
+
const why = final.hostVerifierError
|
|
428
|
+
|| final.hosts?.verifier?.error
|
|
429
|
+
|| final.publicationError
|
|
430
|
+
|| final.sealError
|
|
431
|
+
|| `verdict=${final.verdict ?? '(none)'}`;
|
|
432
|
+
const fixtures = final.hosts?.verifier?.fixtures;
|
|
433
|
+
const detail = fixtures ? ` | fixtures: ${JSON.stringify(fixtures).slice(0, 400)}` : '';
|
|
434
|
+
throw new Error(`final release convergence failed: ${why}${detail}`);
|
|
435
|
+
}
|
|
420
436
|
const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
|
|
421
437
|
if (!(npmLatestIsB(reobserved, identity) && reobserved.github?.latest && githubIsB(reobserved, identity))) {
|
|
422
438
|
throw new Error('provider defaults drifted during finalization');
|
package/scripts/route-cheap.mjs
CHANGED
|
@@ -55,6 +55,20 @@ export const CLAUDE_TIERS = {
|
|
|
55
55
|
'claude-sonnet-5': { in: 2.0, out: 10.0 },
|
|
56
56
|
'claude-opus-4.8': { in: 5.0, out: 25.0 },
|
|
57
57
|
'claude-fable-5': { in: 10.0, out: 50.0 },
|
|
58
|
+
// OPUS 5 ADDED 2026-08-08, and its absence was silently costing every receipt.
|
|
59
|
+
//
|
|
60
|
+
// dispatch-receipt refuses to price an unknown model ("refusing to invent savings"), which is the
|
|
61
|
+
// right call โ but it means a session running on a model this table does not know can NEVER record
|
|
62
|
+
// a dispatch. Opus 5 is the current main-loop model, so every subagent routed from one of those
|
|
63
|
+
// sessions produced no receipt at all, and `/savings` read as "routing stopped" when what actually
|
|
64
|
+
// stopped was RECORDING. That is why the ledger sat unchanged for 14 days.
|
|
65
|
+
//
|
|
66
|
+
// Prices verified LIVE against the OpenRouter /models API on 2026-08-08, per this file's own
|
|
67
|
+
// standing rule โ never recalled, never inferred from the 4.8 row:
|
|
68
|
+
// anthropic/claude-opus-5 in $5.00/Mtok out $25.00/Mtok
|
|
69
|
+
// anthropic/claude-opus-5-fast in $10.00/Mtok out $50.00/Mtok
|
|
70
|
+
'claude-opus-5': { in: 5.0, out: 25.0 },
|
|
71
|
+
'claude-opus-5-fast': { in: 10.0, out: 50.0 },
|
|
58
72
|
};
|
|
59
73
|
|
|
60
74
|
/** Price lookup across both tables. Unknown model โ null (never invent a savings number). */
|