ruvnet-brain 4.7.19 → 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 +8 -16
- package/plugin/scripts/session-start-health.mjs +97 -10
- package/plugin/scripts/session-start-issue-alert.mjs +105 -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 +164 -47
- package/scripts/job-heartbeat.sh +145 -13
- 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 +30 -21
- 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 +549 -0
- 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 +71 -26
- 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
|
@@ -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
|
|
|
@@ -14,7 +14,18 @@
|
|
|
14
14
|
// listed with these exact bytes in the copy's own ARCHIVE-MANIFEST.json, or a public store family (named by either COVERAGE.json
|
|
15
15
|
// or the live SOURCE.json) that a newer release replaced or retired;
|
|
16
16
|
// * installer-written or reinstallable (node_modules/, the updater/validator files the installer places);
|
|
17
|
-
// * a symbolic link identical in the live brain (links are compared, never followed)
|
|
17
|
+
// * a symbolic link identical in the live brain (links are compared, never followed);
|
|
18
|
+
// * (#335) a regular file byte-identical at the same path in a KEPT sibling: another copy the caller has
|
|
19
|
+
// already decided to retain, or a `kb.archive/<stamp>/<copy>/` tree. The caller passes only copies that
|
|
20
|
+
// stay on disk, so duplicate preserved copies release down to one and no unique byte is lost.
|
|
21
|
+
// * an OPERATIONAL file the Brain's own processes (or a ruflo hook run with the KB as its cwd) wrote into
|
|
22
|
+
// the tree: a top-level update.log / forge-update.log / forge-guard-injection.log, or ruflo's
|
|
23
|
+
// .claude-flow/ scratch, each a regular file of at most RESCUE_CAP_BYTES (RESCUE_TOTAL_CAP_BYTES per
|
|
24
|
+
// copy). No bundle ships these and nothing reads them back, but a log can be the only trace of a failed
|
|
25
|
+
// night, so they are not just dropped: they are returned as `rescue` (with their bytes), and the caller
|
|
26
|
+
// copies them out with footprint-io.mjs rescueOperationalFiles BEFORE it removes the copy; a failed
|
|
27
|
+
// rescue keeps the copy. The rescued files are kept for the newest released copies (brain-footprint.mjs
|
|
28
|
+
// FOOTPRINT_POLICY.rescuedCopiesKept).
|
|
18
29
|
// Anything else — a user's own file, an unfenced store, a link the live brain lacks — KEEPS the copy, and
|
|
19
30
|
// is named. Paths are checked for symlink ancestors within both KB roots before any exemption; host
|
|
20
31
|
// aliases above those roots (such as macOS /tmp) are not traversed as part of this check. Invalid metadata,
|
|
@@ -54,11 +65,25 @@ const stemOf = (file) => path.basename(String(file)).replace(/(?:\.big)?\.rvf$/i
|
|
|
54
65
|
const storeStem = (file) => path.basename(String(file)).toLowerCase()
|
|
55
66
|
.replace(/-primer\.md$/, '').replace(/(?:\.big)?\.rvf(?:\.[a-z]+\.json)?$/, '')
|
|
56
67
|
.replace(/(?:\.big)?(?:\.(?:meta|symbols)\.json|\.passages\.jsonl)$/, '');
|
|
68
|
+
// Operational files (see the header). Top-level logs by exact name: plugin/scripts/nightly-scheduler.mjs
|
|
69
|
+
// installScheduler (update.log), kb/forge-guard-injection.mjs LOG_PATH, kb/forge-update.mjs's documented
|
|
70
|
+
// cron line (forge-update.log). .claude-flow/ is ruflo's working state, the same debris brain-footprint.mjs
|
|
71
|
+
// RUFLO_DEBRIS removes from ruflo-cwd. A larger file, a link, or anything else (.swarm/ included) stays unique,
|
|
72
|
+
// and so does every operational file past RESCUE_TOTAL_CAP_BYTES per copy (the rescue itself is bounded).
|
|
73
|
+
const OPERATIONAL_LOG = /^(?:update|forge-update|forge-guard-injection)\.log$/;
|
|
74
|
+
export const RESCUE_CAP_BYTES = 2 * 1024 * 1024;
|
|
75
|
+
export const RESCUE_TOTAL_CAP_BYTES = 16 * 1024 * 1024;
|
|
76
|
+
const isOperational = (relative) => (!relative.includes(path.sep) && OPERATIONAL_LOG.test(relative))
|
|
77
|
+
|| (relative.split(path.sep)[0] === '.claude-flow' && relative.includes(path.sep));
|
|
57
78
|
// Files the installer/updater writes into a KB that no bundle ships (bin/install.mjs placeUpdater,
|
|
58
79
|
// placeTrustedCoverageValidator, ensureVerifier; the updater's snapshot receipt). Re-created on every install.
|
|
59
|
-
|
|
80
|
+
// Every bin/install.mjs UPDATER_FILES name must be here (tests/unit/kb-copy-proof-unknown-content.test.mjs holds the
|
|
81
|
+
// two lists together): download-retry.mjs and forge-guard-injection.mjs were placed but missing, so an old copy whose
|
|
82
|
+
// placed version differed from both the live file and its own manifest was kept as "unique" (review round 2, Opus #5).
|
|
83
|
+
export const INSTALLER_WRITTEN = new Set(['coverage-integrity.mjs', 'RUNTIME-IDENTITY.json', '.refresh-snapshot.json',
|
|
60
84
|
'forge-update.mjs', 'zip-extract.mjs', 'brain-profile.mjs', 'refresh-run.mjs', 'update-storage-transaction.mjs',
|
|
61
|
-
'lifecycle-evidence-retention.mjs', 'corpus-release-identity.mjs', '
|
|
85
|
+
'lifecycle-evidence-retention.mjs', 'corpus-release-identity.mjs', 'download-retry.mjs', 'forge-guard-injection.mjs',
|
|
86
|
+
'verify-citation.mjs', 'package-lock.json', '.DS_Store']);
|
|
62
87
|
|
|
63
88
|
/** A directory that is a knowledge-base tree (SOURCE.json plus a store or the search entry point). */
|
|
64
89
|
export const isKbTree = (dir) => Boolean(lstat(path.join(dir, 'SOURCE.json')))
|
|
@@ -140,10 +165,11 @@ function metadataFailure(root) {
|
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
/**
|
|
143
|
-
* @returns {{disposable: boolean, unique: {file: string, why: string}[],
|
|
144
|
-
* always names the files that keep it
|
|
168
|
+
* @returns {{disposable: boolean, unique: {file: string, why: string}[], rescue?: {file: string, bytes: number,
|
|
169
|
+
* sha256: string}[], reason: string}} — a kept copy always names the files that keep it; a disposable copy
|
|
170
|
+
* lists the operational files that must be rescued before it is removed.
|
|
145
171
|
*/
|
|
146
|
-
export function kbCopyProof({ copyDir, liveDir }) {
|
|
172
|
+
export function kbCopyProof({ copyDir, liveDir, keptSiblings = [] }) {
|
|
147
173
|
const copy = lstat(copyDir);
|
|
148
174
|
if (!copy || copy.isSymbolicLink() || !copy.isDirectory()) return { disposable: false, unique: [], reason: 'not a real directory (a link is never entered)' };
|
|
149
175
|
const live = lstat(liveDir);
|
|
@@ -172,7 +198,19 @@ export function kbCopyProof({ copyDir, liveDir }) {
|
|
|
172
198
|
// it lists with these exact bytes is a public, re-downloadable release file.
|
|
173
199
|
const shipped = new Map((readJson(path.join(copyDir, 'ARCHIVE-MANIFEST.json'))?.files || [])
|
|
174
200
|
.filter((f) => typeof f?.path === 'string').map((f) => [path.normalize(f.path), f]));
|
|
175
|
-
|
|
201
|
+
// #335: retained siblings that may account for a file. Only real directories, never the copy or live itself.
|
|
202
|
+
const siblings = (Array.isArray(keptSiblings) ? keptSiblings : []).filter((dir) => {
|
|
203
|
+
const st = lstat(dir);
|
|
204
|
+
return st && st.isDirectory() && !st.isSymbolicLink() && path.resolve(dir) !== path.resolve(copyDir)
|
|
205
|
+
&& path.resolve(dir) !== path.resolve(liveDir);
|
|
206
|
+
});
|
|
207
|
+
let bySibling = 0;
|
|
208
|
+
const inKeptSibling = (relative) => {
|
|
209
|
+
const hit = siblings.some((dir) => safeAncestors(dir, relative) && sameBytes(path.join(copyDir, relative), path.join(dir, relative)));
|
|
210
|
+
if (hit) bySibling += 1;
|
|
211
|
+
return hit;
|
|
212
|
+
};
|
|
213
|
+
const unique = []; const rescue = []; let rescueBytes = 0;
|
|
176
214
|
let files;
|
|
177
215
|
try { files = walk(copyDir); } catch (error) { return { disposable: false, unique, reason: `unreadable copy: ${error.message}` }; }
|
|
178
216
|
for (const { relative, link, size } of files) {
|
|
@@ -183,7 +221,7 @@ export function kbCopyProof({ copyDir, liveDir }) {
|
|
|
183
221
|
continue;
|
|
184
222
|
}
|
|
185
223
|
if (isPrivate) {
|
|
186
|
-
if (!sameBytes(path.join(copyDir, relative), inLive)) {
|
|
224
|
+
if (!sameBytes(path.join(copyDir, relative), inLive) && !inKeptSibling(relative)) {
|
|
187
225
|
unique.push({ file: relative, why: lstat(inLive) ? 'private file differs from the live brain' : 'private file absent from the live brain' });
|
|
188
226
|
}
|
|
189
227
|
continue;
|
|
@@ -202,9 +240,19 @@ export function kbCopyProof({ copyDir, liveDir }) {
|
|
|
202
240
|
if (listed && listed.bytes === size && listed.sha256 === sha256File(path.join(copyDir, relative))) continue;
|
|
203
241
|
if (sameBytes(path.join(copyDir, relative), inLive)) continue; // existence alone proves no ownership or redundancy
|
|
204
242
|
if (publicNames.has(storeStem(relative))) continue; // a public store family a newer release replaced or retired
|
|
243
|
+
if (inKeptSibling(relative)) continue; // #335: these exact bytes stay in a retained sibling copy or the archive
|
|
244
|
+
if (isOperational(relative) && size <= RESCUE_CAP_BYTES && rescueBytes + size <= RESCUE_TOTAL_CAP_BYTES) {
|
|
245
|
+
rescue.push({ file: relative, bytes: size, sha256: sha256File(path.join(copyDir, relative)) });
|
|
246
|
+
rescueBytes += size;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (isOperational(relative)) { unique.push({ file: relative, why: 'an operational file past the rescue cap (2 MiB each, 16 MiB per copy)' }); continue; }
|
|
205
250
|
unique.push({ file: relative, why: 'not in the live brain, not in this copy\'s release manifest, not a public store' });
|
|
206
251
|
}
|
|
207
252
|
return unique.length
|
|
208
253
|
? { disposable: false, unique, reason: `holds ${unique.length} file(s) the live brain does not: ${unique.slice(0, 4).map((u) => u.file).join(', ')}${unique.length > 4 ? ', …' : ''}` }
|
|
209
|
-
: { disposable: true, unique, reason:
|
|
254
|
+
: { disposable: true, unique, rescue, reason: `${bySibling
|
|
255
|
+
? `${bySibling} file(s) the live brain lacks are byte-identical in a kept copy or kb.archive; the rest is in the live brain or re-downloadable`
|
|
256
|
+
: 'every private file is byte-identical in the live brain; public bytes are re-downloadable'}${
|
|
257
|
+
rescue.length ? `; ${rescue.length} operational log/scratch file(s) are rescued to the brain home first` : ''}` };
|
|
210
258
|
}
|
|
@@ -9,6 +9,7 @@ import { learningContext } from './runtime-preferences.mjs';
|
|
|
9
9
|
import { recordLearningObservation, distillObservations, observationKey } from './learning-store.mjs';
|
|
10
10
|
import { resolveRuflo } from './ruflo-bin.mjs';
|
|
11
11
|
import { recordHookFailure } from './hook-failures.mjs';
|
|
12
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
12
13
|
|
|
13
14
|
// LOAD BOUNDS (measured 2026-10-21: one busy session spawned hundreds of concurrent detached `ruflo memory store`
|
|
14
15
|
// children, which timed out against each other and wrote 826 ledger rows in a day). Owner ruling 2026-10-12 stands:
|
|
@@ -148,7 +149,7 @@ function runDetached(mode) {
|
|
|
148
149
|
} catch (error) { fail(error); } finally { if (process.env.RUVNET_LEARN_SLOT) releaseObserveSlot(process.env.RUVNET_LEARN_SLOT); }
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
const isMain = (
|
|
152
|
+
const isMain = isDirectInvocation(import.meta.url);
|
|
152
153
|
if (isMain && ['--observe', '--distill'].includes(process.argv[2])) {
|
|
153
154
|
runDetached(process.argv[2]);
|
|
154
155
|
process.exit(0);
|
|
@@ -17,23 +17,29 @@ export function learningTarget(context, { env = process.env, explicitLegacyApply
|
|
|
17
17
|
const root = path.join(context.home, '.claude', 'global-memory');
|
|
18
18
|
const chosen = loadRuntimePreferences({ env, cwd: context.projectDir }).values.learningScope === 'user';
|
|
19
19
|
if (!chosen && !explicitLegacyApply) throw new Error('user learning requires persisted user consent or explicit legacy Apply');
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// #377: the user store is a FIXED path, validated directly. It is not a project, so no Git root is
|
|
21
|
+
// resolved (a ~/.claude that is itself a Git checkout made every user write "foreign global learner
|
|
22
|
+
// root"), and a missing store says so instead of surfacing a raw lstat ENOENT.
|
|
22
23
|
const db = path.join(root, '.swarm', 'memory.db');
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
24
|
+
const entry = (file) => { try { return fs.lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
|
|
25
|
+
const [rootStat, swarmStat, dbStat] = [root, path.dirname(db), db].map(entry);
|
|
26
|
+
if (!rootStat || !swarmStat || !dbStat) throw new Error(`user store not adopted (no ${db})`);
|
|
27
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('user learning root is not a real directory');
|
|
28
|
+
if (!swarmStat.isDirectory() || swarmStat.isSymbolicLink()) throw new Error('user learning .swarm is not a real directory');
|
|
29
|
+
if (!dbStat.isFile() || dbStat.isSymbolicLink() || dbStat.nlink !== 1) throw new Error('user store is not an exclusive regular file');
|
|
30
|
+
const canonicalRoot = path.join(fs.realpathSync.native(context.home), '.claude', 'global-memory');
|
|
31
|
+
if (fs.realpathSync.native(root) !== canonicalRoot) throw new Error('user learning root is reached through a link');
|
|
32
|
+
return path.join(canonicalRoot, '.swarm', 'memory.db');
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
export function runLearningRuflo(binary, args, db, { env = process.env, deadline = Date.now() + 18_000, maxStepMs = 6000 } = {}) {
|
|
35
|
+
export function runLearningRuflo(binary, args, db, { env = process.env, deadline = Date.now() + 18_000, maxStepMs = 6000, maxBuffer = 65536 } = {}) {
|
|
30
36
|
const remaining = deadline - Date.now();
|
|
31
37
|
if (remaining <= 0) throw new Error('learning deadline exhausted');
|
|
32
38
|
const cwd = rufloRunDir(db, { root: env.RUVNET_RUFLO_CWD_ROOT || path.join(env.HOME || env.USERPROFILE || os.homedir(), '.cache', 'ruvnet-brain', 'learning-ruflo') });
|
|
33
39
|
try {
|
|
34
40
|
const invocation = rufloInvocation(binary, args);
|
|
35
41
|
return spawnSync(invocation.executable, invocation.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
|
|
36
|
-
encoding: 'utf8', shell: false, timeout: Math.min(maxStepMs, remaining), killSignal: 'SIGKILL', maxBuffer
|
|
42
|
+
encoding: 'utf8', shell: false, timeout: Math.min(maxStepMs, remaining), killSignal: 'SIGKILL', maxBuffer, windowsHide: true });
|
|
37
43
|
} finally { fs.rmSync(cwd, { force: true, recursive: true }); }
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -73,15 +79,53 @@ export function recordLearningObservation(binary, context, { tool, action, sessi
|
|
|
73
79
|
return { db, key, digest: createHash('sha256').update(value).digest('hex'), exactCli: true };
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
/**
|
|
83
|
+
* The namespace's last distillation from `ruflo memory distill status --db` (#377). That command has
|
|
84
|
+
* only a table output: the `distill_state cursor` table's Namespace column is 20 wide, so
|
|
85
|
+
* `learning-observations` prints as `learning-observat...`; a truncated cell matches by prefix, and
|
|
86
|
+
* an ambiguous or unreadable answer stays unknown. `null` with known=true means never distilled.
|
|
87
|
+
*/
|
|
88
|
+
export function parseDistillStatus(text, namespace = LEARNING_NAMESPACE) {
|
|
89
|
+
const lines = String(text || '').replace(/\u001b\[[0-9;]*m/g, '').split(/\r?\n/);
|
|
90
|
+
const start = lines.findIndex((line) => /distill_state cursor/.test(line));
|
|
91
|
+
if (start < 0) return /No distill_state cursor rows yet/.test(lines.join('\n')) ? { known: true, lastDistillAt: null } : { known: false, lastDistillAt: null };
|
|
92
|
+
const rows = [];
|
|
93
|
+
for (const line of lines.slice(start + 1)) {
|
|
94
|
+
const cells = line.split('|').map((cell) => cell.trim());
|
|
95
|
+
if (cells.length < 5 || cells[1] === 'Namespace') continue;
|
|
96
|
+
rows.push({ name: cells[1], lastRun: cells[3] });
|
|
97
|
+
}
|
|
98
|
+
const exact = rows.filter((row) => row.name === namespace);
|
|
99
|
+
const truncated = rows.filter((row) => row.name.endsWith('...') && namespace.startsWith(row.name.slice(0, -3)));
|
|
100
|
+
const matches = exact.length ? exact : truncated;
|
|
101
|
+
if (!matches.length) return { known: true, lastDistillAt: null };
|
|
102
|
+
if (matches.length > 1) return { known: false, lastDistillAt: null };
|
|
103
|
+
if (matches[0].lastRun === 'never') return { known: true, lastDistillAt: null };
|
|
104
|
+
const at = Date.parse(matches[0].lastRun);
|
|
105
|
+
return Number.isFinite(at) ? { known: true, lastDistillAt: new Date(at).toISOString() } : { known: false, lastDistillAt: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Observation count through `ruflo memory list` and the last distillation through `ruflo memory distill status` (no direct SQLite). Unknown stays unknown. */
|
|
109
|
+
export function learningStoreStatus(db, { binary = resolveRuflo(), env = process.env, run = runLearningRuflo } = {}) {
|
|
110
|
+
const unknown = { known: false, observations: 0, patterns: null, lastDistillAt: null, distillKnown: false, pendingObservations: null, oldestPendingAt: null };
|
|
111
|
+
if (!binary) return unknown;
|
|
79
112
|
try {
|
|
80
|
-
const listed =
|
|
113
|
+
const listed = run(binary, ['memory', 'list', '--namespace', LEARNING_NAMESPACE, '--limit', '100000', '--format', 'json', '--path', db], db, { env, maxBuffer: 64 * 1024 * 1024 });
|
|
81
114
|
const text = String(listed.stdout || ''); const at = text.indexOf('[');
|
|
82
|
-
if (listed.status !== 0 || at < 0) return
|
|
83
|
-
|
|
84
|
-
|
|
115
|
+
if (listed.status !== 0 || at < 0) return unknown;
|
|
116
|
+
const rows = JSON.parse(text.slice(at));
|
|
117
|
+
let distill = { known: false, lastDistillAt: null };
|
|
118
|
+
try {
|
|
119
|
+
const status = run(binary, ['memory', 'distill', 'status', '--db', db], db, { env });
|
|
120
|
+
if (!status.error && status.status === 0) distill = parseDistillStatus(status.stdout);
|
|
121
|
+
} catch { /* distillation state stays unknown */ }
|
|
122
|
+
const since = distill.lastDistillAt ? Date.parse(distill.lastDistillAt) : -Infinity;
|
|
123
|
+
// Observations recorded after the last distillation are the ones still waiting for it.
|
|
124
|
+
const pending = distill.known ? rows.filter((row) => !(Number(row?.createdAt) <= since)) : null;
|
|
125
|
+
const oldest = pending ? Math.min(...pending.map((row) => Number(row?.createdAt)).filter(Number.isFinite)) : Infinity;
|
|
126
|
+
return { known: true, observations: rows.length, patterns: null, lastDistillAt: distill.lastDistillAt, distillKnown: distill.known,
|
|
127
|
+
pendingObservations: pending ? pending.length : null, oldestPendingAt: Number.isFinite(oldest) ? new Date(oldest).toISOString() : null };
|
|
128
|
+
} catch { return unknown; }
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
/** `ruflo memory backup` then `ruflo memory distill run`; both are rUv's own commands. */
|
|
@@ -118,9 +162,11 @@ export function observeLearning(options = {}) {
|
|
|
118
162
|
if (!context.enabled) return result;
|
|
119
163
|
try {
|
|
120
164
|
result.learningDb = learningTarget(context, { env });
|
|
121
|
-
const state = learningStoreStatus(result.learningDb, { env });
|
|
165
|
+
const state = learningStoreStatus(result.learningDb, { env, ...(options.binary ? { binary: options.binary } : {}), ...(options.run ? { run: options.run } : {}) });
|
|
122
166
|
result.statusKnown = state.known; result.trajectories = state.observations; result.observations = state.observations;
|
|
123
167
|
result.patterns = state.patterns; result.lastDistillAt = state.lastDistillAt;
|
|
168
|
+
result.distillKnown = state.distillKnown; result.pendingObservations = state.pendingObservations; result.oldestPendingAt = state.oldestPendingAt;
|
|
169
|
+
if (state.lastDistillAt) result.lastTrainSeconds = Math.max(0, Math.round(((options.now ?? Date.now()) - Date.parse(state.lastDistillAt)) / 1000));
|
|
124
170
|
} catch { /* Missing consent/store/readback remains unknown. */ }
|
|
125
171
|
return result;
|
|
126
172
|
}
|
|
@@ -24,6 +24,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
import { makeLesson, updateLessons, loadLessons, ENFORCEMENT, ORIGIN, STATUS, TRIGGERS } from './lesson-store.mjs';
|
|
26
26
|
import { loadNodeSqlite } from './node-sqlite.mjs';
|
|
27
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
27
28
|
|
|
28
29
|
/** Every bridged lesson id starts with this. It is how a merge knows which rows it owns. */
|
|
29
30
|
export const BRIDGE_PREFIX = 'G-';
|
|
@@ -250,8 +251,7 @@ const argv = process.argv.slice(2);
|
|
|
250
251
|
*/
|
|
251
252
|
function isMain() {
|
|
252
253
|
try {
|
|
253
|
-
|
|
254
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
254
|
+
return isDirectInvocation(import.meta.url);
|
|
255
255
|
} catch { return false; }
|
|
256
256
|
}
|
|
257
257
|
if (isMain()) {
|