ruvnet-brain 4.7.20 → 4.7.22
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 +618 -104
- package/console/activity.js +122 -38
- 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-process.mjs +115 -5
- package/plugin/scripts/agentdb-recall-reader.mjs +44 -2
- package/plugin/scripts/agentdb-recall.mjs +49 -17
- 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 +241 -23
- package/plugin/scripts/continuation-gate.mjs +75 -11
- 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-gate.mjs +7 -39
- package/plugin/scripts/lesson-promote.mjs +83 -22
- package/plugin/scripts/lesson-scope.mjs +51 -0
- package/plugin/scripts/lesson-store.mjs +1 -1
- 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/ruflo-bin.mjs +14 -6
- package/plugin/scripts/ruflo-memory-hooks.mjs +157 -19
- package/plugin/scripts/session-snapshot-hook.mjs +25 -5
- package/plugin/scripts/session-start-core.mjs +7 -16
- package/plugin/scripts/session-start-health.mjs +98 -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-boot-surface-proof.mjs +208 -0
- 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 +123 -23
- 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/recall-latency-probe.mjs +48 -0
- 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 +82 -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 +13 -5
- package/scripts/worktree-integrity.mjs +2 -2
|
@@ -58,6 +58,7 @@ import path from 'node:path';
|
|
|
58
58
|
import crypto from 'node:crypto';
|
|
59
59
|
import { spawnSync } from 'node:child_process';
|
|
60
60
|
import { fileURLToPath } from 'node:url';
|
|
61
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
61
62
|
|
|
62
63
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
63
64
|
export const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
@@ -974,20 +975,5 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
974
975
|
return 1;
|
|
975
976
|
}
|
|
976
977
|
|
|
977
|
-
|
|
978
|
-
// NOT follow symlinks, while import.meta.url IS symlink-resolved by Node. Through a symlink (npm bin
|
|
979
|
-
// shims, wrapper scripts, and every os.tmpdir() path on macOS) the two sides disagree, so main()
|
|
980
|
-
// never runs -- and because nothing throws, the process exits 0. A silent exit 0 is indistinguishable
|
|
981
|
-
// from "ran, found nothing", which is how prepareCorpusCandidate once reported SUCCESS with no
|
|
982
|
-
// archive on disk. Reproduced live 2026-07-27; pinned by tests/unit/entrypoint-symlink.test.mjs.
|
|
983
|
-
function isDirectInvocation() {
|
|
984
|
-
try {
|
|
985
|
-
if (!process.argv[1]) return false;
|
|
986
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
987
|
-
} catch {
|
|
988
|
-
return false;
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const invokedDirectly = isDirectInvocation();
|
|
978
|
+
const invokedDirectly = isDirectInvocation(import.meta.url);
|
|
993
979
|
if (invokedDirectly) process.exit(main());
|
|
@@ -45,6 +45,7 @@ import os from 'node:os';
|
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
import { spawnSync } from 'node:child_process';
|
|
47
47
|
import { fileURLToPath } from 'node:url';
|
|
48
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
48
49
|
|
|
49
50
|
export const SCOPE = ['scripts/', 'kb/', 'plugin/scripts/', 'tests/', 'bin/'];
|
|
50
51
|
export const THRESHOLD = 0.5;
|
|
@@ -485,7 +486,7 @@ export function evaluate(payload, { env = process.env, now = Date.now(), deadlin
|
|
|
485
486
|
}
|
|
486
487
|
|
|
487
488
|
const isMain = (() => {
|
|
488
|
-
try { return
|
|
489
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
489
490
|
catch { return false; }
|
|
490
491
|
})();
|
|
491
492
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// plugin/scripts/entrypoint.mjs — the ONE answer to "was this module run as the program, or imported?"
|
|
2
|
+
//
|
|
3
|
+
// Every dual-purpose file in this repo (a library when imported, a CLI when run) asks that question.
|
|
4
|
+
// The copy-pasted answer compared `process.argv[1]` with `import.meta.url` directly, e.g.
|
|
5
|
+
// path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
6
|
+
// import.meta.url === pathToFileURL(process.argv[1]).href
|
|
7
|
+
// argv[1] is the path as the caller typed it, symlinks included; Node resolves symlinks before it sets
|
|
8
|
+
// import.meta.url. Run through a symlink (npm bin shims, wrapper scripts, a symlinked checkout, every
|
|
9
|
+
// os.tmpdir() path on macOS) the two sides differ, main() never runs, and the process exits 0 having
|
|
10
|
+
// done nothing. A release gate that exits 0 without checking anything passes. Dream Cycle PRs #295,
|
|
11
|
+
// #317, #333, #367, #412 and #484 each found one more copy by hand.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// import { isDirectInvocation } from './entrypoint.mjs'; // path relative to this file
|
|
15
|
+
// if (isDirectInvocation(import.meta.url)) main();
|
|
16
|
+
//
|
|
17
|
+
// tests/unit/entrypoint-guard-sweep.test.mjs fails when a raw comparison reappears outside this file,
|
|
18
|
+
// and runs a script through a symlink to prove this helper makes it execute.
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when `moduleUrl` (pass `import.meta.url`) is the file Node was asked to run. Both sides are
|
|
24
|
+
* compared as real paths. Never throws: `realpathSync` throws on a path that does not resolve (under
|
|
25
|
+
* a test runner argv[1] need not be a real file), and an imported module must not crash its importer.
|
|
26
|
+
*/
|
|
27
|
+
export function isDirectInvocation(moduleUrl, argv1 = process.argv[1]) {
|
|
28
|
+
if (!argv1 || !moduleUrl) return false;
|
|
29
|
+
try {
|
|
30
|
+
return fs.realpathSync(argv1) === fs.realpathSync(fileURLToPath(moduleUrl));
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// footprint-io.mjs — the footprint sweep's disk operations (ADR-0098), kept apart from the classifier in
|
|
2
|
-
// brain-footprint.mjs: byte counts, log rotation, the guarded removal,
|
|
2
|
+
// brain-footprint.mjs: byte counts, log rotation, the guarded removal, the rescue of a released copy's
|
|
3
|
+
// operational files, and the KEPT-proof cache.
|
|
3
4
|
//
|
|
4
5
|
// THE KEPT-PROOF CACHE (independent review S7).
|
|
5
6
|
// kbCopyProof hashes every file of a GB-sized copy. A copy kept because it holds data the live brain lacks
|
|
@@ -7,6 +8,7 @@
|
|
|
7
8
|
// which can only keep it again. Only KEPT results are cached, keyed by a cheap stat fingerprint of the copy
|
|
8
9
|
// and of the live brain (no hashing, no tree walk); a stale entry can only KEEP a copy, never remove one, and
|
|
9
10
|
// any change to the live brain's identity files (a restore, an update) invalidates it.
|
|
11
|
+
import crypto from 'node:crypto';
|
|
10
12
|
import fs from 'node:fs';
|
|
11
13
|
import path from 'node:path';
|
|
12
14
|
|
|
@@ -157,7 +159,43 @@ export function removeWithin(target, expectedParent, owned) {
|
|
|
157
159
|
return size;
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
/**
|
|
163
|
+
* RESCUE BEFORE RELEASE. kbCopyProof lists a disposable copy's operational files (logs, ruflo scratch) as
|
|
164
|
+
* `rescue`, each with the bytes it was proven with. They are copied to <brainHome>/kb-copy-rescued/<copy
|
|
165
|
+
* name>/<path> and verified there BEFORE the copy is removed. Any mismatch (the source changed since the
|
|
166
|
+
* proof, a different file already at the destination, a link in the way) throws, and the caller keeps the
|
|
167
|
+
* copy. Retention of the rescued directories is brain-footprint.mjs's (FOOTPRINT_POLICY.rescuedCopiesKept).
|
|
168
|
+
*/
|
|
169
|
+
export const RESCUE_DIR = 'kb-copy-rescued';
|
|
170
|
+
const sha256Of = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
171
|
+
const regular = (file) => { const st = lstat(file); return Boolean(st && st.isFile() && !st.isSymbolicLink()); };
|
|
172
|
+
export function rescueOperationalFiles(copyDir, rescue, brainHome, name = path.basename(copyDir)) {
|
|
173
|
+
if (!rescue?.length) return null;
|
|
174
|
+
const root = path.join(brainHome, RESCUE_DIR); const dest = path.join(root, name);
|
|
175
|
+
for (const dir of [brainHome, root, dest]) {
|
|
176
|
+
if (lstat(dir)?.isSymbolicLink()) throw new Error(`refusing to rescue into ${dir}: it is a link`);
|
|
177
|
+
}
|
|
178
|
+
for (const { file, sha256 } of rescue) {
|
|
179
|
+
const from = path.join(copyDir, file); const to = path.join(dest, file);
|
|
180
|
+
if (!regular(from) || sha256Of(from) !== sha256) throw new Error(`${file} changed after the copy was proven; the copy is kept`);
|
|
181
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
182
|
+
if (!lstat(to)) fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
|
|
183
|
+
if (!regular(to) || sha256Of(to) !== sha256) throw new Error(`could not rescue ${file} to ${to} (a different file is there); the copy is kept`);
|
|
184
|
+
}
|
|
185
|
+
// A reused rescue folder (an earlier sweep rescued, then could not release, this copy) holds the only copy of these
|
|
186
|
+
// logs once the copy goes: make it the newest, so retention keeps it among the newest five (review r2). A failure
|
|
187
|
+
// throws, and the caller keeps the copy.
|
|
188
|
+
const touched = new Date(); fs.utimesSync(dest, touched, touched);
|
|
189
|
+
return dest;
|
|
190
|
+
}
|
|
191
|
+
|
|
160
192
|
const PROOF_CACHE = '.footprint-proof-cache.json';
|
|
193
|
+
// #335: schema 1 KEPT proofs predate kept-sibling accounting (two identical preserved copies each kept the
|
|
194
|
+
// other forever); they are proven once more. A dropped cache entry can only cost a re-hash, never a byte.
|
|
195
|
+
const PROOF_CACHE_SCHEMA = 2;
|
|
196
|
+
// The proof rule a cached KEPT verdict was reached under. A verdict from an older rule (one that kept copies
|
|
197
|
+
// the current rule releases, e.g. for a log file before rescue existed) is not reused; that copy is proven again.
|
|
198
|
+
const PROOF_RULE = 2;
|
|
161
199
|
const IDENTITY_FILES = ['SOURCE.json', 'PRIVATE-STORES.json', 'COVERAGE.json', 'RVF-GENERATIONS.json'];
|
|
162
200
|
const statKey = (file) => { try { const st = fs.lstatSync(file); return `${st.size}:${Math.floor(st.mtimeMs)}`; } catch { return '-'; } };
|
|
163
201
|
const fingerprint = (dir) => [statKey(dir), ...IDENTITY_FILES.map((f) => statKey(path.join(dir, f)))].join('|');
|
|
@@ -165,21 +203,21 @@ const fingerprint = (dir) => [statKey(dir), ...IDENTITY_FILES.map((f) => statKey
|
|
|
165
203
|
export function readProofCache(brainHome) {
|
|
166
204
|
try {
|
|
167
205
|
const doc = JSON.parse(fs.readFileSync(path.join(brainHome, PROOF_CACHE), 'utf8'));
|
|
168
|
-
if (doc && typeof doc.entries === 'object' && doc.entries) return doc;
|
|
206
|
+
if (doc && doc.schemaVersion === PROOF_CACHE_SCHEMA && typeof doc.entries === 'object' && doc.entries) return doc;
|
|
169
207
|
} catch { /* none yet */ }
|
|
170
|
-
return { schemaVersion:
|
|
208
|
+
return { schemaVersion: PROOF_CACHE_SCHEMA, entries: {} };
|
|
171
209
|
}
|
|
172
210
|
|
|
173
211
|
/** The cached KEPT proof for this copy, if neither the copy nor the live brain changed since. */
|
|
174
212
|
export function cachedKept(cache, copyDir, liveDir) {
|
|
175
213
|
const hit = cache.entries[copyDir];
|
|
176
|
-
return hit && hit.copy === fingerprint(copyDir) && hit.live === fingerprint(liveDir) ? hit : null;
|
|
214
|
+
return hit && hit.rule === PROOF_RULE && hit.copy === fingerprint(copyDir) && hit.live === fingerprint(liveDir) ? hit : null;
|
|
177
215
|
}
|
|
178
216
|
|
|
179
217
|
export function rememberKept(brainHome, copyDir, liveDir, proof) {
|
|
180
218
|
try {
|
|
181
219
|
const cache = readProofCache(brainHome);
|
|
182
|
-
cache.entries[copyDir] = { copy: fingerprint(copyDir), live: fingerprint(liveDir), reason: proof.reason,
|
|
220
|
+
cache.entries[copyDir] = { rule: PROOF_RULE, copy: fingerprint(copyDir), live: fingerprint(liveDir), reason: proof.reason,
|
|
183
221
|
unique: (proof.unique || []).slice(0, 10), at: new Date().toISOString() };
|
|
184
222
|
const file = path.join(brainHome, PROOF_CACHE);
|
|
185
223
|
fs.writeFileSync(`${file}.tmp-${process.pid}`, JSON.stringify(cache));
|
|
@@ -187,7 +225,96 @@ export function rememberKept(brainHome, copyDir, liveDir, proof) {
|
|
|
187
225
|
} catch { /* uncached: the next sweep proves it again, which is only slower */ }
|
|
188
226
|
}
|
|
189
227
|
|
|
190
|
-
/** The honest remedy for a copy
|
|
191
|
-
export const keptCopyFix = (copyDir, hit) => `
|
|
228
|
+
/** The honest remedy for a kept copy: what it holds, and the explicit archive command that releases it (#335). */
|
|
229
|
+
export const keptCopyFix = (copyDir, hit) => `npx ruvnet-brain --clean --archive — ${copyDir} is kept because it holds ${
|
|
192
230
|
(hit.unique || []).some((u) => /private/i.test(u.why || '')) ? 'private ' : ''}data the live brain lacks (${
|
|
193
|
-
(hit.unique || []).slice(0, 3).map((u) => u.file).join(', ') || hit.reason});
|
|
231
|
+
(hit.unique || []).slice(0, 3).map((u) => u.file).join(', ') || hit.reason}); that copies only those files into kb.archive/ and releases the copy (or restore them into the live brain yourself)`;
|
|
232
|
+
|
|
233
|
+
const sha256File = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
234
|
+
/**
|
|
235
|
+
* Every `<archiveDir>/<stamp>/<label>` tree `--clean --archive` wrote: real directories only, links never entered.
|
|
236
|
+
* A dot-named entry is an archive still being written (or one a killed sweep left half-written); it is never
|
|
237
|
+
* a reference, so a partial archive can never account for a copy's bytes.
|
|
238
|
+
*/
|
|
239
|
+
export function archiveRoots(archiveDir) {
|
|
240
|
+
const real = (p) => { const st = lstat(p); return Boolean(st && st.isDirectory() && !st.isSymbolicLink()); };
|
|
241
|
+
if (!real(archiveDir)) return [];
|
|
242
|
+
return names(archiveDir).map((s) => path.join(archiveDir, s)).filter(real)
|
|
243
|
+
.flatMap((stampDir) => names(stampDir).filter((n) => !n.startsWith('.')).map((n) => path.join(stampDir, n)).filter(real));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Undo an archive this sweep wrote (the copy it came from is being kept, so the archive holds nothing unique). */
|
|
247
|
+
export function discardArchive(root) {
|
|
248
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
249
|
+
fs.rmSync(`${root}.json`, { force: true });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Discard an archive ONLY while the copy it came from still holds every archived byte (review 4.7.21 r2). Between
|
|
254
|
+
* the archive and the re-proof another process (an updater's reclaimBackups, which counts this archive as
|
|
255
|
+
* evidence) may have removed the copy; a re-proof of a missing directory still says "keep", so an unconditional
|
|
256
|
+
* discard would lose the bytes in both places. The copy must still be a real directory and each archived file a
|
|
257
|
+
* regular file there with the same SHA-256; otherwise the archive is kept and the reason is returned.
|
|
258
|
+
*/
|
|
259
|
+
export function discardArchiveIfMirrored({ root, copyDir, files }) {
|
|
260
|
+
const dir = lstat(copyDir);
|
|
261
|
+
if (!dir || dir.isSymbolicLink() || !dir.isDirectory()) return { discarded: false, reason: `${copyDir} is no longer a real directory` };
|
|
262
|
+
for (const { file, sha256 } of files || []) {
|
|
263
|
+
const held = path.join(copyDir, file);
|
|
264
|
+
let same = false;
|
|
265
|
+
try { same = regular(held) && sha256File(held) === sha256; } catch { same = false; }
|
|
266
|
+
if (!same) return { discarded: false, reason: `${file} is no longer identical in ${copyDir}` };
|
|
267
|
+
}
|
|
268
|
+
discardArchive(root);
|
|
269
|
+
return { discarded: true };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* #335 `--clean --archive` (explicit, never automatic): COPY exactly the files a KEPT proof named unique into
|
|
274
|
+
* `<stampDir>/<label>/<same relative path>`, verify each byte-for-byte, and write `<stampDir>/<label>.json`.
|
|
275
|
+
* Moves and deletes nothing: the caller re-proves the copy against this archive before releasing it. A link,
|
|
276
|
+
* an unsafe ancestor, a non-regular file, too little free space or any copy error archives nothing usable.
|
|
277
|
+
* ALL OR NOTHING: the files go to a dot-named temp sibling and are renamed to `<label>` only after every one
|
|
278
|
+
* verifies (the receipt likewise); a failure removes the temp, so no half-written `<label>` is ever left for a
|
|
279
|
+
* later sweep to compare against.
|
|
280
|
+
*/
|
|
281
|
+
export function archiveUniqueFiles({ copyDir, unique, stampDir, label, now = Date.now() }) {
|
|
282
|
+
if (!Array.isArray(unique) || !unique.length) return { ok: false, reason: 'no file is named unique; the copy is kept for another reason' };
|
|
283
|
+
const root = path.join(stampDir, label);
|
|
284
|
+
if (lstat(root)) return { ok: false, reason: `${root} already exists` };
|
|
285
|
+
let need = 0;
|
|
286
|
+
for (const { file, why } of unique) {
|
|
287
|
+
const st = lstat(path.join(copyDir, file));
|
|
288
|
+
if (/link|ancestor/i.test(String(why)) || !st || !st.isFile() || st.isSymbolicLink()) return { ok: false, reason: `${file} is not a regular file that can be archived (${why})` };
|
|
289
|
+
need += st.size;
|
|
290
|
+
}
|
|
291
|
+
const temp = path.join(stampDir, `.${label}.partial-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
292
|
+
const receipt = path.join(stampDir, `${label}.json`); const receiptTemp = `${temp}.json`;
|
|
293
|
+
let renamed = false;
|
|
294
|
+
try {
|
|
295
|
+
fs.mkdirSync(stampDir, { recursive: true });
|
|
296
|
+
const free = typeof fs.statfsSync === 'function' ? fs.statfsSync(stampDir) : null;
|
|
297
|
+
if (free && Number(free.bavail) * Number(free.bsize) < need + 64 * 1024 * 1024) {
|
|
298
|
+
return { ok: false, reason: `not enough free space to archive ${need} bytes` };
|
|
299
|
+
}
|
|
300
|
+
const files = [];
|
|
301
|
+
for (const { file } of unique) {
|
|
302
|
+
const src = path.join(copyDir, file); const dst = path.join(temp, file);
|
|
303
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
304
|
+
fs.copyFileSync(src, dst, fs.constants.COPYFILE_EXCL);
|
|
305
|
+
const sha256 = sha256File(src);
|
|
306
|
+
if (sha256File(dst) !== sha256) throw new Error(`archived bytes of ${file} differ from the source`);
|
|
307
|
+
files.push({ file, bytes: fs.lstatSync(dst).size, sha256 });
|
|
308
|
+
}
|
|
309
|
+
fs.writeFileSync(receiptTemp, `${JSON.stringify({ schemaVersion: 1, kind: 'ruvnet-brain-kb-archive',
|
|
310
|
+
source: copyDir, archivedAt: new Date(now).toISOString(), files }, null, 2)}\n`, { flag: 'wx' });
|
|
311
|
+
if (lstat(root) || lstat(receipt)) throw new Error(`${root} appeared while archiving`);
|
|
312
|
+
fs.renameSync(temp, root); renamed = true;
|
|
313
|
+
fs.renameSync(receiptTemp, receipt);
|
|
314
|
+
return { ok: true, root, files, bytes: need };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
fs.rmSync(temp, { recursive: true, force: true }); fs.rmSync(receiptTemp, { force: true });
|
|
317
|
+
if (renamed) fs.rmSync(root, { recursive: true, force: true });
|
|
318
|
+
return { ok: false, reason: `archive failed: ${error.message}` };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -36,6 +36,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
36
36
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
37
37
|
import { normalizeHostEvent } from './hook-input.mjs';
|
|
38
38
|
import { developmentHooksSuspended } from './development-maintenance.mjs';
|
|
39
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
39
40
|
|
|
40
41
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
41
42
|
const PLUGIN_ROOT = path.resolve(HERE, '..');
|
|
@@ -155,5 +156,5 @@ async function main() {
|
|
|
155
156
|
return out.code;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
const isMain = (
|
|
159
|
+
const isMain = isDirectInvocation(import.meta.url);
|
|
159
160
|
if (isMain) process.exit(await main().catch(() => 0));
|
|
@@ -235,7 +235,8 @@ fi
|
|
|
235
235
|
# FOOT_V computed above are reused there.)
|
|
236
236
|
if [ "$RUFLO_STATE" = "yes" ] && [ "$MEM_STATE" = "off" ]; then
|
|
237
237
|
out_to 1-2-memory-offer <<'EOF'
|
|
238
|
-
This project runs the Ruflo stack but AgentDB persistent project memory is NOT set up (.swarm/memory.db does not exist) — decisions made here are being lost between sessions. rUv's default is memory ON. If you have not already
|
|
238
|
+
This project runs the Ruflo stack but AgentDB persistent project memory is NOT set up (.swarm/memory.db does not exist) — decisions made here are being lost between sessions. rUv's default is memory ON. If you have not already mentioned it this session, state it ONCE, plainly and warmly: "One thing I noticed: this project doesn't have persistent memory turned on — AgentDB would let me carry decisions and context across sessions instead of starting cold each time; saying 'turn on memory' sets it up." If the user says to, set it up with the ruflo tools you have (a first memory_store write creates the store) and confirm with the real file path. If they decline, respect it for the rest of the session — the status line keeps them informed without nagging.
|
|
239
|
+
Optional, informational advice: mention it at most once as a statement and continue the current task. Do not open a question or approval dialog because of it; it grants no authority to install or update anything.
|
|
239
240
|
EOF
|
|
240
241
|
fi
|
|
241
242
|
# A stale mtime is not a user-facing event. SessionEnd/PreCompact own capture;
|
|
@@ -330,7 +331,8 @@ Explain it in the user's words, not the plumbing's:
|
|
|
330
331
|
in THIS project, and read the reason it prints. "opt-in required" or "anchor required" means off.
|
|
331
332
|
- TURN IT OFF: remove that env var (and `RUFLO_DAEMON_AUTOSTART=0` stops the daemon auto-starting).
|
|
332
333
|
|
|
333
|
-
Offer like this: "Ruflo can quietly tune how it recalls memory — testing changes against a frozen benchmark and only keeping what provably wins, with a receipt you can replay. It's free, it's off by default, and it does nothing until this project has enough history
|
|
334
|
+
Offer like this: "Ruflo can quietly tune how it recalls memory — testing changes against a frozen benchmark and only keeping what provably wins, with a receipt you can replay. It's free, it's off by default, and it does nothing until this project has enough history; saying 'turn on the flywheel' enables it." If they decline, respect it and never raise it again unless they ask.
|
|
335
|
+
Optional, informational advice: mention it at most once as a statement and continue the current task. Do not open a question or approval dialog because of it; it grants no authority to install or update anything.
|
|
334
336
|
EOF
|
|
335
337
|
fi
|
|
336
338
|
|
|
@@ -431,7 +433,8 @@ if [ -s "$VCACHE" ]; then
|
|
|
431
433
|
done < "$VCACHE"
|
|
432
434
|
if [ -n "$OUTDATED" ]; then
|
|
433
435
|
{ echo "[RuvNet Brain — stack updates available:$OUTDATED]"
|
|
434
|
-
echo "Mention this ONCE per session, briefly
|
|
436
|
+
echo "Mention this ONCE per session, briefly, as a statement (\"your ruflo is a version behind the latest release.\"); if already mentioned this session, stay silent."
|
|
437
|
+
echo "Optional, informational advice: mention it at most once as a statement and continue the current task. Do not open a question or approval dialog because of it; it grants no authority to install or update anything."
|
|
435
438
|
} | out_to 4-2-stack
|
|
436
439
|
fi
|
|
437
440
|
fi
|
|
@@ -23,6 +23,7 @@ import os from 'node:os';
|
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
import { normalizeToolOutcome } from './continuity-events.mjs';
|
|
26
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
26
27
|
|
|
27
28
|
const BANNER = /^Searched \d+ RuvNet repos \(/;
|
|
28
29
|
const CARD = /^⚡ FAST LANE — [^\n]*\n#1 repo=\S+ evidence=curated-capability-card\n/;
|
|
@@ -126,7 +127,7 @@ async function main() {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
function isMain() {
|
|
129
|
-
try { return
|
|
130
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
130
131
|
catch { return false; }
|
|
131
132
|
}
|
|
132
133
|
if (isMain()) main();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Keep executable strings and paths: mentioning a product in code still requires grounding.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
5
6
|
|
|
6
7
|
export function codeWithoutInertText(source, python = false) {
|
|
7
8
|
if (/\r(?!\n)|[\u2028\u2029]/.test(source)) throw new Error('Unsupported line boundary');
|
|
@@ -80,7 +81,7 @@ export function projectGroundingInput(raw) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function isMain() {
|
|
83
|
-
try { return
|
|
84
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
84
85
|
catch { return false; }
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -60,6 +60,7 @@ import crypto from 'node:crypto';
|
|
|
60
60
|
import { spawnSync } from 'node:child_process';
|
|
61
61
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
62
62
|
import { parseHookEvent, toolName } from './hook-input.mjs';
|
|
63
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
63
64
|
|
|
64
65
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
65
66
|
|
|
@@ -585,7 +586,7 @@ function main() {
|
|
|
585
586
|
// stdin here is a pipe bash has already drained and re-fed (`printf '%s' "$INPUT" | node …`), so the
|
|
586
587
|
// held-open-stdin hole ADR-055 F20 found in unprompted-runtime.mjs cannot occur on this path: there
|
|
587
588
|
// is no interactive terminal on the other end, ever.
|
|
588
|
-
if (
|
|
589
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
589
590
|
let code = 0;
|
|
590
591
|
try { code = main(); } catch (e) {
|
|
591
592
|
try { process.stderr.write(`[grounding-substance] degraded (${e && e.message}) — write permitted\n`); } catch { /* never */ }
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
architectureShadow, auditAssertions, correctionText, describeSources, loadVocabulary, logShadow,
|
|
89
89
|
relayShadow, ruvCapabilityClaims, searchedThisTurn, turnSources,
|
|
90
90
|
} from './grounding-turn-evidence.mjs';
|
|
91
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
91
92
|
|
|
92
93
|
const HOME = os.homedir();
|
|
93
94
|
const EXIT_ALLOW = 0;
|
|
@@ -238,8 +239,7 @@ async function main() {
|
|
|
238
239
|
* helpers — same guard decision-gate.mjs uses, for the same reason (entrypoint-guard-safety). */
|
|
239
240
|
function isMain() {
|
|
240
241
|
try {
|
|
241
|
-
|
|
242
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
242
|
+
return isDirectInvocation(import.meta.url);
|
|
243
243
|
} catch { return false; }
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -58,6 +58,7 @@ import { loadSettings, writeAtomic } from './user-settings.mjs';
|
|
|
58
58
|
import { classifyPrompt, loadVocabulary } from './grounding-turn-evidence.mjs';
|
|
59
59
|
import { answerOf, brainAnsweredResponse } from './grounding-answer.mjs';
|
|
60
60
|
import { RUVNET_GATE1_TERMS } from './ruvnet-gate1-pattern.mjs';
|
|
61
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
61
62
|
|
|
62
63
|
const HOME = os.homedir();
|
|
63
64
|
export const MARKER_DIR = process.env.RUVNET_GROUNDING_TURN_DIR
|
|
@@ -305,8 +306,7 @@ async function main() {
|
|
|
305
306
|
* helpers — same guard decision-gate.mjs uses, for the same reason (entrypoint-guard-safety). */
|
|
306
307
|
function isMain() {
|
|
307
308
|
try {
|
|
308
|
-
|
|
309
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
309
|
+
return isDirectInvocation(import.meta.url);
|
|
310
310
|
} catch { return false; }
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
6
7
|
|
|
7
8
|
export const FRAME_KIND = 'brain-owned-context-blocks';
|
|
8
9
|
const digest = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
@@ -73,7 +74,7 @@ export function emitOwnedContext(frame, { env = process.env, rawText = false } =
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
let direct = false;
|
|
76
|
-
try { direct =
|
|
77
|
+
try { direct = isDirectInvocation(import.meta.url); } catch {}
|
|
77
78
|
if (direct && process.argv[2] === '--ground-dir') {
|
|
78
79
|
const directory = process.argv[3]; const blocks = [];
|
|
79
80
|
for (const id of fs.readdirSync(directory).filter((id) => /^\d+-\d+-[\w-]+$/.test(id)).sort((a, b) => Number(a.split('-')[0]) - Number(b.split('-')[0]))) {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import fs from 'node:fs';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
31
32
|
|
|
32
33
|
// ── GROK CLI PAYLOADS (4.5) ──────────────────────────────────────────────────────────────────────
|
|
33
34
|
//
|
|
@@ -677,8 +678,7 @@ export function invocationLines(cmd, tools) {
|
|
|
677
678
|
// argv[1] pointing at some other file.
|
|
678
679
|
function isMain() {
|
|
679
680
|
try {
|
|
680
|
-
|
|
681
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
681
|
+
return isDirectInvocation(import.meta.url);
|
|
682
682
|
} catch {
|
|
683
683
|
return false;
|
|
684
684
|
}
|
|
@@ -215,8 +215,13 @@ if (BRAIN_OFF && entry.offBehavior === 'silence') process.exit(0);
|
|
|
215
215
|
// prompt. They stay registered and run only when RUVNET_STOP_NUDGES=full, so nothing is deleted and the
|
|
216
216
|
// tests still exercise them. The blocking hooks (decision-gate), the grounding directive and the silent
|
|
217
217
|
// capture hooks are unaffected.
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
// unprompted-speech is not exited (2026-10-25): its lesson channel is the only route a recorded correction
|
|
219
|
+
// takes to the next prompt, and exiting it whole silenced every learned lesson from 4.7.8 (D4 wire: 0 bytes).
|
|
220
|
+
// Quiet runs only its lesson/alarm producers; advocacy, promotion and anticipate offers stay silent.
|
|
221
|
+
const NUDGES_FULL = String(process.env.RUVNET_STOP_NUDGES || '').toLowerCase() === 'full';
|
|
222
|
+
const QUIET_BY_DEFAULT = new Set(['capacity-aware-parallel-work', 'grounding-turn-gate']);
|
|
223
|
+
if (QUIET_BY_DEFAULT.has(hookId) && !NUDGES_FULL) process.exit(0);
|
|
224
|
+
const QUIET_SPEECH_CHANNELS = hookId === 'unprompted-speech' && !NUDGES_FULL ? 'lesson,alarm' : null;
|
|
220
225
|
|
|
221
226
|
// Read a bounded payload before dispatch. The ground body owns the quiet-prompt
|
|
222
227
|
// decision because it resolves canonical memory beyond the hook process's cwd.
|
|
@@ -370,6 +375,7 @@ function registeredTimeoutMs(extraArgs) {
|
|
|
370
375
|
// chosen from the typed table — never from input.
|
|
371
376
|
function runHook(file, activeVersion = '') {
|
|
372
377
|
if (!fs.existsSync(file)) return 0; // nothing to run — never invent a failure
|
|
378
|
+
try { if (QUIET_SPEECH_CHANNELS && !fs.readFileSync(file, 'utf8').includes('RUVNET_UNPROMPTED_CHANNELS')) return 0; } catch { return 0; } // quiet + a <=4.7.20 spine body that ignores the narrowing (it would speak advocacy) → the old whole-hook quiet exit
|
|
373
379
|
let cmd;
|
|
374
380
|
if (entry.interpreter === 'node') {
|
|
375
381
|
cmd = process.execPath;
|
|
@@ -395,6 +401,7 @@ function runHook(file, activeVersion = '') {
|
|
|
395
401
|
// machine where `node` is not on the PATH the host hands its hooks. Additive: older bodies ignore it.
|
|
396
402
|
RUVNET_NODE_BIN: process.execPath,
|
|
397
403
|
...(['unprompted-speech', 'ground-ruvnet', 'decision-gate'].includes(hookId) ? { RUVNET_HOOK_CONTEXT_BUDGET: '1' } : {}),
|
|
404
|
+
...(QUIET_SPEECH_CHANNELS ? { RUVNET_UNPROMPTED_CHANNELS: QUIET_SPEECH_CHANNELS } : {}),
|
|
398
405
|
...(activeVersion ? { RUVNET_BRAIN_ACTIVE_VERSION: activeVersion } : {}),
|
|
399
406
|
...(BRAIN_OFF && entry.offBehavior === 'partial' ? { RUVNET_BRAIN_OFF: '1' } : {}),
|
|
400
407
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Host-neutral automatic updater. The published installer is the single coordinator for Claude
|
|
3
3
|
// Code and Codex, so lifecycle updates cannot drift into host-specific shell pipelines again.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { automaticInvocation, automaticPath, updateSource } from './automatic-update.mjs';
|
|
5
|
+
import { automaticInvocation, automaticPath, installedUpdater, updateSource } from './automatic-update.mjs';
|
|
6
6
|
import { developerCoordinatorOwner } from './developer-update-owner.mjs';
|
|
7
7
|
|
|
8
8
|
const CHILD_ENV_KEYS = new Set([
|
|
@@ -92,7 +92,20 @@ if (knowledgeAt >= 0) {
|
|
|
92
92
|
// Decide by the recorded IDENTITY verdict whenever this run wrote one; the exit code only when it did
|
|
93
93
|
// not (a network failure or an incompatible release dies before recording). Measured against the real
|
|
94
94
|
// API: an install whose profile selects no stores exits 0 on UPDATE_AVAILABLE ("All stores current").
|
|
95
|
-
|
|
95
|
+
let newer = verdict ? ['UPDATE_AVAILABLE', 'UNKNOWN'].includes(verdict) : check.status === 10;
|
|
96
|
+
// Exit 5 is a remembered rejection keyed to the KB's runtime stamp. Only the installer re-stamps it, so when the
|
|
97
|
+
// version that would run differs from the stamp, run the update anyway (AuditC #5; the coordinator does the same).
|
|
98
|
+
if (!newer && !verdict && check.status === 5) {
|
|
99
|
+
let stamped = null, target = null;
|
|
100
|
+
try { stamped = readJson(path.join(kbDir, 'RUNTIME-IDENTITY.json'))?.brainVersion || null; } catch { /* no stamp */ }
|
|
101
|
+
try {
|
|
102
|
+
if (source === 'latest') {
|
|
103
|
+
const response = await fetch('https://registry.npmjs.org/ruvnet-brain/latest', { signal: AbortSignal.timeout(5_000) });
|
|
104
|
+
target = response.ok ? (await response.json()).version || null : null;
|
|
105
|
+
} else target = installedUpdater().version || null;
|
|
106
|
+
} catch { /* unknown target: stays incompatible */ }
|
|
107
|
+
newer = Boolean(target && target !== stamped);
|
|
108
|
+
}
|
|
96
109
|
if (!newer && (verdict || check.status === 0)) {
|
|
97
110
|
note({ outcome: verdict === 'REFUSED' ? 'refused' : 'current', verdict: verdict || 'CURRENT', candidateTag, reason: '' });
|
|
98
111
|
stop();
|
|
@@ -125,7 +138,9 @@ if (knowledgeAt >= 0) {
|
|
|
125
138
|
const invocation = automaticInvocation(['--update', '--no-nightly-prompt'], { source });
|
|
126
139
|
const run = spawnSync(invocation.executable, invocation.args, {
|
|
127
140
|
env: childEnvironment(), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
|
128
|
-
|
|
141
|
+
// Above the coordinator's installer limit (135 min: a 90-minute bundle deadline plus the apply), below the
|
|
142
|
+
// SessionStart TTL (session-start-update-plane.mjs AUTO_UPDATE_POLICY.ttlSec), so an outcome is always recorded.
|
|
143
|
+
timeout: Number(process.env.RUVNET_KNOWLEDGE_UPDATE_TIMEOUT_MS || 150 * 60_000),
|
|
129
144
|
});
|
|
130
145
|
process.stdout.write(run.stdout || '');
|
|
131
146
|
process.stderr.write(run.stderr || '');
|
|
@@ -133,8 +148,11 @@ if (knowledgeAt >= 0) {
|
|
|
133
148
|
const tail = `${run.stderr || ''}\n${run.stdout || ''}`.replace(/\u001b\[[0-9;]*m/g, '')
|
|
134
149
|
.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
135
150
|
const code = run.error ? null : run.status;
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
// Exit 12 is the installer's "knowledge applied, redundant rollback cleanup still pending"; the coordinator
|
|
152
|
+
// (developer-update.mjs) accepts it as success too. Recording it FAILED spoke a false failure line.
|
|
153
|
+
const succeeded = code === 0 || code === 12;
|
|
154
|
+
record({ outcome: succeeded ? 'succeeded' : 'failed', code,
|
|
155
|
+
reason: succeeded ? '' : String(run.error?.message || tail.find((l) => /✗|error|fail/i.test(l)) || tail.at(-1) || 'no output').slice(0, 200) });
|
|
138
156
|
}
|
|
139
157
|
} catch (error) {
|
|
140
158
|
record({ outcome: 'failed', code: null, reason: error.message.slice(0, 200) });
|
|
@@ -147,6 +165,12 @@ if (knowledgeAt >= 0) {
|
|
|
147
165
|
let invocation;
|
|
148
166
|
try { invocation = automaticInvocation(['--update', '--host-sync-only', '--no-nightly-prompt']); }
|
|
149
167
|
catch (error) { process.stderr.write(`[ruvnet-brain] automatic update refused: ${error.message}\n`); process.exit(1); }
|
|
168
|
+
if (invocation.source === 'developer-suite') {
|
|
169
|
+
// The coordinator installs code packages only in its scheduled run, and its knowledge step belongs to the SessionStart
|
|
170
|
+
// self-heal (--knowledge, sized for the bundle download). Running it here under a 9-minute limit would kill it mid-apply.
|
|
171
|
+
process.stdout.write('[ruvnet-brain] coordinated updates own this machine: the scheduled nightly installs new code; knowledge updates run from the SessionStart self-heal\n');
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
150
174
|
const result = spawnSync(invocation.executable, invocation.args, {
|
|
151
175
|
// The downloaded package must not inherit unrelated API keys, cloud credentials, or tokens from
|
|
152
176
|
// the interactive host. npm's registry integrity protects the package bytes; this boundary
|
|
@@ -36,6 +36,7 @@ import fs from 'node:fs';
|
|
|
36
36
|
import os from 'node:os';
|
|
37
37
|
import path from 'node:path';
|
|
38
38
|
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Each entry knows how to enumerate the identifiers its CLI actually accepts on THIS machine.
|
|
@@ -118,7 +119,7 @@ export function check(command, opts = {}) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
const isMain = (() => {
|
|
121
|
-
try { return
|
|
122
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
122
123
|
catch { return false; }
|
|
123
124
|
})();
|
|
124
125
|
|