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
|
@@ -26,6 +26,7 @@ import { storeRoot } from '../kb/store-root.mjs';
|
|
|
26
26
|
import { validateCoverageDirectory, sha256File } from './coverage-integrity.mjs';
|
|
27
27
|
import { verifyPayload, verifyPayloadMembers, payloadIdFor } from './release-payload.mjs';
|
|
28
28
|
import { extractZip } from '../kb/zip-extract.mjs';
|
|
29
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
29
30
|
|
|
30
31
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
32
|
|
|
@@ -206,6 +207,9 @@ export function readCoverageClaims(readme) {
|
|
|
206
207
|
export const COVERAGE_REGEN_CMD = 'npm run test:cov';
|
|
207
208
|
|
|
208
209
|
const DIR_PRUNE = new Set(['node_modules', '.git', 'coverage', 'dist', 'clones']);
|
|
210
|
+
// Root-relative trees that are never coverage sources but can be huge or busy: agent worktrees (whole
|
|
211
|
+
// checkouts), job logs, the QE fleet's state and local caches.
|
|
212
|
+
const ROOT_PRUNE = new Set(['.claude/worktrees', 'logs', '.agentic-qe', '.cache']);
|
|
209
213
|
|
|
210
214
|
/** Minimal glob → RegExp for the shapes coverage.include/exclude actually use (`**`, `*`, `?`). */
|
|
211
215
|
function globToRe(glob) {
|
|
@@ -250,9 +254,9 @@ export async function coverageSourceSet(vitestFile = path.join(ROOT, 'vitest.con
|
|
|
250
254
|
try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
251
255
|
for (const ent of ents) {
|
|
252
256
|
const abs = path.join(dir, ent.name);
|
|
253
|
-
if (ent.isDirectory()) { if (!DIR_PRUNE.has(ent.name)) walk(abs); continue; }
|
|
254
|
-
if (!ent.isFile()) continue;
|
|
255
257
|
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
258
|
+
if (ent.isDirectory()) { if (!DIR_PRUNE.has(ent.name) && !ROOT_PRUNE.has(rel)) walk(abs); continue; }
|
|
259
|
+
if (!ent.isFile()) continue;
|
|
256
260
|
if (incl.some((r) => r.test(rel)) && !exclude.some((r) => r.test(rel))) files.push(abs);
|
|
257
261
|
}
|
|
258
262
|
};
|
|
@@ -903,6 +907,6 @@ async function main() {
|
|
|
903
907
|
if (verdict === 'UNKNOWN' && process.argv.includes('--strict')) process.exitCode = 4;
|
|
904
908
|
}
|
|
905
909
|
|
|
906
|
-
if (
|
|
910
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
907
911
|
await main();
|
|
908
912
|
}
|
|
@@ -14,6 +14,7 @@ import { decideNativeTurn, appendGatewayReceipt } from './model-routing-gateway.
|
|
|
14
14
|
import { subscriptionEnvironment, assertSubscriptionAuth, validateDispatchDecision } from './model-router-dispatch.mjs';
|
|
15
15
|
import { validateClaudeTerminalSettings } from './model-terminal-launchers.mjs';
|
|
16
16
|
import { runManagedPrompt } from './model-managed-prompt.mjs';
|
|
17
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
17
18
|
|
|
18
19
|
const REFUSED = 'Controlled Claude turn refused; no fallback.';
|
|
19
20
|
const uuid = value => /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(value || '');
|
|
@@ -386,7 +387,7 @@ export async function launchControlledClaudeTerminal({ binary, args = [], input
|
|
|
386
387
|
} finally { controller.abort(); terminal.close(); for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) signalSource.removeListener(name, cancel); }
|
|
387
388
|
}
|
|
388
389
|
|
|
389
|
-
if (
|
|
390
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
390
391
|
const args = process.argv.slice(2);
|
|
391
392
|
if (args[0] !== '--real-binary' || !args[1] || args[2] !== '--') {
|
|
392
393
|
process.stderr.write('Expected --real-binary /absolute/native/claude -- [--resume UUID]\n'); process.exitCode = 1;
|
|
@@ -6,6 +6,7 @@ import crypto from 'node:crypto';
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
import { classify } from '../config/model-router/policy.default.mjs';
|
|
9
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
9
10
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
11
|
export const MOD_SOURCE = path.join(ROOT, 'config/model-router/claude-terminal-mod');
|
|
11
12
|
export function terminalModDigest(pluginRoot) {
|
|
@@ -84,6 +85,6 @@ async function main() {
|
|
|
84
85
|
else if (mode === '--prepare') process.stdout.write(JSON.stringify(prepareClaudeTerminalMod(request)) + '\n');
|
|
85
86
|
else throw new Error('Expected --prepare, --decision, --ready, or --health');
|
|
86
87
|
}
|
|
87
|
-
if (
|
|
88
|
+
if (isDirectInvocation(import.meta.url)) main().catch(() => {
|
|
88
89
|
process.stderr.write('claude-terminal-mod: routing or activation unavailable; no fallback\n'); process.exitCode = 1;
|
|
89
90
|
});
|
|
@@ -34,6 +34,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
35
35
|
import { verifyCoverageSidecar } from './corpus-coverage-sidecar.mjs';
|
|
36
36
|
import { resolveNextCorpusSeed } from './corpus-next-seed.mjs';
|
|
37
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
37
38
|
|
|
38
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
40
|
const CORPUS_TAG = /^corpus-sha256-([0-9a-f]{64})$/;
|
|
@@ -266,7 +267,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
266
267
|
|
|
267
268
|
function isMain() {
|
|
268
269
|
try {
|
|
269
|
-
return
|
|
270
|
+
return isDirectInvocation(import.meta.url);
|
|
270
271
|
} catch {
|
|
271
272
|
return false;
|
|
272
273
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Proof that the Codex boot surface a release changed is the surface a FRESH Codex session will load (issue #391,
|
|
2
|
+
// ADR-0070, ADR-0051). A receipt that names `scripts/hook-shim.mjs`, `mcp/server.mjs` or `skills/` as changed used to stay
|
|
3
|
+
// restartScope:'unproven' forever, because only the two hook-declaration files could be cleared. Each path is proven by
|
|
4
|
+
// the strongest evidence that can be gathered without a Codex relaunch, and any path with no prover stays unproven,
|
|
5
|
+
// NAMED in the result:
|
|
6
|
+
// hooks/codex-hooks.json, hooks/hooks.json the fresh-declarations proof (scripts/codex-fresh-host-proof.mjs)
|
|
7
|
+
// skills/ that proof plus a byte-identical skills/ tree
|
|
8
|
+
// scripts/hook-shim.mjs byte-identical AND a real run of the INSTALLED shim with an unknown hook
|
|
9
|
+
// id (it exits 0 doing nothing and lists its roles; every registered Codex
|
|
10
|
+
// role must be listed)
|
|
11
|
+
// mcp/server.mjs byte-identical AND a real stdio initialize + tools/list that must
|
|
12
|
+
// include search_ruvnet
|
|
13
|
+
// The result ALWAYS reports windows that are already open as `unproven` and a host relaunch as `not-observed`. It is
|
|
14
|
+
// read-only: both real processes run against an isolated temporary HOME and brain home (the MCP server leases its
|
|
15
|
+
// generation while running), under a hard deadline, and the temp tree is removed. It never touches a receipt.
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { shellDiff } from '../plugin/scripts/host-shell-boundary.mjs';
|
|
21
|
+
|
|
22
|
+
export const BOOT_REASON_PREFIX = 'boot-level declarations changed: ';
|
|
23
|
+
export const PROVABLE_PATHS = Object.freeze(['hooks/codex-hooks.json', 'hooks/hooks.json', 'skills/',
|
|
24
|
+
'scripts/hook-shim.mjs', 'mcp/server.mjs']);
|
|
25
|
+
const MAX_CAPTURE = 64 * 1024;
|
|
26
|
+
|
|
27
|
+
/** The changed paths a machine-generated sessionSafetyReason names, or [] when it names none. */
|
|
28
|
+
export function parseChangedPaths(reason) {
|
|
29
|
+
if (typeof reason !== 'string' || !reason.startsWith(BOOT_REASON_PREFIX)) return [];
|
|
30
|
+
return reason.slice(BOOT_REASON_PREFIX.length).split(', ').map((entry) => entry.trim()).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The hook roles a Codex hooks file registers: the id after the timeout in each `... <timeoutMs> <role> [event]` command. */
|
|
34
|
+
export function registeredCodexRoles(hooksFile) {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(hooksFile, 'utf8'));
|
|
36
|
+
const roles = new Set();
|
|
37
|
+
for (const groups of Object.values(parsed.hooks || {})) {
|
|
38
|
+
for (const group of groups) {
|
|
39
|
+
for (const handler of group.hooks || []) {
|
|
40
|
+
const match = /"\s+\d+\s+([a-z][a-z0-9-]*)(?:\s+\S+)?\s*$/.exec(String(handler.command || ''));
|
|
41
|
+
if (match) roles.add(match[1]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...roles];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Windows cannot start node (or resolve DLLs, temp, shells) without these; copied only when present, never a secret.
|
|
49
|
+
const WINDOWS_NEEDS = ['SystemRoot', 'windir', 'ComSpec', 'PATHEXT'];
|
|
50
|
+
const IS_WIN = process.platform === 'win32';
|
|
51
|
+
|
|
52
|
+
/** SIGKILL the probe's whole process group (it is spawned detached, so it leads one); the child alone on Windows. */
|
|
53
|
+
function killTree(child) {
|
|
54
|
+
try {
|
|
55
|
+
if (!IS_WIN && Number.isInteger(child?.pid) && child.pid > 1) process.kill(-child.pid, 'SIGKILL');
|
|
56
|
+
else child?.kill('SIGKILL');
|
|
57
|
+
} catch { try { child?.kill('SIGKILL'); } catch { /* gone */ } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isolatedEnv(tmp, extra = {}, source = process.env) {
|
|
61
|
+
const needs = {};
|
|
62
|
+
for (const name of WINDOWS_NEEDS) if (source[name]) needs[name] = source[name];
|
|
63
|
+
const env = { ...needs, PATH: source.PATH || source.Path || '', HOME: tmp, USERPROFILE: tmp, TMPDIR: tmp,
|
|
64
|
+
RUVNET_BRAIN_HOME: path.join(tmp, 'brain'), RUVNET_BRAIN_STATE_DIR: path.join(tmp, 'state'),
|
|
65
|
+
XDG_CACHE_HOME: path.join(tmp, 'xdg'), XDG_CONFIG_HOME: path.join(tmp, 'xdgc'), CODEX_HOME: path.join(tmp, 'codex'),
|
|
66
|
+
RUVNET_BRAIN_KB: path.join(tmp, 'kb'), RUVNET_NIGHTLY: '', ...extra };
|
|
67
|
+
if (IS_WIN) { env.TEMP = tmp; env.TMP = tmp; } // USERPROFILE is already the isolated tmp; TEMP/TMP must not point at the real profile
|
|
68
|
+
return env;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runBounded(args, { cwd, env, timeoutMs }) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
let out = ''; let err = ''; let timedOut = false; let settled = false;
|
|
74
|
+
const child = spawn(process.execPath, args, { cwd, env, detached: !IS_WIN, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
75
|
+
const finish = (result) => { if (settled) return; settled = true; clearTimeout(timer); resolve({ stdout: out, stderr: err, timedOut, ...result }); };
|
|
76
|
+
const timer = setTimeout(() => { timedOut = true; killTree(child); }, timeoutMs);
|
|
77
|
+
child.stdout.on('data', (b) => { if (out.length < MAX_CAPTURE) out += b; });
|
|
78
|
+
child.stderr.on('data', (b) => { if (err.length < MAX_CAPTURE) err += b; });
|
|
79
|
+
child.once('error', (error) => finish({ code: null, signal: null, spawnError: error.message }));
|
|
80
|
+
child.once('close', (code, signal) => { killTree(child); finish({ code, signal }); });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Run the INSTALLED shim with an id it does not know. Proven only by exit 0 plus a role list covering every registered role. */
|
|
85
|
+
export async function runShimProbe({ installedRoot, roles, tmp, timeoutMs }) {
|
|
86
|
+
const shim = path.join(installedRoot, 'scripts', 'hook-shim.mjs');
|
|
87
|
+
const started = Date.now();
|
|
88
|
+
const run = await runBounded([shim, 'boot-surface-proof-unknown-hook'], { cwd: tmp, env: isolatedEnv(tmp), timeoutMs });
|
|
89
|
+
const elapsedMs = Date.now() - started;
|
|
90
|
+
if (run.timedOut) return { ok: false, elapsedMs, reason: `the installed hook shim did not exit within ${timeoutMs} ms and was killed` };
|
|
91
|
+
if (run.spawnError) return { ok: false, elapsedMs, reason: `the installed hook shim could not start: ${run.spawnError}` };
|
|
92
|
+
if (run.code !== 0) return { ok: false, elapsedMs, reason: `the installed hook shim exited ${run.code ?? run.signal} for an unknown hook id (expected 0)` };
|
|
93
|
+
const listed = /known:\s*([^\n]*)/.exec(run.stderr)?.[1]?.split(',').map((s) => s.trim()).filter(Boolean) || [];
|
|
94
|
+
const missing = roles.filter((role) => !listed.includes(role));
|
|
95
|
+
if (!listed.length) return { ok: false, elapsedMs, reason: 'the installed hook shim listed no hook roles' };
|
|
96
|
+
if (missing.length) return { ok: false, elapsedMs, listed, reason: `the installed hook shim does not list registered Codex role(s): ${missing.join(', ')}` };
|
|
97
|
+
return { ok: true, elapsedMs, rolesListed: listed.length, rolesRegistered: roles.length };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const STUB_WORKER = "require('node:fs').writeFileSync(process.env.RNB_PROOF_MARKER,String(process.pid));process.exit(0);\n";
|
|
101
|
+
|
|
102
|
+
/** Real stdio MCP handshake against the INSTALLED server: initialize, tools/list. Never starts the real search worker. */
|
|
103
|
+
export async function runMcpProbe({ installedRoot, tmp, timeoutMs }) {
|
|
104
|
+
const server = path.join(installedRoot, 'mcp', 'server.mjs');
|
|
105
|
+
const stub = path.join(tmp, 'stub-worker.cjs'); const marker = path.join(tmp, 'worker-started');
|
|
106
|
+
fs.writeFileSync(stub, STUB_WORKER);
|
|
107
|
+
const env = isolatedEnv(tmp, { RUVNET_BRAIN_CHILD_MCP: stub, RNB_PROOF_MARKER: marker });
|
|
108
|
+
const started = Date.now();
|
|
109
|
+
return await new Promise((resolve) => {
|
|
110
|
+
let buffer = ''; let settled = false; let timedOut = false;
|
|
111
|
+
const replies = new Map(); let stderr = '';
|
|
112
|
+
const child = spawn(process.execPath, [server], { cwd: tmp, env, detached: !IS_WIN, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
113
|
+
const kill = () => killTree(child);
|
|
114
|
+
const deadline = setTimeout(() => { timedOut = true; kill(); }, timeoutMs);
|
|
115
|
+
const send = (message) => { try { child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`); } catch { /* exit handler reports */ } };
|
|
116
|
+
const done = (result) => { if (settled) return; settled = true; clearTimeout(deadline); resolve({ elapsedMs: Date.now() - started, ...result }); };
|
|
117
|
+
child.stdout.on('data', (chunk) => {
|
|
118
|
+
buffer += chunk;
|
|
119
|
+
let at;
|
|
120
|
+
while ((at = buffer.indexOf('\n')) >= 0) {
|
|
121
|
+
const line = buffer.slice(0, at); buffer = buffer.slice(at + 1);
|
|
122
|
+
let message; try { message = JSON.parse(line); } catch { continue; }
|
|
123
|
+
if (message.id !== undefined) replies.set(message.id, message);
|
|
124
|
+
if (message.id === 1) { send({ method: 'notifications/initialized' }); send({ id: 2, method: 'tools/list', params: {} }); }
|
|
125
|
+
if (message.id === 2) {
|
|
126
|
+
// initialize launches the warm search worker in the background (plugin/mcp/server.mjs `void ensureChild()`); the
|
|
127
|
+
// stub worker records that it was launched, so the result reports the fact instead of assuming it.
|
|
128
|
+
setTimeout(() => { try { child.stdin.end(); } catch { /* gone */ } }, 400);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
child.stderr.on('data', (b) => { if (stderr.length < MAX_CAPTURE) stderr += b; });
|
|
133
|
+
child.once('error', (error) => done({ ok: false, reason: `the installed MCP server could not start: ${error.message}` }));
|
|
134
|
+
child.once('close', (code, signal) => {
|
|
135
|
+
killTree(child);
|
|
136
|
+
const leaseLeft = fs.existsSync(path.join(tmp, 'brain', 'leases')) && fs.readdirSync(path.join(tmp, 'brain', 'leases')).length > 0;
|
|
137
|
+
const initialize = replies.get(1); const list = replies.get(2);
|
|
138
|
+
const workerStarted = fs.existsSync(marker);
|
|
139
|
+
const base = { exit: code ?? signal, leaseLeftBehind: leaseLeft, initializeStartsWarmWorker: workerStarted };
|
|
140
|
+
if (timedOut) return done({ ok: false, ...base, reason: `the installed MCP server did not finish an initialize + tools/list within ${timeoutMs} ms and was killed` });
|
|
141
|
+
if (!initialize?.result) return done({ ok: false, ...base, reason: 'the installed MCP server did not answer initialize' });
|
|
142
|
+
const names = Array.isArray(list?.result?.tools) ? list.result.tools.map((tool) => tool?.name) : null;
|
|
143
|
+
if (!names) return done({ ok: false, ...base, reason: 'the installed MCP server did not answer tools/list' });
|
|
144
|
+
if (!names.includes('search_ruvnet')) return done({ ok: false, ...base, tools: names, reason: 'the installed MCP server tools/list does not include search_ruvnet' });
|
|
145
|
+
if (leaseLeft) return done({ ok: false, ...base, tools: names, reason: 'the installed MCP server left its lease file behind after exit' });
|
|
146
|
+
return done({ ok: true, ...base, tools: names.length });
|
|
147
|
+
});
|
|
148
|
+
send({ id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'boot-surface-proof', version: '1' } } });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {object} o
|
|
154
|
+
* @param {string[]} o.changedPaths paths the convergence receipt names as changed at boot level
|
|
155
|
+
* @param {string} o.releasedPluginRoot the release being converged to (its plugin/ directory)
|
|
156
|
+
* @param {string} o.installedPluginRoot the plugin tree Codex loads (its plugin cache for the active version)
|
|
157
|
+
* @param {object} [o.declarations] a probeFreshCodexDeclarations() result; required for the declaration paths
|
|
158
|
+
*/
|
|
159
|
+
export async function proveCodexBootSurface({ changedPaths, releasedPluginRoot, installedPluginRoot, declarations = null, timeoutMs = 15_000 } = {}) {
|
|
160
|
+
const proven = []; const unproven = [];
|
|
161
|
+
const result = (extra = {}) => ({ ok: !extra.error && changedPaths?.length > 0 && unproven.length === 0,
|
|
162
|
+
state: !extra.error && changedPaths?.length > 0 && unproven.length === 0 ? 'boot-surface-ready' : 'boot-surface-unproven',
|
|
163
|
+
scope: 'fresh-codex-boot-surface', changed: changedPaths || [], proven, unproven,
|
|
164
|
+
reason: extra.error || (unproven.length ? unproven.map((u) => `${u.path}: ${u.reason}`).join('; ') : null),
|
|
165
|
+
existingWindows: 'unproven', hostRelaunch: 'not-observed', inferenceRequests: 0, ...extra.detail });
|
|
166
|
+
if (!Array.isArray(changedPaths) || !changedPaths.length) return result({ error: 'no changed boot-level paths are recorded, so there is nothing to prove' });
|
|
167
|
+
if (!releasedPluginRoot || !installedPluginRoot) return result({ error: 'the released or installed plugin root is unknown' });
|
|
168
|
+
let diff;
|
|
169
|
+
try { diff = new Set(shellDiff(installedPluginRoot, releasedPluginRoot)); }
|
|
170
|
+
catch (error) { return result({ error: `the installed and released plugin trees could not be compared: ${error.message}` }); }
|
|
171
|
+
const identical = (rel) => !diff.has(rel);
|
|
172
|
+
const declarationsReady = declarations?.ok === true && declarations.state === 'fresh-declarations-ready';
|
|
173
|
+
const wants = (rel) => changedPaths.includes(rel);
|
|
174
|
+
let tmp = null; let shim = null; let mcp = null;
|
|
175
|
+
try {
|
|
176
|
+
if (wants('scripts/hook-shim.mjs') || wants('mcp/server.mjs')) {
|
|
177
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rnb-boot-proof-'));
|
|
178
|
+
for (const dir of ['brain', 'state', 'xdg', 'xdgc', 'codex', 'kb']) fs.mkdirSync(path.join(tmp, dir), { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
for (const rel of changedPaths) {
|
|
181
|
+
if (!PROVABLE_PATHS.includes(rel)) { unproven.push({ path: rel, reason: 'no prover exists for this path; it stays unproven' }); continue; }
|
|
182
|
+
if (rel === 'hooks/codex-hooks.json' || rel === 'hooks/hooks.json') {
|
|
183
|
+
if (declarationsReady) proven.push({ path: rel, evidence: 'fresh-declarations-ready' });
|
|
184
|
+
else unproven.push({ path: rel, reason: `fresh Codex hook declarations are not proven${declarations?.reason ? ` (${declarations.reason})` : ''}` });
|
|
185
|
+
} else if (rel === 'skills/') {
|
|
186
|
+
if (!identical(rel)) unproven.push({ path: rel, reason: 'the installed skills/ tree is not byte-identical to the release' });
|
|
187
|
+
else if (!declarationsReady) unproven.push({ path: rel, reason: `fresh Codex declarations are not proven${declarations?.reason ? ` (${declarations.reason})` : ''}` });
|
|
188
|
+
else proven.push({ path: rel, evidence: 'byte-identical skills/ tree plus fresh-declarations-ready' });
|
|
189
|
+
} else if (rel === 'scripts/hook-shim.mjs') {
|
|
190
|
+
if (!identical(rel)) { unproven.push({ path: rel, reason: 'the installed hook shim is not byte-identical to the release' }); continue; }
|
|
191
|
+
let roles;
|
|
192
|
+
try { roles = registeredCodexRoles(path.join(installedPluginRoot, 'hooks', 'codex-hooks.json')); }
|
|
193
|
+
catch (error) { unproven.push({ path: rel, reason: `the registered Codex roles could not be read: ${error.message}` }); continue; }
|
|
194
|
+
shim = await runShimProbe({ installedRoot: installedPluginRoot, roles, tmp, timeoutMs });
|
|
195
|
+
if (shim.ok) proven.push({ path: rel, evidence: `byte-identical; real run exited 0 and listed all ${shim.rolesRegistered} registered Codex roles` });
|
|
196
|
+
else unproven.push({ path: rel, reason: shim.reason });
|
|
197
|
+
} else if (rel === 'mcp/server.mjs') {
|
|
198
|
+
if (!identical(rel)) { unproven.push({ path: rel, reason: 'the installed MCP server is not byte-identical to the release' }); continue; }
|
|
199
|
+
mcp = await runMcpProbe({ installedRoot: installedPluginRoot, tmp, timeoutMs });
|
|
200
|
+
if (mcp.ok) proven.push({ path: rel, evidence: `byte-identical; real stdio initialize + tools/list returned ${mcp.tools} tools including search_ruvnet` });
|
|
201
|
+
else unproven.push({ path: rel, reason: mcp.reason });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
if (tmp) { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* best effort */ } }
|
|
206
|
+
}
|
|
207
|
+
return result({ detail: { ...(shim ? { shim } : {}), ...(mcp ? { mcp } : {}) } });
|
|
208
|
+
}
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { spawn, spawnSync } from 'node:child_process';
|
|
8
8
|
import zlib from 'node:zlib';
|
|
9
9
|
import { codexHookIdentities, codexPluginContentHash } from './codex-hook-trust.mjs';
|
|
10
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* sha256 of every regular file under package/plugin/ in a verified npm tarball, read in memory (ustar with pax `path`
|
|
@@ -335,8 +336,73 @@ export async function reconcileVerifiedCodexHookTrust({ releaseIdentity, install
|
|
|
335
336
|
} finally { await client?.close(); }
|
|
336
337
|
}
|
|
337
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Refresh a RUNNING shared Codex app-server (the managed daemon the TUI attaches to) after the Brain changed its
|
|
341
|
+
* plugin bytes or hook trust from another process (#492). Without this, that long-lived server keeps the plugin
|
|
342
|
+
* declarations it parsed earlier — codex-rs/core-plugins/src/manager.rs LoadedPluginsCache::get() only drops an
|
|
343
|
+
* entry when the active plugin ROOT changes, not when files change in place — and its loaded sessions keep their
|
|
344
|
+
* old hook runtimes. Measured 2026-10-25, codex-cli 0.164.0, isolated CODEX_HOME: after an in-place change the
|
|
345
|
+
* daemon's hooks/list still showed the old SessionEnd declaration while a fresh server showed the new one.
|
|
346
|
+
*
|
|
347
|
+
* The refresh is Codex's own: config/batchWrite with no edits and reloadUserConfig. With no edits nothing is
|
|
348
|
+
* persisted (config_manager_service.rs apply_edits writes only when config_edits is non-empty); batch_write then
|
|
349
|
+
* clears the plugin and skill caches (config_processor.rs handle_config_mutation) and reloads every loaded thread
|
|
350
|
+
* (reload_user_config). The expectedVersion check makes a concurrent config writer fail it safely. It never grants
|
|
351
|
+
* an approval, starts or stops a server, or touches a thread. No server listening = nothing stale to refresh.
|
|
352
|
+
* Transport: WebSocket over $CODEX_HOME/app-server-control/app-server-control.sock, a symlink to the physical
|
|
353
|
+
* socket (codex-rs/app-server-transport/src/transport/{mod.rs,unix_socket.rs}).
|
|
354
|
+
*/
|
|
355
|
+
export async function refreshSharedCodexAppServer({ codexHome, timeoutMs = 8000, connect } = {}) {
|
|
356
|
+
if (typeof codexHome !== 'string' || !path.isAbsolute(codexHome)) return { state: 'blocked', changed: false, reason: 'Absolute Codex home required' };
|
|
357
|
+
const rendezvous = path.join(codexHome, 'app-server-control', 'app-server-control.sock');
|
|
358
|
+
let socketPath;
|
|
359
|
+
try { socketPath = fs.realpathSync(rendezvous); if (!fs.statSync(socketPath).isSocket()) throw new Error('not a socket'); }
|
|
360
|
+
catch { return { state: 'no-shared-server', changed: false }; }
|
|
361
|
+
// ONE total deadline, started BEFORE connecting: a socket that accepts but never completes the WebSocket handshake
|
|
362
|
+
// must not hang the installer (cross-vendor review F). On timeout the connecting socket is terminated.
|
|
363
|
+
let ws; let timer; let timedOut = false;
|
|
364
|
+
const deadline = new Promise((_, reject) => { timer = setTimeout(() => { timedOut = true; reject(new Error('Shared Codex app-server refresh timed out')); }, timeoutMs); });
|
|
365
|
+
deadline.catch(() => {});
|
|
366
|
+
try {
|
|
367
|
+
const run = (async () => {
|
|
368
|
+
ws = connect ? await connect(socketPath) : await (async () => {
|
|
369
|
+
const { default: WebSocket } = await import('ws');
|
|
370
|
+
const socket = new WebSocket(`ws+unix://${socketPath}:/`);
|
|
371
|
+
ws = socket; // reachable by the timeout's teardown while the handshake is still pending
|
|
372
|
+
await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
|
|
373
|
+
return socket;
|
|
374
|
+
})();
|
|
375
|
+
if (timedOut) { try { if (ws.terminate) ws.terminate(); else ws.close(); } catch { /* already gone */ } return; }
|
|
376
|
+
const pending = new Map(); let nextId = 1;
|
|
377
|
+
ws.on('message', (data) => {
|
|
378
|
+
let row; try { row = JSON.parse(String(data)); } catch { return; }
|
|
379
|
+
const item = pending.get(row.id); if (!item) return;
|
|
380
|
+
pending.delete(row.id);
|
|
381
|
+
// Native error text can quote private config values; report only which call was refused.
|
|
382
|
+
if (row.error) item.reject(new Error(`Shared Codex app-server rejected ${item.method}`)); else item.resolve(row.result);
|
|
383
|
+
});
|
|
384
|
+
const rpc = (method, params) => new Promise((resolve, reject) => {
|
|
385
|
+
const id = nextId++; pending.set(id, { resolve, reject, method }); ws.send(JSON.stringify({ id, method, params }));
|
|
386
|
+
});
|
|
387
|
+
await rpc('initialize', { clientInfo: { name: 'rnb_host_refresh', version: '1' }, capabilities: { experimentalApi: true } });
|
|
388
|
+
ws.send(JSON.stringify({ method: 'initialized' }));
|
|
389
|
+
const read = await rpc('config/read', { includeLayers: true });
|
|
390
|
+
const users = (read?.layers || []).filter((r) => r.name?.type === 'user');
|
|
391
|
+
if (users.length !== 1 || typeof users[0].version !== 'string' || typeof users[0].name.file !== 'string') {
|
|
392
|
+
throw new Error('Shared Codex app-server user config version unavailable');
|
|
393
|
+
}
|
|
394
|
+
await rpc('config/batchWrite', { filePath: users[0].name.file, expectedVersion: users[0].version, reloadUserConfig: true, edits: [] });
|
|
395
|
+
})();
|
|
396
|
+
run.catch(() => {}); // a late failure after the deadline is already reported
|
|
397
|
+
await Promise.race([run, deadline]);
|
|
398
|
+
return { state: 'refreshed', changed: false, approvalsGranted: 0 };
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return { state: 'failed', changed: false, reason: error.message };
|
|
401
|
+
} finally { clearTimeout(timer); try { if (timedOut && ws?.terminate) ws.terminate(); else ws?.close(); } catch { /* already closed */ } }
|
|
402
|
+
}
|
|
403
|
+
|
|
338
404
|
// Explicit CLI only; importing the helper never starts a process or reads stdin.
|
|
339
|
-
if (
|
|
405
|
+
if (isDirectInvocation(import.meta.url)
|
|
340
406
|
&& process.argv[2] === '--reconcile-installed') {
|
|
341
407
|
const started = Date.now(); let emitted = false; let reconcileStarted = false;
|
|
342
408
|
const emit = (receipt) => { if (!emitted) { emitted = true; process.stdout.write(JSON.stringify(receipt) + '\n'); } };
|
|
@@ -78,17 +78,56 @@ export function codexHookIdentities(hooksFile, keySource = 'ruvnet-brain@ruvnet-
|
|
|
78
78
|
* OLD file's hash at the same key ('modified') or that did not exist before ('untrusted'). A hook the user
|
|
79
79
|
* had already trusted at the old hash stops running until reviewed; that is the whole point of the list.
|
|
80
80
|
*/
|
|
81
|
-
export function codexTrustChanges(prevHooksFile, nextHooksFile, keySource) {
|
|
81
|
+
export function codexTrustChanges(prevHooksFile, nextHooksFile, keySource, { contentChanged = false } = {}) {
|
|
82
82
|
const prev = codexHookIdentities(prevHooksFile, keySource);
|
|
83
83
|
const next = codexHookIdentities(nextHooksFile, keySource);
|
|
84
84
|
const changes = [];
|
|
85
85
|
for (const [key, hash] of next) {
|
|
86
86
|
if (!prev.has(key)) changes.push({ key, status: 'untrusted' });
|
|
87
87
|
else if (prev.get(key) !== hash) changes.push({ key, status: 'modified' });
|
|
88
|
+
// Codex 0.164+: an unchanged declaration is still "modified" once the plugin's bytes change, because the approval
|
|
89
|
+
// also pins trusted_content_hash (plugin_trust.rs matches_hash; legacy approvals pin a baseline the same way).
|
|
90
|
+
else if (contentChanged) changes.push({ key, status: 'modified', reason: 'plugin-content' });
|
|
88
91
|
}
|
|
89
92
|
return changes;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Does this Codex approve plugin hooks by content as well as declaration? True from codex-cli 0.164.0, where
|
|
97
|
+
* discovery.rs reports currentHash "<declaration>/<content>" and plugin_trust.rs checks trusted_content_hash.
|
|
98
|
+
* Unknown or unparseable versions answer null so a caller can fail toward asking for review.
|
|
99
|
+
*/
|
|
100
|
+
export function codexUsesContentTrust(versionText) {
|
|
101
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(String(versionText || ''));
|
|
102
|
+
if (!m) return null;
|
|
103
|
+
const [major, minor] = [Number(m[1]), Number(m[2])];
|
|
104
|
+
return major > 0 || minor >= 164;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Codex writes this under the installed plugin itself (codex-rs/core-plugins/src/command_migration.rs); a candidate
|
|
108
|
+
// release never carries it, so it says nothing about whether the release's bytes changed.
|
|
109
|
+
const CODEX_GENERATED_PREFIX = '.codex-plugin/migrated-command-skills/';
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Would Codex see different plugin contents after replacing `installedRoot` with `candidateRoot`? Compares exactly
|
|
113
|
+
* the files codexPluginContentHash() fingerprints (Codex's own generated migrations aside). Anything unreadable
|
|
114
|
+
* counts as changed: the answer is used to warn, so it fails toward asking the user to review.
|
|
115
|
+
*/
|
|
116
|
+
export function codexPluginContentChanged(installedRoot, candidateRoot, options = {}) {
|
|
117
|
+
if (!installedRoot || !candidateRoot) return true;
|
|
118
|
+
const files = (root) => {
|
|
119
|
+
const out = new Map();
|
|
120
|
+
codexPluginContentHash(root, { ...options, onFile: (rel, bytes) => {
|
|
121
|
+
if (!rel.startsWith(CODEX_GENERATED_PREFIX)) out.set(rel, crypto.createHash('sha256').update(bytes).digest('hex'));
|
|
122
|
+
} });
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
const a = files(installedRoot); const b = files(candidateRoot);
|
|
127
|
+
return a.size !== b.size || [...b].some(([rel, h]) => a.get(rel) !== h);
|
|
128
|
+
} catch { return true; }
|
|
129
|
+
}
|
|
130
|
+
|
|
92
131
|
/** The exact instruction for a user whose Brain hooks Codex is holding back. */
|
|
93
132
|
export const CODEX_TRUST_ACTION = 'Start Codex (interactive, in any project). At the "Hooks need review" prompt choose'
|
|
94
133
|
+ ' "Trust all and continue" — or type /hooks and trust each RuvNet Brain hook. Until then Codex skips them'
|
|
@@ -21,6 +21,7 @@ import fs from 'node:fs';
|
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { execFileSync } from 'node:child_process';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
24
25
|
|
|
25
26
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
export const HISTORY_FILE = path.join('data', 'codex-hook-wrapper-history.json');
|
|
@@ -122,6 +123,6 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
122
123
|
return 2;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
if (
|
|
126
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
126
127
|
process.exitCode = main();
|
|
127
128
|
}
|
|
@@ -10,6 +10,7 @@ import { probeFreshCodexDeclarations, classifyFreshCodexDeclarations } from './c
|
|
|
10
10
|
import { runtimeSnapshot } from './model-routing-launchers.mjs';
|
|
11
11
|
import { maintenanceStatus } from '../plugin/scripts/development-maintenance.mjs';
|
|
12
12
|
import { proofFile, proofDigest, verifyReleasedHostRuntime, assertHostRuntimeUnchanged } from './codex-host-proof-runtime.mjs';
|
|
13
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
13
14
|
|
|
14
15
|
const OWNER = 'ruvnet-brain@ruvnet-brain';
|
|
15
16
|
// The diagnostic verifies the canonical ordinary-work route, never selects a separate model.
|
|
@@ -241,6 +242,6 @@ export async function executionProofMain(args, { run = produceOwnedCodexExecutio
|
|
|
241
242
|
return result.ok ? 0 : 1;
|
|
242
243
|
} catch (error) { output(`${JSON.stringify(failure(error.message))}\n${usage}`); return 2; }
|
|
243
244
|
}
|
|
244
|
-
if (
|
|
245
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
245
246
|
process.exitCode = await executionProofMain(process.argv.slice(2));
|
|
246
247
|
}
|
|
@@ -20,6 +20,7 @@ import os from 'node:os';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { auditCompletionClaims, claudeTurnEvents, extractCommitments } from '../plugin/scripts/completion-claim-evidence.mjs';
|
|
23
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
23
24
|
|
|
24
25
|
const argv = process.argv.slice(2);
|
|
25
26
|
const opt = (flag, fallback) => { const i = argv.indexOf(flag); return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback; };
|
|
@@ -110,5 +111,5 @@ function main() {
|
|
|
110
111
|
console.log(JSON.stringify(summary, null, 2));
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
const isMain = (
|
|
114
|
+
const isMain = isDirectInvocation(import.meta.url);
|
|
114
115
|
if (isMain) main();
|
|
@@ -102,9 +102,14 @@ export function buildHealthRecommendations({ memory = null, learning = null } =
|
|
|
102
102
|
|
|
103
103
|
// 3. A learner that has not trained in days. Installed-but-dormant is a DEFECT, not a neutral
|
|
104
104
|
// state — the entire point of shipping a learning system is that it runs.
|
|
105
|
+
// #377: "quiet" means observations have WAITED more than two days for a distillation, measured
|
|
106
|
+
// from `ruflo memory distill status`. An unknown distillation state, or a learner that is caught
|
|
107
|
+
// up (nothing recorded since its last distillation), never shows this card.
|
|
105
108
|
const STALE_TRAIN_SECONDS = 60 * 60 * 24 * 2; // two days
|
|
106
|
-
const age =
|
|
107
|
-
|
|
109
|
+
const age = Number(learning?.lastTrainSeconds);
|
|
110
|
+
const waited = learning?.oldestPendingAt ? (Date.now() - Date.parse(learning.oldestPendingAt)) / 1000 : NaN;
|
|
111
|
+
if (learning?.enabled !== false && learning?.statusKnown && learning.distillKnown === true
|
|
112
|
+
&& learning.pendingObservations > 0 && Number.isFinite(waited) && waited > STALE_TRAIN_SECONDS) {
|
|
108
113
|
recs.push(makeRecommendation({
|
|
109
114
|
id: 'learning:train',
|
|
110
115
|
scope: learning?.scope === 'user' ? 'user' : 'project',
|
|
@@ -115,7 +120,7 @@ export function buildHealthRecommendations({ memory = null, learning = null } =
|
|
|
115
120
|
plainImpact:
|
|
116
121
|
'Runs one local training cycle so recent work becomes patterns it can reuse next time. '
|
|
117
122
|
+ 'Uses the same AgentDB, retains a checked local snapshot, and verifies pattern counts separately from recorded observations.',
|
|
118
|
-
evidence: [{ observed: `last distillation ${
|
|
123
|
+
evidence: [{ observed: `last distillation ${Number.isFinite(age) ? (age / 86400).toFixed(1) + " days ago" : "not recorded"}; ${learning.pendingObservations} of ${learning?.observations ?? learning?.trajectories ?? 0} observations waiting, oldest ${(waited / 86400).toFixed(1)} days${learning?.learningDb ? ` in ${learning.learningDb}` : ''}` }],
|
|
119
124
|
cost: { time: 'under a minute', risk: 'local structural patterns; snapshot retained; automatic exact restore unavailable' },
|
|
120
125
|
change: { human: 'run one training cycle so captured work becomes reusable patterns' },
|
|
121
126
|
undo: { human: 'retain the WAL-safe snapshot for supervised offline recovery; automatic exact restore is unavailable' },
|
|
@@ -48,6 +48,7 @@ import { CANARY_VERDICT_KIND, CORPUS_TAG_PATTERN, REQUIRED_CANARY_CHECKS } from
|
|
|
48
48
|
import { inventoryFootprint } from '../plugin/scripts/brain-footprint.mjs';
|
|
49
49
|
import { eligibleRepositoryStanding } from '../plugin/scripts/coverage-integrity.mjs';
|
|
50
50
|
import { TRANSIENT_HTTP, retryTransientAsync } from './transient-gh.mjs';
|
|
51
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
51
52
|
|
|
52
53
|
export const PUBLIC_API = 'https://api.github.com';
|
|
53
54
|
export const FRESHNESS_LIMIT_MS = 48 * 3_600_000;
|
|
@@ -579,6 +580,6 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
579
580
|
return record.verdict === 'PASS' ? 0 : 1;
|
|
580
581
|
}
|
|
581
582
|
|
|
582
|
-
if (
|
|
583
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
583
584
|
main().then((code) => { process.exitCode = code; });
|
|
584
585
|
}
|
|
@@ -34,6 +34,7 @@ import { canonicalJson, digest, fileIdentity, sha256File, validateGistAggregateR
|
|
|
34
34
|
import { auditCorpusStores, isPassingState } from './rvf-index-audit.mjs';
|
|
35
35
|
import { readDiagnosticAccuracyReport } from './oracle/retrieval-accuracy.mjs';
|
|
36
36
|
import { loadFixture, readRecallReport } from './oracle/repo-recall.mjs';
|
|
37
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
37
38
|
|
|
38
39
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
39
40
|
const REQUIRED_SUFFIXES = [
|
|
@@ -571,8 +572,7 @@ async function main() {
|
|
|
571
572
|
// with no archive and no receipt on disk. Same defect, same fix as plugin/scripts/hook-input.mjs:518.
|
|
572
573
|
function isMain() {
|
|
573
574
|
try {
|
|
574
|
-
|
|
575
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
575
|
+
return isDirectInvocation(import.meta.url);
|
|
576
576
|
} catch {
|
|
577
577
|
return false;
|
|
578
578
|
}
|
|
@@ -26,6 +26,7 @@ import fs from 'node:fs';
|
|
|
26
26
|
import path from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import { eligibleRepositoryStanding, validateCoverageLedger } from '../plugin/scripts/coverage-integrity.mjs';
|
|
29
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
29
30
|
|
|
30
31
|
export const COVERAGE_ASSET = 'CORPUS-COVERAGE.json';
|
|
31
32
|
export const COVERAGE_RECEIPT_ASSET = 'coverage-receipt.json';
|
|
@@ -195,7 +196,7 @@ export function main(argv = process.argv.slice(2), { stdout = process.stdout, st
|
|
|
195
196
|
|
|
196
197
|
function isMain() {
|
|
197
198
|
try {
|
|
198
|
-
return
|
|
199
|
+
return isDirectInvocation(import.meta.url);
|
|
199
200
|
} catch {
|
|
200
201
|
return false;
|
|
201
202
|
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import fs from 'node:fs';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
import { pathToFileURL } from 'node:url';
|
|
37
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
37
38
|
|
|
38
39
|
export const MIN_PIPELINE_VERSION = '4.3.38'; // sync-version-ignore: a fixed pipeline floor, not the current version
|
|
39
40
|
/** The dispatcher's second schedule entry; corpus-nightly-dispatch.yml's `on.schedule` must carry exactly this line. */
|
|
@@ -188,6 +189,6 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
|
|
|
188
189
|
return decision.verdict === 'fail' ? 1 : 0;
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
if (
|
|
192
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
192
193
|
main().then((code) => { process.exitCode = code; });
|
|
193
194
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Correlation is not authorization: protected-release still enforces its own release authority.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
4
5
|
|
|
5
6
|
export function selectCorpusDispatch(runs, { sha, dispatchId, notBefore }) {
|
|
6
7
|
if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^corpus-\d+-\d+$/.test(dispatchId || '')
|
|
@@ -12,7 +13,7 @@ export function selectCorpusDispatch(runs, { sha, dispatchId, notBefore }) {
|
|
|
12
13
|
return matches[0] || null;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
if (
|
|
16
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
16
17
|
const response = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
17
18
|
const run = selectCorpusDispatch(response.workflow_runs || [], {
|
|
18
19
|
sha: process.env.CANDIDATE_SHA, dispatchId: process.env.CORPUS_DISPATCH_ID,
|
|
@@ -62,6 +62,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
62
62
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
63
63
|
import { COVERAGE_ASSET, COVERAGE_RECEIPT_ASSET, bindCoverageToReceipt, verifyCoverageSidecar } from './corpus-coverage-sidecar.mjs';
|
|
64
64
|
import { retryTransientResult } from './transient-gh.mjs';
|
|
65
|
+
import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
|
|
65
66
|
|
|
66
67
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
67
68
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
@@ -423,7 +424,7 @@ export async function main(argv = process.argv.slice(2), { run = defaultRun, std
|
|
|
423
424
|
// temp directories, and the CLI then exits 0 having done nothing.
|
|
424
425
|
function isMain() {
|
|
425
426
|
try {
|
|
426
|
-
return
|
|
427
|
+
return isDirectInvocation(import.meta.url);
|
|
427
428
|
} catch {
|
|
428
429
|
return false;
|
|
429
430
|
}
|