ruvnet-brain 4.7.20 → 4.7.21
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 +494 -97
- package/kb/download-retry.mjs +93 -21
- package/kb/forge-guard-injection.mjs +277 -0
- package/kb/forge-update.mjs +166 -38
- package/kb/lifecycle-evidence-retention.mjs +151 -14
- package/kb/model-requirements.mjs +4 -1
- package/kb/refresh-run.mjs +175 -27
- package/kb/verify-citation.mjs +15 -2
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +16 -8
- package/plugin/scripts/adr-currency-gate.mjs +2 -1
- package/plugin/scripts/advisory-contract.mjs +21 -0
- package/plugin/scripts/advocacy-route.mjs +5 -4
- package/plugin/scripts/agentdb-recall.mjs +15 -5
- package/plugin/scripts/automatic-update.mjs +9 -1
- package/plugin/scripts/brain-confirmation.mjs +2 -1
- package/plugin/scripts/brain-footprint.mjs +72 -11
- package/plugin/scripts/capability-registry.mjs +1 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +2 -1
- package/plugin/scripts/codex-overnight-notice.mjs +33 -0
- package/plugin/scripts/completion-claim-evidence.mjs +56 -12
- package/plugin/scripts/continuation-gate.mjs +28 -9
- package/plugin/scripts/decision-gate.mjs +13 -63
- package/plugin/scripts/decision-outcomes.mjs +36 -0
- package/plugin/scripts/decision-recall-gate.mjs +40 -5
- package/plugin/scripts/decision-record.mjs +8 -3
- package/plugin/scripts/degradation-watch.mjs +2 -1
- package/plugin/scripts/developer-update-lock.mjs +172 -33
- package/plugin/scripts/developer-update-owner.mjs +8 -5
- package/plugin/scripts/developer-update-policy.mjs +4 -1
- package/plugin/scripts/developer-update.mjs +155 -53
- package/plugin/scripts/doc-currency.mjs +2 -16
- package/plugin/scripts/duplicate-gate.mjs +2 -1
- package/plugin/scripts/entrypoint.mjs +34 -0
- package/plugin/scripts/footprint-io.mjs +135 -8
- package/plugin/scripts/grok-hook-adapter.mjs +2 -1
- package/plugin/scripts/ground-ruvnet.sh +6 -3
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-code-projection.mjs +2 -1
- package/plugin/scripts/grounding-substance.mjs +2 -1
- package/plugin/scripts/grounding-turn-gate.mjs +2 -2
- package/plugin/scripts/grounding-turn-mark.mjs +2 -2
- package/plugin/scripts/hook-context-budget.mjs +2 -1
- package/plugin/scripts/hook-input.mjs +2 -2
- package/plugin/scripts/hook-shim.mjs +9 -2
- package/plugin/scripts/host-update.mjs +29 -5
- package/plugin/scripts/identifier-preflight.mjs +2 -1
- package/plugin/scripts/kb-copy-proof.mjs +57 -9
- package/plugin/scripts/learn-capture.mjs +2 -1
- package/plugin/scripts/learning-store.mjs +62 -16
- package/plugin/scripts/lesson-bridge.mjs +2 -2
- package/plugin/scripts/lesson-promote.mjs +83 -22
- package/plugin/scripts/md-stamp.mjs +2 -1
- package/plugin/scripts/native-turn-transcript.mjs +98 -6
- package/plugin/scripts/nightly-scheduler.mjs +10 -5
- package/plugin/scripts/package-recommender.mjs +2 -2
- package/plugin/scripts/project-store-resolver.mjs +19 -0
- package/plugin/scripts/routing-outcome-capture.mjs +2 -1
- package/plugin/scripts/session-snapshot-hook.mjs +24 -4
- package/plugin/scripts/session-start-core.mjs +5 -15
- package/plugin/scripts/session-start-health.mjs +97 -10
- package/plugin/scripts/session-start-issue-alert.mjs +63 -0
- package/plugin/scripts/session-start-update-plane.mjs +15 -1
- package/plugin/scripts/signal-watch.mjs +2 -1
- package/plugin/scripts/spend-guard.mjs +2 -1
- package/plugin/scripts/substitution-wall.mjs +2 -1
- package/plugin/scripts/turn-outcome-capture.mjs +7 -3
- package/plugin/scripts/unprompted-runtime.mjs +20 -3
- package/plugin/scripts/update-apply.mjs +186 -15
- package/plugin/skills/release-proof/scripts/release-proof.mjs +2 -1
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +2 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +3 -2
- package/scripts/adr-072-completion.mjs +2 -1
- package/scripts/agentdb-context.mjs +2 -1
- package/scripts/agentdb-fleet-doctor.mjs +30 -8
- package/scripts/approved-runtime.mjs +2 -1
- package/scripts/architecture-review-lock.mjs +2 -1
- package/scripts/ascii-drift.mjs +2 -1
- package/scripts/brain-latency-50.mjs +2 -1
- package/scripts/brain-novice-50.mjs +2 -16
- package/scripts/brain-score.mjs +13 -2
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/build-concepts.mjs +2 -1
- package/scripts/candidate-ci-receipt.mjs +2 -1
- package/scripts/candidate-host-evidence.mjs +2 -1
- package/scripts/card-from-source.mjs +2 -1
- package/scripts/ci/warm-brain-models.mjs +2 -1
- package/scripts/claims-verify.mjs +7 -3
- package/scripts/claude-controlled-terminal.mjs +2 -1
- package/scripts/claude-terminal-mod.mjs +2 -1
- package/scripts/code-release-corpus.mjs +2 -1
- package/scripts/codex-hook-trust-reconcile.mjs +67 -1
- package/scripts/codex-hook-trust.mjs +40 -1
- package/scripts/codex-hook-wrapper-history.mjs +2 -1
- package/scripts/codex-host-execution-proof.mjs +2 -1
- package/scripts/completion-claim-replay.mjs +2 -1
- package/scripts/console-engine.mjs +8 -3
- package/scripts/corpus-canary.mjs +2 -1
- package/scripts/corpus-candidate.mjs +2 -2
- package/scripts/corpus-coverage-sidecar.mjs +2 -1
- package/scripts/corpus-dispatch-decision.mjs +2 -1
- package/scripts/corpus-dispatch-receipt.mjs +2 -1
- package/scripts/corpus-next-seed.mjs +2 -1
- package/scripts/corpus-qa.mjs +2 -1
- package/scripts/corpus-reconcile.mjs +2 -2
- package/scripts/corpus-watchdog.mjs +2 -1
- package/scripts/count-chunks.mjs +2 -1
- package/scripts/customer-state-matrix.mjs +2 -1
- package/scripts/derive-passage-content-map.mjs +2 -1
- package/scripts/development-push-check.mjs +2 -1
- package/scripts/dispatch-receipt.mjs +2 -1
- package/scripts/doc-currency.mjs +2 -1
- package/scripts/dream-issue-gate.mjs +2 -1
- package/scripts/dual-host-deliberation.mjs +2 -16
- package/scripts/dual-host-suggest.mjs +2 -16
- package/scripts/eval-brain.mjs +7 -3
- package/scripts/execution-policy.mjs +2 -1
- package/scripts/execution-preflight.mjs +2 -1
- package/scripts/falsify.mjs +2 -1
- package/scripts/fix-metaharness-memretrieve.mjs +2 -1
- package/scripts/fix-workstream.mjs +2 -1
- package/scripts/full-hints.mjs +2 -1
- package/scripts/full-suite-gate.mjs +2 -1
- package/scripts/grok-host.mjs +68 -0
- package/scripts/hook-qualify-core.mjs +3 -1
- package/scripts/hook-qualify.mjs +2 -1
- package/scripts/hook-retirement-check.mjs +2 -1
- package/scripts/host-registry.mjs +2 -1
- package/scripts/independent-review-receipt.mjs +2 -1
- package/scripts/ingest-new-repos.mjs +2 -1
- package/scripts/ingest-repo.mjs +12 -2
- package/scripts/integration-evidence.mjs +2 -1
- package/scripts/issue-fix.mjs +7 -24
- package/scripts/issue-watch.mjs +116 -40
- package/scripts/job-heartbeat.sh +54 -19
- package/scripts/key-canary.mjs +9 -3
- package/scripts/latency-to-surface.mjs +2 -1
- package/scripts/learning-replay-cli.mjs +97 -19
- package/scripts/learning-replay-execution.mjs +2 -0
- package/scripts/learning-replay-fixture.mjs +3 -0
- package/scripts/learning-replay-proof.mjs +14 -13
- package/scripts/learning-replay.mjs +3 -3
- package/scripts/learnings.mjs +2 -1
- package/scripts/lesson-ratify.mjs +88 -40
- package/scripts/loop-checkpoint.mjs +2 -1
- package/scripts/metaharness-receipts.mjs +2 -1
- package/scripts/model-catalog.mjs +2 -1
- package/scripts/model-currency-evidence.mjs +6 -4
- package/scripts/model-currency.mjs +33 -3
- package/scripts/model-native-catalog.mjs +2 -1
- package/scripts/model-router-agent-hook.mjs +2 -1
- package/scripts/model-router-dispatch.mjs +2 -1
- package/scripts/model-router-engine.mjs +2 -1
- package/scripts/model-router-outcome.mjs +2 -1
- package/scripts/model-router-setup.mjs +2 -1
- package/scripts/model-routing-eval.mjs +2 -1
- package/scripts/model-routing-gateway.mjs +2 -1
- package/scripts/model-routing-launchers.mjs +4 -1
- package/scripts/model-terminal-gateway.mjs +2 -1
- package/scripts/model-terminal-launchers.mjs +2 -1
- package/scripts/model-weekly-analyst.mjs +2 -1
- package/scripts/model-weekly-cycle.mjs +2 -1
- package/scripts/model-weekly-qualification.mjs +2 -1
- package/scripts/nightly-gists.sh +26 -20
- package/scripts/nightly-two-run-proof.mjs +2 -1
- package/scripts/nightly-watchdog.mjs +158 -47
- package/scripts/no-silent-substitution.mjs +2 -1
- package/scripts/notify.sh +11 -6
- package/scripts/ntfy-inbox.mjs +41 -18
- package/scripts/onboarding-console.mjs +47 -8
- package/scripts/oracle/abstain-threshold-sweep.mjs +2 -1
- package/scripts/oracle/abstain-trace.mjs +2 -1
- package/scripts/oracle/doc2query-generate.mjs +2 -1
- package/scripts/oracle/doc2query-reach.mjs +2 -1
- package/scripts/oracle/judge-train.mjs +2 -1
- package/scripts/oracle/need-set-split.mjs +2 -1
- package/scripts/oracle/produce-questions.mjs +2 -16
- package/scripts/oracle/repo-recall.mjs +2 -2
- package/scripts/oracle/retrieval-accuracy.mjs +2 -2
- package/scripts/oracle/sona-query-adapter-eval.mjs +2 -1
- package/scripts/oracle/source-tree.mjs +2 -15
- package/scripts/oracle/source-units.mjs +2 -16
- package/scripts/oracle/spike-run.mjs +2 -16
- package/scripts/oracle/validate-labels.mjs +2 -16
- package/scripts/org-repo-count.mjs +2 -1
- package/scripts/package-cards.mjs +2 -1
- package/scripts/prepublication-evidence.mjs +2 -1
- package/scripts/private-overlay.mjs +2 -1
- package/scripts/product-integrity-contract.mjs +2 -1
- package/scripts/protected-release-invocation.mjs +2 -1
- package/scripts/public-verification-abandon.mjs +2 -1
- package/scripts/public-verification-aggregate.mjs +2 -1
- package/scripts/public-verification-finalizer.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +2 -1
- package/scripts/public-verification-lane.mjs +2 -1
- package/scripts/publication-receipt.mjs +2 -1
- package/scripts/published-surface-probe.mjs +2 -1
- package/scripts/qe/card-lane-gate.mjs +2 -16
- package/scripts/qe/session-start-gate.mjs +2 -16
- package/scripts/qe/ux-suite.mjs +2 -1
- package/scripts/qualified-candidate-check.mjs +2 -1
- package/scripts/rebuild-gists-from-receipts.mjs +2 -1
- package/scripts/recommendation-eval.mjs +2 -1
- package/scripts/recommendation-floor.mjs +2 -1
- package/scripts/recommendation-judge-score.mjs +2 -1
- package/scripts/recommendation-real-host-score.mjs +2 -1
- package/scripts/recommendation-real-host.mjs +2 -1
- package/scripts/refresh-capability-only-store.mjs +2 -1
- package/scripts/refresh-model-catalog.mjs +2 -1
- package/scripts/rehearse-corpus-pipeline.mjs +2 -1
- package/scripts/release-authority.mjs +2 -1
- package/scripts/release-evidence-aggregate.mjs +2 -1
- package/scripts/release-payload.mjs +2 -1
- package/scripts/release-projection.mjs +2 -1
- package/scripts/release-proof.mjs +2 -1
- package/scripts/release-qualification-contract.mjs +64 -28
- package/scripts/release-qualification.mjs +8 -1
- package/scripts/release-vector.mjs +2 -1
- package/scripts/restore-local-ingests.mjs +2 -1
- package/scripts/retrieval-canary.mjs +2 -1
- package/scripts/route-cheap.mjs +2 -1
- package/scripts/route-gold-rank.mjs +2 -1
- package/scripts/route-index-memory.mjs +2 -1
- package/scripts/route-latency-warm.mjs +2 -1
- package/scripts/router-utilization.mjs +2 -1
- package/scripts/routing-flywheel.mjs +2 -1
- package/scripts/run-operational-benchmark.mjs +2 -1
- package/scripts/run-operational-benchmark.v3.mjs +2 -1
- package/scripts/rvf-index-audit.mjs +2 -16
- package/scripts/seal-gist-receipt.mjs +2 -1
- package/scripts/security-guidance-codex-compat.mjs +2 -1
- package/scripts/selfcheck.mjs +2 -1
- package/scripts/signal-watch.mjs +2 -1
- package/scripts/source-coverage.mjs +2 -1
- package/scripts/stabilization-receipt.mjs +2 -1
- package/scripts/stamp-sweep.mjs +2 -1
- package/scripts/status-honesty.mjs +2 -1
- package/scripts/sync-commands.mjs +2 -2
- package/scripts/top100-benchmark.mjs +2 -1
- package/scripts/top100-corpus.mjs +2 -1
- package/scripts/user-model-hook-install.mjs +2 -1
- package/scripts/user-model-prompt-hook.mjs +2 -1
- package/scripts/verify-bundle.mjs +3 -1
- package/scripts/verify-model-catalog.mjs +2 -1
- package/scripts/version.mjs +2 -1
- package/scripts/wired-check.mjs +11 -5
- package/scripts/worktree-integrity.mjs +2 -2
|
@@ -90,8 +90,11 @@ export function requiredEmbedderModels(kbDir) {
|
|
|
90
90
|
return models.size ? [...models].sort() : [MINILM_MODEL];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// A model is present only when the files the strict offline reader loads are there (#301): a
|
|
94
|
+
// folder left by a download that did not finish (no onnx/model_quantized.onnx) counts as missing,
|
|
95
|
+
// or the doctor calls a cache warm that cannot answer a query.
|
|
93
96
|
export function missingEmbedderModels(modelCache, models) {
|
|
94
|
-
return models.filter((model) => !
|
|
97
|
+
return models.filter((model) => !modelCacheReady(modelCache, model));
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
export function configureTransformersModel(T, modelCache, model) {
|
package/kb/refresh-run.mjs
CHANGED
|
@@ -58,15 +58,19 @@ function sameExecutable(left, right) {
|
|
|
58
58
|
try { return fs.realpathSync(left) === fs.realpathSync(right); } catch { return path.resolve(left || '') === path.resolve(right || ''); }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
const observed = inspectProcess
|
|
61
|
+
function observeOwnerProcess(owner, { inspectProcess, isAlive }) {
|
|
62
|
+
return inspectProcess
|
|
64
63
|
? inspectProcess(owner.pid)
|
|
65
64
|
: (isAlive && isAlive !== alive)
|
|
66
65
|
? (isAlive(owner.pid)
|
|
67
66
|
? { state: 'live', processStart: owner.processStart, executable: owner.executable }
|
|
68
67
|
: { state: 'dead' })
|
|
69
68
|
: observedProcessIdentity(owner.pid);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ownerState(owner, { hostname, inspectProcess, isAlive }) {
|
|
72
|
+
if (!owner || owner.host !== hostname || !owner.processStart || !owner.executable) return 'unknown';
|
|
73
|
+
const observed = observeOwnerProcess(owner, { inspectProcess, isAlive });
|
|
70
74
|
if (observed?.state !== 'live') return observed?.state === 'dead' ? 'dead' : 'unknown';
|
|
71
75
|
return observed.processStart === owner.processStart && sameExecutable(observed.executable, owner.executable)
|
|
72
76
|
? 'live'
|
|
@@ -78,6 +82,135 @@ export function inspectRefreshOwner(owner, { hostname = os.hostname(), inspectPr
|
|
|
78
82
|
return ownerState(owner, { hostname, inspectProcess, isAlive });
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
// A lock nobody can prove alive must not wedge every later run (4.7.21, audit C). Liveness cannot be proven
|
|
86
|
+
// across hosts (and on macOS os.hostname() follows the network, so "another host" is often this Mac on
|
|
87
|
+
// another Wi-Fi: ownerVerdict checks the pid here first), but a refresh run cannot outlive the updater's own deadlines. An owner.json that cannot be
|
|
88
|
+
// read or lacks its identity is debris once no writer could still be publishing it.
|
|
89
|
+
export const REMOTE_OWNER_DEAD_MS = 3 * 3600_000;
|
|
90
|
+
export const INCOMPLETE_OWNER_RECLAIM_MS = 3600_000;
|
|
91
|
+
const ownerComplete = (owner) => owner?.schemaVersion === 3 && Boolean(owner.runId && owner.token && owner.host
|
|
92
|
+
&& Number.isSafeInteger(owner.pid) && owner.receiptPath && owner.brainHome);
|
|
93
|
+
const removalCommand = (lockPath) => (process.platform === 'win32'
|
|
94
|
+
? `Remove-Item -Recurse -Force "${lockPath}"` : `rm -rf "${lockPath}"`);
|
|
95
|
+
function newestMtimeMs(...files) {
|
|
96
|
+
let newest = Number.NaN;
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
try { const ms = fs.lstatSync(file).mtimeMs; if (!(newest >= ms)) newest = ms; } catch { /* absent */ }
|
|
99
|
+
}
|
|
100
|
+
return newest;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 'dead' also for a remote owner past REMOTE_OWNER_DEAD_MS; every other answer is ownerState's.
|
|
105
|
+
* 4.7.21 review (Opus updater #2, reproduced): on macOS the hostname follows the network, so an owner recorded
|
|
106
|
+
* under "another host" is often THIS Mac, asleep mid-update on the old Wi-Fi. Its pid is inspected here first: the
|
|
107
|
+
* same pid alive with the same process start and executable IS the owner, whatever the hostname says. Only an
|
|
108
|
+
* absent or different local process may be aged out.
|
|
109
|
+
*/
|
|
110
|
+
function ownerVerdict(owner, { hostname, inspectProcess, isAlive, nowMs }) {
|
|
111
|
+
const state = ownerState(owner, { hostname, inspectProcess, isAlive });
|
|
112
|
+
if (state !== 'unknown' || !ownerComplete(owner) || owner.host === hostname) return { state, reason: 'exact-dead-owner' };
|
|
113
|
+
if (owner.processStart && owner.executable) {
|
|
114
|
+
const local = observeOwnerProcess(owner, { inspectProcess, isAlive });
|
|
115
|
+
if (local?.state === 'live' && local.processStart === owner.processStart
|
|
116
|
+
&& sameExecutable(local.executable, owner.executable)) return { state: 'live', reason: 'same-process-other-hostname' };
|
|
117
|
+
}
|
|
118
|
+
const startedMs = Date.parse(owner.startedAt || '');
|
|
119
|
+
return Number.isFinite(startedMs) && nowMs - startedMs > REMOTE_OWNER_DEAD_MS
|
|
120
|
+
? { state: 'dead', reason: 'stale-remote-owner' }
|
|
121
|
+
: { state, reason: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── takeover is serialized (4.7.21 review, Codex #1) ─────────────────────────────────────────────
|
|
125
|
+
// Two contenders can judge the same abandoned lock. Without a claim, A reclaims and acquires, then B removes
|
|
126
|
+
// A's FRESH lock on its stale judgement and both run. A takeover now (1) creates an exclusive claim file
|
|
127
|
+
// inside the judged lock directory ('wx': one winner per directory instance), (2) re-checks, while holding
|
|
128
|
+
// the claim, that the directory is the same one (dev/ino) and owner.json is byte-identical to what was
|
|
129
|
+
// judged, and only then (3) moves it aside. A claim left by a crashed reclaimer is superseded by the next
|
|
130
|
+
// numbered claim once CLAIM_STALE_MS old, so a crash mid-takeover cannot wedge the lock either.
|
|
131
|
+
export const CLAIM_STALE_MS = 5 * 60_000;
|
|
132
|
+
const CLAIM_FILE = /^takeover(?:-(\d+))?\.json$/;
|
|
133
|
+
function dirIdentity(dir) {
|
|
134
|
+
const stat = fs.lstatSync(dir, { bigint: true });
|
|
135
|
+
if (!stat.isDirectory()) throw Object.assign(new Error(`${dir} is not a lock directory`), { code: 'ENOTDIR' });
|
|
136
|
+
return `${stat.dev}:${stat.ino}`;
|
|
137
|
+
}
|
|
138
|
+
function readRaw(file) {
|
|
139
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (error) { if (error?.code === 'ENOENT') return null; throw error; }
|
|
140
|
+
}
|
|
141
|
+
// A claim file must not restart the age an incomplete owner is judged by (Opus r2 #6): writing or removing one bumps
|
|
142
|
+
// the lock directory's mtime, which newestMtimeMs reads, so the directory's own times are put back.
|
|
143
|
+
function keepDirectoryTimes(dir, change) {
|
|
144
|
+
let before = null;
|
|
145
|
+
try { before = fs.lstatSync(dir); } catch { /* judged below */ }
|
|
146
|
+
try { return change(); } finally { if (before) try { fs.lutimesSync(dir, before.atime, before.mtime); } catch { /* gone */ } }
|
|
147
|
+
}
|
|
148
|
+
const GONE = Symbol('judged lock vanished');
|
|
149
|
+
function claimTakeover(lockPath, hostname) {
|
|
150
|
+
let highest = -1;
|
|
151
|
+
let highestName = null;
|
|
152
|
+
let names;
|
|
153
|
+
// The judged lock vanished (another reclaimer moved it): re-judge rather than throw (Fable r2 P3-1).
|
|
154
|
+
try { names = fs.readdirSync(lockPath); } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return GONE; throw error; }
|
|
155
|
+
for (const name of names) {
|
|
156
|
+
const match = CLAIM_FILE.exec(name);
|
|
157
|
+
if (!match) continue;
|
|
158
|
+
const level = match[1] ? Number(match[1]) : 0;
|
|
159
|
+
if (level > highest) { highest = level; highestName = name; }
|
|
160
|
+
}
|
|
161
|
+
if (highestName) {
|
|
162
|
+
let ageMs = 0;
|
|
163
|
+
try { ageMs = Date.now() - fs.lstatSync(path.join(lockPath, highestName)).mtimeMs; } catch { return null; }
|
|
164
|
+
if (!(ageMs > CLAIM_STALE_MS)) return null; // another reclaimer is mid-takeover
|
|
165
|
+
}
|
|
166
|
+
const level = highest + 1;
|
|
167
|
+
const file = path.join(lockPath, level ? `takeover-${level}.json` : 'takeover.json');
|
|
168
|
+
const claim = { schemaVersion: 1, token: crypto.randomBytes(24).toString('hex'), host: hostname,
|
|
169
|
+
pid: process.pid, claimedAt: new Date().toISOString(), file };
|
|
170
|
+
let fd;
|
|
171
|
+
try { fd = keepDirectoryTimes(lockPath, () => fs.openSync(file, 'wx', 0o600)); } catch (error) {
|
|
172
|
+
if (error?.code === 'EEXIST') return null;
|
|
173
|
+
if (error?.code === 'ENOENT') return GONE;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
try { fs.writeFileSync(fd, `${JSON.stringify({ ...claim, file: undefined }, null, 2)}\n`); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
177
|
+
return claim;
|
|
178
|
+
}
|
|
179
|
+
function dropClaim(claim) {
|
|
180
|
+
try { if (readRaw(claim.file)?.includes(claim.token)) keepDirectoryTimes(path.dirname(claim.file), () => fs.unlinkSync(claim.file)); } catch { /* gone with its directory */ }
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Remove the lock directory at `lockPath` only if it is still exactly the one observed. Returns 'removed',
|
|
184
|
+
* 'changed' (it is not the judged directory any more: re-judge it) or 'busy' (another reclaimer holds the claim).
|
|
185
|
+
*/
|
|
186
|
+
function removeObservedLock(lockPath, observed, { hostname, beforeRemove = null, afterClaim = null } = {}) {
|
|
187
|
+
const claim = claimTakeover(lockPath, hostname);
|
|
188
|
+
if (claim === GONE) return 'changed';
|
|
189
|
+
if (!claim) return 'busy';
|
|
190
|
+
afterClaim?.({ lockPath, claim });
|
|
191
|
+
let same = false;
|
|
192
|
+
try {
|
|
193
|
+
same = dirIdentity(lockPath) === observed.identity
|
|
194
|
+
&& readRaw(path.join(lockPath, 'owner.json')) === observed.raw
|
|
195
|
+
&& Boolean(readRaw(claim.file)?.includes(claim.token));
|
|
196
|
+
} catch { same = false; }
|
|
197
|
+
if (!same) { dropClaim(claim); return 'changed'; }
|
|
198
|
+
try { beforeRemove?.(claim); } catch (error) { dropClaim(claim); throw error; }
|
|
199
|
+
const tomb = `${lockPath}.stale-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
200
|
+
try { fs.renameSync(lockPath, tomb); } catch (error) {
|
|
201
|
+
if (error?.code === 'ENOENT') return 'changed';
|
|
202
|
+
dropClaim(claim);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
// Only the claim holder can move the judged directory, so the moved directory must carry this claim.
|
|
206
|
+
if (!readRaw(path.join(tomb, path.basename(claim.file)))?.includes(claim.token)) {
|
|
207
|
+
try { if (!fs.existsSync(lockPath)) fs.renameSync(tomb, lockPath); } catch { /* keep it as evidence */ }
|
|
208
|
+
throw new Error(`refresh lock takeover moved a directory it did not claim (${lockPath}); refusing`);
|
|
209
|
+
}
|
|
210
|
+
fs.rmSync(tomb, { recursive: true, force: true });
|
|
211
|
+
return 'removed';
|
|
212
|
+
}
|
|
213
|
+
|
|
81
214
|
/**
|
|
82
215
|
* The ONE physical-path rule for brain directories. A user may reach the brain through a symlink
|
|
83
216
|
* (`~/.cache -> /Volumes/<disk>`) while the updater knows it by its real path (Node resolves
|
|
@@ -103,15 +236,12 @@ export function refreshReceiptPath(brainHome, runId) {
|
|
|
103
236
|
return file;
|
|
104
237
|
}
|
|
105
238
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const claim = { schemaVersion: 1, token: crypto.randomBytes(24).toString('hex'), host: hostname,
|
|
109
|
-
pid: process.pid, claimedAt: now() };
|
|
110
|
-
const fd = fs.openSync(claimFile, 'wx', 0o600);
|
|
111
|
-
try { fs.writeFileSync(fd, `${JSON.stringify(claim, null, 2)}\n`); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
239
|
+
/** Records the dead owner's run ABANDONED. Runs only while this process holds the takeover claim on its lock. */
|
|
240
|
+
function abandonDeadOwner({ lockPath, owner, hostname, inspectProcess, isAlive, now, claim }) {
|
|
112
241
|
const observed = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
242
|
+
const verdict = ownerVerdict(observed, { hostname, inspectProcess, isAlive, nowMs: Date.parse(now()) });
|
|
113
243
|
if (observed.schemaVersion !== 3 || observed.runId !== owner.runId || observed.token !== owner.token
|
|
114
|
-
||
|
|
244
|
+
|| verdict.state !== 'dead') {
|
|
115
245
|
throw new Error('refresh owner changed or is no longer exactly dead during takeover');
|
|
116
246
|
}
|
|
117
247
|
const expectedReceipt = refreshReceiptPath(observed.brainHome, observed.runId);
|
|
@@ -136,7 +266,7 @@ function abandonDeadOwner({ lockPath, owner, hostname, inspectProcess, isAlive,
|
|
|
136
266
|
receipt.status = 'ABANDONED';
|
|
137
267
|
receipt.terminalVerdict = 'abandoned';
|
|
138
268
|
receipt.finishedAt = now();
|
|
139
|
-
receipt.detail = { reason:
|
|
269
|
+
receipt.detail = { reason: verdict.reason, abandonedOwner: { host: observed.host, pid: observed.pid,
|
|
140
270
|
processStart: observed.processStart, executable: observed.executable }, takeoverToken: claim.token };
|
|
141
271
|
delete receipt.intendedStatus;
|
|
142
272
|
delete receipt.intendedTerminalVerdict;
|
|
@@ -163,6 +293,7 @@ export function acquireRefreshLock({
|
|
|
163
293
|
processStart = null,
|
|
164
294
|
inspectProcess = null,
|
|
165
295
|
afterStage = null,
|
|
296
|
+
afterTakeoverClaim = null,
|
|
166
297
|
} = {}) {
|
|
167
298
|
const root = path.resolve(kbDir || '');
|
|
168
299
|
const lockPath = refreshLockPath(root);
|
|
@@ -204,23 +335,34 @@ export function acquireRefreshLock({
|
|
|
204
335
|
// Windows refuses rename-over-existing with EPERM/EACCES rather than EEXIST. Treat those
|
|
205
336
|
// codes as contention, then inspect the owner through the same liveness fence.
|
|
206
337
|
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error?.code)) throw error;
|
|
338
|
+
// Judge one exact directory: its identity and owner.json bytes are what the takeover re-checks later.
|
|
339
|
+
let observed;
|
|
340
|
+
try { observed = { identity: dirIdentity(lockPath), raw: readRaw(path.join(lockPath, 'owner.json')) }; }
|
|
341
|
+
catch (observeError) { if (['ENOENT', 'ENOTDIR'].includes(observeError?.code)) continue; throw observeError; }
|
|
207
342
|
let owner = null;
|
|
208
|
-
try { owner = JSON.parse(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
343
|
+
try { owner = JSON.parse(observed.raw); } catch { /* judged below */ }
|
|
344
|
+
const nowMs = Date.parse(now());
|
|
345
|
+
let beforeRemove = null;
|
|
346
|
+
if (!ownerComplete(owner)) {
|
|
347
|
+
const ageMs = nowMs - newestMtimeMs(lockPath, path.join(lockPath, 'owner.json'));
|
|
348
|
+
if (!(ageMs > INCOMPLETE_OWNER_RECLAIM_MS)) {
|
|
349
|
+
throw new Error(`refresh lock owner is incomplete or unreadable; refusing takeover until it is 1 h old `
|
|
350
|
+
+ `(lock ${lockPath}). If no update is running, remove it: ${removalCommand(lockPath)}`);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
const verdict = ownerVerdict(owner, { hostname, inspectProcess, isAlive, nowMs });
|
|
354
|
+
if (verdict.state !== 'dead') {
|
|
355
|
+
throw new Error(`${verdict.state === 'live' ? 'another refresh run is active' : 'refresh lock owner is ambiguous'} `
|
|
356
|
+
+ `(pid ${owner.pid || '?'} on ${owner.host || '?'}, started ${owner.startedAt || '?'}, lock ${lockPath}). `
|
|
357
|
+
+ `If no update is running${owner.host && owner.host !== hostname ? ` on ${owner.host}` : ''}, remove it: ${removalCommand(lockPath)}`);
|
|
358
|
+
}
|
|
359
|
+
beforeRemove = (claim) => abandonDeadOwner({ lockPath, owner, hostname, inspectProcess, isAlive, now, claim });
|
|
215
360
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
fs.renameSync(lockPath, stale);
|
|
220
|
-
fs.rmSync(stale, { recursive: true, force: true });
|
|
221
|
-
} catch (renameError) {
|
|
222
|
-
if (renameError?.code !== 'ENOENT') throw renameError;
|
|
361
|
+
const outcome = removeObservedLock(lockPath, observed, { hostname, beforeRemove, afterClaim: afterTakeoverClaim });
|
|
362
|
+
if (outcome === 'busy') {
|
|
363
|
+
throw new Error(`another process is taking over the refresh lock ${lockPath}; refusing to race it (retry shortly)`);
|
|
223
364
|
}
|
|
365
|
+
// 'removed' or 'changed': acquire again; a changed lock is judged afresh.
|
|
224
366
|
}
|
|
225
367
|
}
|
|
226
368
|
throw new Error(`could not acquire refresh lock ${lockPath}`);
|
|
@@ -260,8 +402,14 @@ function atomicJson(file, value) {
|
|
|
260
402
|
const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
261
403
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
262
404
|
const fd = fs.openSync(temporary, 'wx', 0o600);
|
|
263
|
-
try {
|
|
264
|
-
|
|
405
|
+
try {
|
|
406
|
+
try { fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
407
|
+
fs.renameSync(temporary, file);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
// #434: a failed write/rename must not leave a temp behind to block lifecycle retention.
|
|
410
|
+
try { fs.rmSync(temporary, { force: true }); } catch { /* the original error is the one to report */ }
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
265
413
|
}
|
|
266
414
|
|
|
267
415
|
export function openRefreshReceipt({ brainHome, lock, action, desiredVersion = null,
|
package/kb/verify-citation.mjs
CHANGED
|
@@ -127,7 +127,20 @@ function samePath(stored, docPath) {
|
|
|
127
127
|
* takes to reach the hit. A malformed JSON line is skipped, never fatal.
|
|
128
128
|
*/
|
|
129
129
|
export async function citationResolves(citation, kbDir) {
|
|
130
|
-
const
|
|
130
|
+
const own = await resolveIn(passagesFilesFor(citation.repo, kbDir), citation.docPath);
|
|
131
|
+
if (own.resolved) return own;
|
|
132
|
+
// A capability card is cited under its OWN repo (`repo=ruvector`, `path : ruvector/CARD/ruvector-card`,
|
|
133
|
+
// kb/card-lane.mjs renderCardHit) but is stored only in the derived `concepts` aggregate, under
|
|
134
|
+
// `<repo>/CARD/<repo>-card` (scripts/corpus-aggregates.mjs). Look exactly that path up there. Narrow
|
|
135
|
+
// on purpose: only a `CARD/` path, only in `concepts`, and only the exact stored path.
|
|
136
|
+
if (/^CARD\/[^/#]+-card$/.test(String(citation.docPath)) && citation.repo !== 'concepts') {
|
|
137
|
+
const card = await resolveIn(passagesFilesFor('concepts', kbDir), `${citation.repo}/${citation.docPath}`);
|
|
138
|
+
if (card.resolved) return card;
|
|
139
|
+
}
|
|
140
|
+
return own;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function resolveIn(files, docPath) {
|
|
131
144
|
if (!files.length) return { resolved: false, reason: 'no-store', file: null, storedPath: null };
|
|
132
145
|
for (const file of files) {
|
|
133
146
|
const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
|
|
@@ -136,7 +149,7 @@ export async function citationResolves(citation, kbDir) {
|
|
|
136
149
|
if (!line) continue;
|
|
137
150
|
let rec;
|
|
138
151
|
try { rec = JSON.parse(line); } catch { continue; }
|
|
139
|
-
if (typeof rec?.path === 'string' && samePath(rec.path,
|
|
152
|
+
if (typeof rec?.path === 'string' && samePath(rec.path, docPath)) {
|
|
140
153
|
return { resolved: true, reason: 'ok', file: path.basename(file), storedPath: rec.path };
|
|
141
154
|
}
|
|
142
155
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.21",
|
|
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": {
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"learning:replay:check": "node scripts/learning-replay.mjs --check-portfolio",
|
|
67
67
|
"learning:replay:mutants:check": "node scripts/learning-replay.mjs --check-mutants",
|
|
68
68
|
"learning:replay:dry": "node scripts/learning-replay.mjs --dry-run",
|
|
69
|
+
"learning:replay:wire": "node scripts/learning-replay.mjs --check-wire",
|
|
69
70
|
"wired:check": "node scripts/wired-check.mjs --check",
|
|
70
71
|
"doc:currency": "node scripts/doc-currency.mjs --check",
|
|
71
72
|
"status:check": "node scripts/status-honesty.mjs",
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
"kb/retrieval-result.mjs",
|
|
108
109
|
"kb/forge-update.mjs",
|
|
109
110
|
"kb/download-retry.mjs",
|
|
111
|
+
"kb/forge-guard-injection.mjs",
|
|
110
112
|
"kb/corpus-release-identity.mjs",
|
|
111
113
|
"kb/capability-only.mjs",
|
|
112
114
|
"kb/capability-summaries/",
|
|
@@ -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.7.
|
|
4
|
+
"version": "4.7.21",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { managedGenerationIdentity } from './managed-cli-generation.mjs';
|
|
7
7
|
import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
|
|
8
|
+
import { resolveRuflo } from '../scripts/ruflo-bin.mjs';
|
|
8
9
|
import { projectDirectory } from '../scripts/project-identity.mjs';
|
|
9
10
|
import { recordManagedCliObservation, recordRegistryLatestObservation } from '../scripts/capability-claim-evidence.mjs';
|
|
10
11
|
|
|
@@ -180,12 +181,19 @@ function writeStamps(executable, argv, env, binding) {
|
|
|
180
181
|
|
|
181
182
|
export function resolveManagedExecutable(executable, env = process.env) {
|
|
182
183
|
if (executable !== 'ruflo') return executable;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
// #420 item 5: same rules as every other Brain caller (RUFLO_BIN is authoritative, then ~/.npm-global/bin, then PATH); this used to check ~/.npm-global only and ignored RUFLO_BIN.
|
|
185
|
+
return resolveRuflo({ env, home: env.HOME || os.homedir() }) || executable;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* ruflo (and claude-flow, the same @claude-flow/cli) self-updates at startup by running `npm install` in its
|
|
190
|
+
* working directory, which here is the user's project: measured 2026-10-22, a 1.3 GB install landed there.
|
|
191
|
+
* ruflo-bin.mjs promises every Brain spawn sets CLAUDE_FLOW_AUTO_UPDATE=false; this boundary is a Brain spawn
|
|
192
|
+
* too. RUFLO_DAEMON_AUTOSTART=0 keeps a background daemon from being left behind in that project.
|
|
193
|
+
*/
|
|
194
|
+
export function managedChildEnv(executable, env) {
|
|
195
|
+
if (executable !== 'ruflo' && executable !== 'claude-flow') return env;
|
|
196
|
+
return { ...env, CLAUDE_FLOW_AUTO_UPDATE: 'false', RUFLO_DAEMON_AUTOSTART: '0' };
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
function quoteWindowsCommandArg(value) {
|
|
@@ -323,7 +331,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
|
|
|
323
331
|
if (toolName === 'ruvnet_cli_help') {
|
|
324
332
|
stampKeysForHelp(executable, argv);
|
|
325
333
|
const commandArgv = [...argv, '--help'];
|
|
326
|
-
const execution = await execute(executable, commandArgv, env);
|
|
334
|
+
const execution = await execute(executable, commandArgv, managedChildEnv(executable, env));
|
|
327
335
|
if (normalizeManagedExecution(execution).outcome === 'success') writeStamps(executable, argv, env, binding);
|
|
328
336
|
recordManagedCliObservation({ toolName, executable, argv: commandArgv, execution, env });
|
|
329
337
|
return resultOf(executable, commandArgv, execution);
|
|
@@ -368,7 +376,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
|
|
|
368
376
|
// host by supplying an arbitrary `host` property.
|
|
369
377
|
const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
|
|
370
378
|
? runtimeChildEnv({ env, cwd: projectRoot })
|
|
371
|
-
: env;
|
|
379
|
+
: managedChildEnv(executable, env);
|
|
372
380
|
if (lifecycle && typeof lifecycle.beforeExecute === 'function') {
|
|
373
381
|
await lifecycle.beforeExecute({ executable, argv, projectRoot, env: childEnv });
|
|
374
382
|
}
|
|
@@ -33,6 +33,7 @@ import fs from 'node:fs';
|
|
|
33
33
|
import { execFileSync, spawn } from 'node:child_process';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
36
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
36
37
|
|
|
37
38
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const REPO = path.resolve(HERE, '..', '..');
|
|
@@ -128,7 +129,7 @@ An ADR describing a world the code left is worse than no ADR: the next reader tr
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const isMain = (() => {
|
|
131
|
-
try { return
|
|
132
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
132
133
|
catch { return false; }
|
|
133
134
|
})();
|
|
134
135
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delivery contract for OPTIONAL Brain advice (#489).
|
|
3
|
+
*
|
|
4
|
+
* A capability recommendation, a package recommendation or a routine stack-update notice is
|
|
5
|
+
* information, not a decision the user must make now. Phrased as a question ("want me to update
|
|
6
|
+
* it?"), it was promoted by Codex into a structured question dialog that suspended an already
|
|
7
|
+
* authorized task. So optional advice is worded as statements, and the model-visible context
|
|
8
|
+
* carries this one line saying so. Real permission gates and genuinely missing information keep
|
|
9
|
+
* their normal question behaviour; this applies only to optional advisory channels.
|
|
10
|
+
*
|
|
11
|
+
* ground-ruvnet.sh cannot import this module and carries the same sentence verbatim;
|
|
12
|
+
* tests/unit/advisory-contract.test.mjs fails if the two drift.
|
|
13
|
+
*/
|
|
14
|
+
export const ADVISORY_CONTRACT = 'Optional, informational advice: mention it at most once as a statement and continue the current task. '
|
|
15
|
+
+ 'Do not open a question or approval dialog because of it; it grants no authority to install or update anything.';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Question-shaped wording that invites a decision dialog, including the "say X to proceed, or ignore
|
|
19
|
+
* this" choice prompt the advocacy copy used to carry. Optional advice must match none of it.
|
|
20
|
+
*/
|
|
21
|
+
export const QUESTION_SHAPED = /\?|\b(want me to|would you like|should i|shall i|do you want|can i|to proceed|or ignore this)\b/i;
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
ACTIONS, record, shouldStillOffer, stateHashOf, precision, pendingOffers, reconcileIgnored, loadOutcomes,
|
|
62
62
|
} from './advocacy-outcomes.mjs';
|
|
63
63
|
import { packageRecommenderEnabled, offerNames } from './package-recommender-flag.mjs';
|
|
64
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
64
65
|
// Matcher + socket client load only when the flag is on at process start: default-off pays nothing.
|
|
65
66
|
const PKG = packageRecommenderEnabled() ? { ...(await import('./package-recommender.mjs')), ...(await import('./package-recommender-client.mjs')) } : null;
|
|
66
67
|
|
|
@@ -341,9 +342,9 @@ export function buildCandidate({ prompt, match, availability }) {
|
|
|
341
342
|
? `It is already installed here.`
|
|
342
343
|
: `Install state unknown from here — say so rather than claiming it is available.`;
|
|
343
344
|
const copy = [
|
|
344
|
-
`[RuvNet Brain — capability advocacy] If it genuinely fits,
|
|
345
|
-
+ `"
|
|
346
|
-
`If
|
|
345
|
+
`[RuvNet Brain — capability advocacy] If it genuinely fits, state it in ONE sentence: `
|
|
346
|
+
+ `"${cap.id} fits here — ${cap.benefit}; 'use ${cap.id}' adopts it." ${avail}`,
|
|
347
|
+
`If the user later says to use it, the safe first step is \`${cap.nextAction}\` (undo: ${cap.undo}); confirm it with `
|
|
347
348
|
+ `search_ruvnet before you build. Say it once, do not expand it, then carry on with the actual work.`,
|
|
348
349
|
].join('\n');
|
|
349
350
|
return {
|
|
@@ -429,7 +430,7 @@ export function decide({ prompt, sessionId, file, state, now = Date.now(), start
|
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
// ── CLI ───────────────────────────────────────────────────────────────────────────────────────────
|
|
432
|
-
const isMain =
|
|
433
|
+
const isMain = isDirectInvocation(import.meta.url);
|
|
433
434
|
|
|
434
435
|
async function main() {
|
|
435
436
|
const startedAt = Date.now();
|
|
@@ -26,6 +26,7 @@ import { searchOnce, nativeRead, exactReads, searchJsonResult } from './agentdb-
|
|
|
26
26
|
import { makeLesson } from './lesson-store.mjs';
|
|
27
27
|
import { supersededKeys, tombstonedKeys } from './decision-record.mjs';
|
|
28
28
|
import { checkpointMs, checkpointNamespaces, newestCheckpoint } from './checkpoint-keys.mjs';
|
|
29
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
29
30
|
export const STORE_FILES = Object.freeze(['memory.db']);
|
|
30
31
|
export const DEFAULT_DEADLINE_MS = 1900;
|
|
31
32
|
export const BLOCK_MAX_BYTES = 600;
|
|
@@ -115,7 +116,9 @@ function clean(value, limit) {
|
|
|
115
116
|
return out;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
const
|
|
119
|
+
const MISSING_KNOWLEDGE = /\b(?:cannot\s+(?:be\s+determined|determine)|can(?:'|’)?t\s+(?:be\s+)?determined?|do\s+not\s+know|don(?:'|’)?t\s+know|cannot\s+answer|no\s+(?:verified\s+|supplied\s+|historical\s+)?(?:record|memory|evidence)\s+(?:is\s+)?(?:available|found))\b/i;
|
|
120
|
+
const MISSING_KNOWLEDGE_QUERY = /\b(?:fail(?:ed|ing|ures?)?|missing|unavailable|unknown)\b/i;
|
|
121
|
+
const NOISE_KEY =/^(?:verify[-_]|probe[-_]|test[-_]|rnb-quality-probe|session[-_]|turn[-_]|project-progress[-_]|cevt[-_])/i;
|
|
119
122
|
const SIGNAL_NAMESPACES = new Set(['lessons', 'patterns', 'pattern']);
|
|
120
123
|
/** Turn knowledge is the actual redacted outcome, never its session/transcript wrapper. */
|
|
121
124
|
function turnOutcomeClauses(value, prompt) {
|
|
@@ -393,9 +396,10 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
393
396
|
...await searchOnce({ ruflo: bin, store: jobStore, deadline: searchDeadline, env, scratch: scratchFor, args, signal }) })));
|
|
394
397
|
let status = results.every((r) => r.state === 'ok') ? 'ok'
|
|
395
398
|
: results.some((r) => r.state === 'timed out') ? 'timed out' : 'unavailable';
|
|
396
|
-
// Overfetch a bounded
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
+
// Overfetch a bounded twelve exact values (the one search already returns twelve; the exact reads are
|
|
400
|
+
// one batched reader, so this adds no Ruflo startup) so rejected turn metadata and repeated "cannot
|
|
401
|
+
// determine" answers cannot hide the next useful outcome (#429); at most three excerpts are delivered.
|
|
402
|
+
const candidates = pickRows(results, 12);
|
|
399
403
|
// Ruflo previews are ~60 characters. Read the actual selected values within
|
|
400
404
|
// the same deadline so the block contains useful evidence rather than titles.
|
|
401
405
|
const exactResults = await exactReads({ candidates, bin, deadline, env, scratch: scratchFor, signal });
|
|
@@ -417,6 +421,12 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
417
421
|
}
|
|
418
422
|
return { pick: null, state: r.state === 'ok' ? 'unavailable' : r.state };
|
|
419
423
|
}));
|
|
424
|
+
// A turn that admitted it could not answer is evidence of a failed recall, not the missing fact (#429).
|
|
425
|
+
// It stays available, after concrete outcomes; a query about failures keeps the ranked order.
|
|
426
|
+
if (!MISSING_KNOWLEDGE_QUERY.test(prompt)) {
|
|
427
|
+
const rank = ({ pick }) => Number(pick?.namespace === 'turns' && MISSING_KNOWLEDGE.test(pick.preview));
|
|
428
|
+
retrieved.sort((a, b) => rank(a) - rank(b));
|
|
429
|
+
}
|
|
420
430
|
const seenClauses = new Set();
|
|
421
431
|
const picks = retrieved.flatMap(({ pick }) => {
|
|
422
432
|
if (!pick) return [];
|
|
@@ -470,7 +480,7 @@ async function main() {
|
|
|
470
480
|
}
|
|
471
481
|
|
|
472
482
|
function isMain() {
|
|
473
|
-
try { return
|
|
483
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
474
484
|
catch { return false; }
|
|
475
485
|
}
|
|
476
486
|
if (isMain()) main().catch(() => process.exit(0));
|
|
@@ -98,7 +98,15 @@ export function automaticInvocation(args, { source, home = os.homedir(), nodePat
|
|
|
98
98
|
const coordinator = developerCoordinatorOwner({ home });
|
|
99
99
|
if (coordinator.active) {
|
|
100
100
|
if (!coordinator.ready) throw new Error(`coordinated updater is not ready: ${coordinator.reason}`);
|
|
101
|
-
|
|
101
|
+
// Hooks and timers get knowledge only, never the whole suite (brew, cargo, every npm install) under a session TTL
|
|
102
|
+
// that kills it mid-install (AuditC #10). The full suite runs only from the scheduled runner, which calls the
|
|
103
|
+
// coordinator directly, not through here.
|
|
104
|
+
// The registered snapshot moves only when an --update reconciles it, so it can predate --knowledge-only
|
|
105
|
+
// (4.7.20 and older refuse it: "unsupported developer update argument", exit 1). Pass the flag only to a
|
|
106
|
+
// coordinator whose source parses it, as runUpdate does for the updater's --result-file.
|
|
107
|
+
const knowledgeOnly = fs.readFileSync(coordinator.entry, 'utf8').includes("'--knowledge-only'");
|
|
108
|
+
return { executable: nodePath, args: [coordinator.entry, '--apply', ...(knowledgeOnly ? ['--knowledge-only'] : [])],
|
|
109
|
+
source: 'developer-suite', coordinator };
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
source ??= updateSource({ home });
|
|
@@ -143,7 +143,8 @@ export function confirm({ footprint, env = process.env, home = os.homedir(), now
|
|
|
143
143
|
const checkable = Boolean(integrity) && fs.existsSync(path.join(roots.kbDir, 'CORPUS-COVERAGE.json'));
|
|
144
144
|
if (checkable && !integrity.valid) knowledgeProblems.push([`the bytes on disk fail the release-coverage check (${integrity.failures?.[0] || 'unknown'})`, UPDATE]);
|
|
145
145
|
// A newer corpus is published and not yet installed (identity, not age): advisory, never a gate.
|
|
146
|
-
const pending = newerCorpusPending({ source, corpusCheck: readJson(autoUpdatePaths(roots.brainHome).checkFile)
|
|
146
|
+
const pending = newerCorpusPending({ source, corpusCheck: readJson(autoUpdatePaths(roots.brainHome).checkFile),
|
|
147
|
+
installedAtMs: signed ? Date.parse(signature.verifiedAt || '') : NaN });
|
|
147
148
|
if (pending) {
|
|
148
149
|
const lockAt = readJson(autoUpdatePaths(roots.brainHome).lockFile)?.at;
|
|
149
150
|
const running = Number.isFinite(Date.parse(lockAt || '')) && now - Date.parse(lockAt) < AUTO_UPDATE_LOCK_STALE_MS;
|