ruvnet-brain 4.7.20 → 4.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +494 -97
  3. package/kb/download-retry.mjs +93 -21
  4. package/kb/forge-guard-injection.mjs +277 -0
  5. package/kb/forge-update.mjs +166 -38
  6. package/kb/lifecycle-evidence-retention.mjs +151 -14
  7. package/kb/model-requirements.mjs +4 -1
  8. package/kb/refresh-run.mjs +175 -27
  9. package/kb/verify-citation.mjs +15 -2
  10. package/package.json +3 -1
  11. package/plugin/.claude-plugin/plugin.json +1 -1
  12. package/plugin/.codex-plugin/plugin.json +1 -1
  13. package/plugin/mcp/managed-cli-interface.mjs +16 -8
  14. package/plugin/scripts/adr-currency-gate.mjs +2 -1
  15. package/plugin/scripts/advisory-contract.mjs +21 -0
  16. package/plugin/scripts/advocacy-route.mjs +5 -4
  17. package/plugin/scripts/agentdb-recall.mjs +15 -5
  18. package/plugin/scripts/automatic-update.mjs +9 -1
  19. package/plugin/scripts/brain-confirmation.mjs +2 -1
  20. package/plugin/scripts/brain-footprint.mjs +72 -11
  21. package/plugin/scripts/capability-registry.mjs +1 -1
  22. package/plugin/scripts/capacity-aware-parallel-work.mjs +2 -1
  23. package/plugin/scripts/codex-overnight-notice.mjs +33 -0
  24. package/plugin/scripts/completion-claim-evidence.mjs +56 -12
  25. package/plugin/scripts/continuation-gate.mjs +28 -9
  26. package/plugin/scripts/decision-gate.mjs +13 -63
  27. package/plugin/scripts/decision-outcomes.mjs +36 -0
  28. package/plugin/scripts/decision-recall-gate.mjs +40 -5
  29. package/plugin/scripts/decision-record.mjs +8 -3
  30. package/plugin/scripts/degradation-watch.mjs +2 -1
  31. package/plugin/scripts/developer-update-lock.mjs +172 -33
  32. package/plugin/scripts/developer-update-owner.mjs +8 -5
  33. package/plugin/scripts/developer-update-policy.mjs +4 -1
  34. package/plugin/scripts/developer-update.mjs +155 -53
  35. package/plugin/scripts/doc-currency.mjs +2 -16
  36. package/plugin/scripts/duplicate-gate.mjs +2 -1
  37. package/plugin/scripts/entrypoint.mjs +34 -0
  38. package/plugin/scripts/footprint-io.mjs +135 -8
  39. package/plugin/scripts/grok-hook-adapter.mjs +2 -1
  40. package/plugin/scripts/ground-ruvnet.sh +6 -3
  41. package/plugin/scripts/grounding-answer.mjs +2 -1
  42. package/plugin/scripts/grounding-code-projection.mjs +2 -1
  43. package/plugin/scripts/grounding-substance.mjs +2 -1
  44. package/plugin/scripts/grounding-turn-gate.mjs +2 -2
  45. package/plugin/scripts/grounding-turn-mark.mjs +2 -2
  46. package/plugin/scripts/hook-context-budget.mjs +2 -1
  47. package/plugin/scripts/hook-input.mjs +2 -2
  48. package/plugin/scripts/hook-shim.mjs +9 -2
  49. package/plugin/scripts/host-update.mjs +29 -5
  50. package/plugin/scripts/identifier-preflight.mjs +2 -1
  51. package/plugin/scripts/kb-copy-proof.mjs +57 -9
  52. package/plugin/scripts/learn-capture.mjs +2 -1
  53. package/plugin/scripts/learning-store.mjs +62 -16
  54. package/plugin/scripts/lesson-bridge.mjs +2 -2
  55. package/plugin/scripts/lesson-promote.mjs +83 -22
  56. package/plugin/scripts/md-stamp.mjs +2 -1
  57. package/plugin/scripts/native-turn-transcript.mjs +98 -6
  58. package/plugin/scripts/nightly-scheduler.mjs +10 -5
  59. package/plugin/scripts/package-recommender.mjs +2 -2
  60. package/plugin/scripts/project-store-resolver.mjs +19 -0
  61. package/plugin/scripts/routing-outcome-capture.mjs +2 -1
  62. package/plugin/scripts/session-snapshot-hook.mjs +24 -4
  63. package/plugin/scripts/session-start-core.mjs +5 -15
  64. package/plugin/scripts/session-start-health.mjs +97 -10
  65. package/plugin/scripts/session-start-issue-alert.mjs +63 -0
  66. package/plugin/scripts/session-start-update-plane.mjs +15 -1
  67. package/plugin/scripts/signal-watch.mjs +2 -1
  68. package/plugin/scripts/spend-guard.mjs +2 -1
  69. package/plugin/scripts/substitution-wall.mjs +2 -1
  70. package/plugin/scripts/turn-outcome-capture.mjs +7 -3
  71. package/plugin/scripts/unprompted-runtime.mjs +20 -3
  72. package/plugin/scripts/update-apply.mjs +186 -15
  73. package/plugin/skills/release-proof/scripts/release-proof.mjs +2 -1
  74. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +2 -2
  75. package/plugin/skills/ruvnet-brain/SKILL.md +3 -2
  76. package/scripts/adr-072-completion.mjs +2 -1
  77. package/scripts/agentdb-context.mjs +2 -1
  78. package/scripts/agentdb-fleet-doctor.mjs +30 -8
  79. package/scripts/approved-runtime.mjs +2 -1
  80. package/scripts/architecture-review-lock.mjs +2 -1
  81. package/scripts/ascii-drift.mjs +2 -1
  82. package/scripts/brain-latency-50.mjs +2 -1
  83. package/scripts/brain-novice-50.mjs +2 -16
  84. package/scripts/brain-score.mjs +13 -2
  85. package/scripts/build-bundle.mjs +2 -1
  86. package/scripts/build-concepts.mjs +2 -1
  87. package/scripts/candidate-ci-receipt.mjs +2 -1
  88. package/scripts/candidate-host-evidence.mjs +2 -1
  89. package/scripts/card-from-source.mjs +2 -1
  90. package/scripts/ci/warm-brain-models.mjs +2 -1
  91. package/scripts/claims-verify.mjs +7 -3
  92. package/scripts/claude-controlled-terminal.mjs +2 -1
  93. package/scripts/claude-terminal-mod.mjs +2 -1
  94. package/scripts/code-release-corpus.mjs +2 -1
  95. package/scripts/codex-hook-trust-reconcile.mjs +67 -1
  96. package/scripts/codex-hook-trust.mjs +40 -1
  97. package/scripts/codex-hook-wrapper-history.mjs +2 -1
  98. package/scripts/codex-host-execution-proof.mjs +2 -1
  99. package/scripts/completion-claim-replay.mjs +2 -1
  100. package/scripts/console-engine.mjs +8 -3
  101. package/scripts/corpus-canary.mjs +2 -1
  102. package/scripts/corpus-candidate.mjs +2 -2
  103. package/scripts/corpus-coverage-sidecar.mjs +2 -1
  104. package/scripts/corpus-dispatch-decision.mjs +2 -1
  105. package/scripts/corpus-dispatch-receipt.mjs +2 -1
  106. package/scripts/corpus-next-seed.mjs +2 -1
  107. package/scripts/corpus-qa.mjs +2 -1
  108. package/scripts/corpus-reconcile.mjs +2 -2
  109. package/scripts/corpus-watchdog.mjs +2 -1
  110. package/scripts/count-chunks.mjs +2 -1
  111. package/scripts/customer-state-matrix.mjs +2 -1
  112. package/scripts/derive-passage-content-map.mjs +2 -1
  113. package/scripts/development-push-check.mjs +2 -1
  114. package/scripts/dispatch-receipt.mjs +2 -1
  115. package/scripts/doc-currency.mjs +2 -1
  116. package/scripts/dream-issue-gate.mjs +2 -1
  117. package/scripts/dual-host-deliberation.mjs +2 -16
  118. package/scripts/dual-host-suggest.mjs +2 -16
  119. package/scripts/eval-brain.mjs +7 -3
  120. package/scripts/execution-policy.mjs +2 -1
  121. package/scripts/execution-preflight.mjs +2 -1
  122. package/scripts/falsify.mjs +2 -1
  123. package/scripts/fix-metaharness-memretrieve.mjs +2 -1
  124. package/scripts/fix-workstream.mjs +2 -1
  125. package/scripts/full-hints.mjs +2 -1
  126. package/scripts/full-suite-gate.mjs +2 -1
  127. package/scripts/grok-host.mjs +68 -0
  128. package/scripts/hook-qualify-core.mjs +3 -1
  129. package/scripts/hook-qualify.mjs +2 -1
  130. package/scripts/hook-retirement-check.mjs +2 -1
  131. package/scripts/host-registry.mjs +2 -1
  132. package/scripts/independent-review-receipt.mjs +2 -1
  133. package/scripts/ingest-new-repos.mjs +2 -1
  134. package/scripts/ingest-repo.mjs +12 -2
  135. package/scripts/integration-evidence.mjs +2 -1
  136. package/scripts/issue-fix.mjs +7 -24
  137. package/scripts/issue-watch.mjs +116 -40
  138. package/scripts/job-heartbeat.sh +54 -19
  139. package/scripts/key-canary.mjs +9 -3
  140. package/scripts/latency-to-surface.mjs +2 -1
  141. package/scripts/learning-replay-cli.mjs +97 -19
  142. package/scripts/learning-replay-execution.mjs +2 -0
  143. package/scripts/learning-replay-fixture.mjs +3 -0
  144. package/scripts/learning-replay-proof.mjs +14 -13
  145. package/scripts/learning-replay.mjs +3 -3
  146. package/scripts/learnings.mjs +2 -1
  147. package/scripts/lesson-ratify.mjs +88 -40
  148. package/scripts/loop-checkpoint.mjs +2 -1
  149. package/scripts/metaharness-receipts.mjs +2 -1
  150. package/scripts/model-catalog.mjs +2 -1
  151. package/scripts/model-currency-evidence.mjs +6 -4
  152. package/scripts/model-currency.mjs +33 -3
  153. package/scripts/model-native-catalog.mjs +2 -1
  154. package/scripts/model-router-agent-hook.mjs +2 -1
  155. package/scripts/model-router-dispatch.mjs +2 -1
  156. package/scripts/model-router-engine.mjs +2 -1
  157. package/scripts/model-router-outcome.mjs +2 -1
  158. package/scripts/model-router-setup.mjs +2 -1
  159. package/scripts/model-routing-eval.mjs +2 -1
  160. package/scripts/model-routing-gateway.mjs +2 -1
  161. package/scripts/model-routing-launchers.mjs +4 -1
  162. package/scripts/model-terminal-gateway.mjs +2 -1
  163. package/scripts/model-terminal-launchers.mjs +2 -1
  164. package/scripts/model-weekly-analyst.mjs +2 -1
  165. package/scripts/model-weekly-cycle.mjs +2 -1
  166. package/scripts/model-weekly-qualification.mjs +2 -1
  167. package/scripts/nightly-gists.sh +26 -20
  168. package/scripts/nightly-two-run-proof.mjs +2 -1
  169. package/scripts/nightly-watchdog.mjs +158 -47
  170. package/scripts/no-silent-substitution.mjs +2 -1
  171. package/scripts/notify.sh +11 -6
  172. package/scripts/ntfy-inbox.mjs +41 -18
  173. package/scripts/onboarding-console.mjs +47 -8
  174. package/scripts/oracle/abstain-threshold-sweep.mjs +2 -1
  175. package/scripts/oracle/abstain-trace.mjs +2 -1
  176. package/scripts/oracle/doc2query-generate.mjs +2 -1
  177. package/scripts/oracle/doc2query-reach.mjs +2 -1
  178. package/scripts/oracle/judge-train.mjs +2 -1
  179. package/scripts/oracle/need-set-split.mjs +2 -1
  180. package/scripts/oracle/produce-questions.mjs +2 -16
  181. package/scripts/oracle/repo-recall.mjs +2 -2
  182. package/scripts/oracle/retrieval-accuracy.mjs +2 -2
  183. package/scripts/oracle/sona-query-adapter-eval.mjs +2 -1
  184. package/scripts/oracle/source-tree.mjs +2 -15
  185. package/scripts/oracle/source-units.mjs +2 -16
  186. package/scripts/oracle/spike-run.mjs +2 -16
  187. package/scripts/oracle/validate-labels.mjs +2 -16
  188. package/scripts/org-repo-count.mjs +2 -1
  189. package/scripts/package-cards.mjs +2 -1
  190. package/scripts/prepublication-evidence.mjs +2 -1
  191. package/scripts/private-overlay.mjs +2 -1
  192. package/scripts/product-integrity-contract.mjs +2 -1
  193. package/scripts/protected-release-invocation.mjs +2 -1
  194. package/scripts/public-verification-abandon.mjs +2 -1
  195. package/scripts/public-verification-aggregate.mjs +2 -1
  196. package/scripts/public-verification-finalizer.mjs +2 -1
  197. package/scripts/public-verification-inputs.mjs +2 -1
  198. package/scripts/public-verification-lane.mjs +2 -1
  199. package/scripts/publication-receipt.mjs +2 -1
  200. package/scripts/published-surface-probe.mjs +2 -1
  201. package/scripts/qe/card-lane-gate.mjs +2 -16
  202. package/scripts/qe/session-start-gate.mjs +2 -16
  203. package/scripts/qe/ux-suite.mjs +2 -1
  204. package/scripts/qualified-candidate-check.mjs +2 -1
  205. package/scripts/rebuild-gists-from-receipts.mjs +2 -1
  206. package/scripts/recommendation-eval.mjs +2 -1
  207. package/scripts/recommendation-floor.mjs +2 -1
  208. package/scripts/recommendation-judge-score.mjs +2 -1
  209. package/scripts/recommendation-real-host-score.mjs +2 -1
  210. package/scripts/recommendation-real-host.mjs +2 -1
  211. package/scripts/refresh-capability-only-store.mjs +2 -1
  212. package/scripts/refresh-model-catalog.mjs +2 -1
  213. package/scripts/rehearse-corpus-pipeline.mjs +2 -1
  214. package/scripts/release-authority.mjs +2 -1
  215. package/scripts/release-evidence-aggregate.mjs +2 -1
  216. package/scripts/release-payload.mjs +2 -1
  217. package/scripts/release-projection.mjs +2 -1
  218. package/scripts/release-proof.mjs +2 -1
  219. package/scripts/release-qualification-contract.mjs +64 -28
  220. package/scripts/release-qualification.mjs +8 -1
  221. package/scripts/release-vector.mjs +2 -1
  222. package/scripts/restore-local-ingests.mjs +2 -1
  223. package/scripts/retrieval-canary.mjs +2 -1
  224. package/scripts/route-cheap.mjs +2 -1
  225. package/scripts/route-gold-rank.mjs +2 -1
  226. package/scripts/route-index-memory.mjs +2 -1
  227. package/scripts/route-latency-warm.mjs +2 -1
  228. package/scripts/router-utilization.mjs +2 -1
  229. package/scripts/routing-flywheel.mjs +2 -1
  230. package/scripts/run-operational-benchmark.mjs +2 -1
  231. package/scripts/run-operational-benchmark.v3.mjs +2 -1
  232. package/scripts/rvf-index-audit.mjs +2 -16
  233. package/scripts/seal-gist-receipt.mjs +2 -1
  234. package/scripts/security-guidance-codex-compat.mjs +2 -1
  235. package/scripts/selfcheck.mjs +2 -1
  236. package/scripts/signal-watch.mjs +2 -1
  237. package/scripts/source-coverage.mjs +2 -1
  238. package/scripts/stabilization-receipt.mjs +2 -1
  239. package/scripts/stamp-sweep.mjs +2 -1
  240. package/scripts/status-honesty.mjs +2 -1
  241. package/scripts/sync-commands.mjs +2 -2
  242. package/scripts/top100-benchmark.mjs +2 -1
  243. package/scripts/top100-corpus.mjs +2 -1
  244. package/scripts/user-model-hook-install.mjs +2 -1
  245. package/scripts/user-model-prompt-hook.mjs +2 -1
  246. package/scripts/verify-bundle.mjs +3 -1
  247. package/scripts/verify-model-catalog.mjs +2 -1
  248. package/scripts/version.mjs +2 -1
  249. package/scripts/wired-check.mjs +11 -5
  250. 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 (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
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 (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
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 (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main().catch(() => {
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 Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
270
+ return isDirectInvocation(import.meta.url);
270
271
  } catch {
271
272
  return false;
272
273
  }
@@ -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 (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
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 (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
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 (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
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 = (() => { try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })();
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 = learning?.enabled === false ? 0 : Number(learning?.lastTrainSeconds);
107
- if (learning?.statusKnown && ((Number.isFinite(age) && age > STALE_TRAIN_SECONDS) || (learning.observations > 0 && learning.lastTrainSeconds === null))) {
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 ${learning.lastTrainSeconds === null ? "not recorded" : (age / 86400).toFixed(1) + " days ago"} (${learning?.observations ?? learning?.trajectories ?? 0} observations recorded)${learning?.learningDb ? ` in ${learning.learningDb}` : ''}` }],
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 (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
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
- if (!process.argv[1]) return false;
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 Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
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 (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
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 (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
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 Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
427
+ return isDirectInvocation(import.meta.url);
427
428
  } catch {
428
429
  return false;
429
430
  }
@@ -50,6 +50,7 @@ import path from 'node:path';
50
50
  import readline from 'node:readline';
51
51
  import { fileURLToPath } from 'node:url';
52
52
  import { FULL_HINTS } from './full-hints.mjs';
53
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
53
54
 
54
55
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
55
56
  const KB = path.join(ROOT, 'kb');
@@ -245,7 +246,7 @@ export function discoverStores(dir) {
245
246
  }
246
247
 
247
248
  // ---------------- CLI ----------------
248
- if (import.meta.url === `file://${process.argv[1]}`) {
249
+ if (isDirectInvocation(import.meta.url)) {
249
250
  const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
250
251
  const has = (f) => process.argv.includes(f);
251
252
  const DIR = path.resolve(arg('--dir', KB));
@@ -26,6 +26,7 @@ import { captureGistSources } from './gist-receipts.mjs';
26
26
  import { projectSourceStore, RUNTIME_LEDGER_KIND } from './rvf-generation.mjs';
27
27
  import { compareKnowledgeInputs, fromCoverage as knowledgeFromCoverage, fromSeed as knowledgeFromSeed } from './knowledge-input-digest.mjs';
28
28
  import { defaultBaseDelayMs } from './transient-gh.mjs';
29
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
29
30
 
30
31
  export { rebuildCorpusAggregates };
31
32
 
@@ -1306,8 +1307,7 @@ export async function main(argv = process.argv.slice(2), {
1306
1307
  // with no archive and no receipt on disk. Same defect, same fix as plugin/scripts/hook-input.mjs:518.
1307
1308
  function isMain() {
1308
1309
  try {
1309
- if (!process.argv[1]) return false;
1310
- return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
1310
+ return isDirectInvocation(import.meta.url);
1311
1311
  } catch {
1312
1312
  return false;
1313
1313
  }
@@ -48,6 +48,7 @@ import { AGGREGATE_ASSET, SIGNING_PUBLIC_KEY_FILE } from './approved-runtime.mjs
48
48
  import { COVERAGE_ASSET, COVERAGE_RECEIPT_ASSET, verifyCoverageSidecar } from './corpus-coverage-sidecar.mjs';
49
49
  import { parseCorpusGeneration } from './corpus-promotion.mjs';
50
50
  import { retryTransientResult } from './transient-gh.mjs';
51
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
51
52
 
52
53
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
53
54
  export const RED = 'RED';
@@ -496,6 +497,6 @@ async function main(argv = process.argv.slice(2)) {
496
497
  return result.verdict === RED ? 1 : 0;
497
498
  }
498
499
 
499
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
500
+ if (isDirectInvocation(import.meta.url)) {
500
501
  main().then((code) => { process.exitCode = code; });
501
502
  }
@@ -23,6 +23,7 @@
23
23
  import path from 'node:path';
24
24
  import { fileURLToPath } from 'node:url';
25
25
  import { applyFix } from './claims-verify.mjs';
26
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
26
27
 
27
28
  const CHECK = process.argv.includes('--check');
28
29
 
@@ -49,6 +50,6 @@ async function main() {
49
50
  console.log(`\n[count-chunks] done — ${want} chunks is the live count; ${drifted.length} surface(s) rewritten.`);
50
51
  }
51
52
 
52
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
53
+ if (isDirectInvocation(import.meta.url)) {
53
54
  await main();
54
55
  }
@@ -34,6 +34,7 @@ import { customerEnv, customerManifestUrl, installCustomer, installedModules } f
34
34
  import { addPrivateStore, resolveRuntime } from './customer-seams.mjs';
35
35
  export { resolveRuntime };
36
36
  import { doctorSmokeArgs } from './installed-brain-health.mjs';
37
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
37
38
 
38
39
  export const REPO = 'stuinfla/ruvnet-brain';
39
40
  const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -358,6 +359,6 @@ async function main(argv = process.argv.slice(2)) {
358
359
  return 0;
359
360
  }
360
361
 
361
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
362
+ if (isDirectInvocation(import.meta.url)) {
362
363
  main().then((code) => { process.exitCode = code; }, (error) => { console.error(error.stack || error.message); process.exitCode = 1; });
363
364
  }
@@ -16,6 +16,7 @@ import { spawnSync } from 'node:child_process';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { digest } from './coverage-integrity.mjs';
18
18
  import { CONTENT_MAP_FILE, CONTENT_MAP_KIND, passageContentDigest } from './retrieval-passage-identity.mjs';
19
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
19
20
 
20
21
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
22
  const sha256File = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
@@ -68,4 +69,4 @@ function main(argv) {
68
69
  console.log(JSON.stringify({ out, resolved: Object.keys(map.entries).length, unresolved: map.unresolved.length }));
69
70
  }
70
71
 
71
- if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
72
+ if (isDirectInvocation(import.meta.url)) main(process.argv.slice(2));
@@ -5,6 +5,7 @@ import fs from 'node:fs';
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
8
9
 
9
10
  export function inspectPush(root, input) {
10
11
  const checked = [];
@@ -27,7 +28,7 @@ export function inspectPush(root, input) {
27
28
  return { ok: true, checked, scope: 'unpublished-commit-secret-scan' };
28
29
  }
29
30
 
30
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
31
+ if (isDirectInvocation(import.meta.url)) {
31
32
  try {
32
33
  const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
33
34
  console.log(JSON.stringify(inspectPush(root, fs.readFileSync(0, 'utf8'))));
@@ -26,6 +26,7 @@ import fs from 'node:fs';
26
26
  import path from 'node:path';
27
27
  import { pathToFileURL } from 'node:url';
28
28
  import { CLAUDE_MODEL_IDS, CLAUDE_TIERS, PRICING, estTokens, estimateCosts, receiptLine, receiptsPath, priceOf } from './route-cheap.mjs';
29
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
29
30
 
30
31
  // Default input share when only a MEASURED TOTAL is known. A subagent's tokens are dominated by input
31
32
  // (it re-reads files and tool output on every turn); its final report is small. 0.9 is an assumption,
@@ -109,4 +110,4 @@ function main() {
109
110
  console.log(receiptLine(receipt.model, { cost: receipt.est_cost, frontier: receipt.est_frontier_cost, saved: receipt.saved, ref: receipt.inherited }));
110
111
  }
111
112
 
112
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
113
+ if (isDirectInvocation(import.meta.url)) main();
@@ -3,7 +3,8 @@
3
3
  import fs from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { main } from '../plugin/scripts/doc-currency.mjs';
6
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
6
7
  export * from '../plugin/scripts/doc-currency.mjs';
7
8
  let direct = false;
8
- try { direct = Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch {}
9
+ try { direct = isDirectInvocation(import.meta.url); } catch {}
9
10
  if (direct) process.exit(main());
@@ -5,6 +5,7 @@
5
5
  // the caller supplies the finding and the current open issues, and receives a disposition before
6
6
  // invoking any GitHub write operation.
7
7
 
8
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
8
9
  const HEX_SHA = /^[0-9a-f]{7,64}$/i;
9
10
  const ISSUE_MARKER = 'dream-fingerprint:';
10
11
 
@@ -98,7 +99,7 @@ export function buildIssueBody(finding, decision = assessFinding(finding)) {
98
99
  // Adapter contract for the Dream Machine runner. Input is JSON on stdin:
99
100
  // { "finding": { ... }, "openIssues": [{ "number", "title", "body" }] }
100
101
  // Output is the disposition JSON. This command intentionally cannot create or mutate GitHub data.
101
- if (process.argv[1] === new URL(import.meta.url).pathname) {
102
+ if (isDirectInvocation(import.meta.url)) {
102
103
  let input = '';
103
104
  process.stdin.setEncoding('utf8');
104
105
  process.stdin.on('data', (chunk) => { input += chunk; });
@@ -6,6 +6,7 @@ import { spawn } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { probeSubscriptionHosts, subscriptionOnlyEnv } from './subscription-hosts.mjs';
8
8
  import { DUAL_HOST_MODEL_IDS } from './review-model-defaults.mjs';
9
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
9
10
 
10
11
  const HOSTS = Object.freeze(['claude-code', 'codex']);
11
12
  // Top subscription models verified on the native hosts on 2026-09-10.
@@ -322,21 +323,6 @@ export async function main(argv = process.argv.slice(2), {
322
323
  }
323
324
  }
324
325
 
325
- // Entry-point guard. Compares REALPATHS on both sides: path.resolve() normalizes a path but does
326
- // NOT follow symlinks, while import.meta.url IS symlink-resolved by Node. Through a symlink (npm bin
327
- // shims, wrapper scripts, and every os.tmpdir() path on macOS) the two sides disagree, so main()
328
- // never runs -- and because nothing throws, the process exits 0. A silent exit 0 is indistinguishable
329
- // from "ran, found nothing", which is how prepareCorpusCandidate once reported SUCCESS with no
330
- // archive on disk. Reproduced live 2026-07-27; pinned by tests/unit/entrypoint-symlink.test.mjs.
331
- function isDirectInvocation() {
332
- try {
333
- if (!process.argv[1]) return false;
334
- return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
335
- } catch {
336
- return false;
337
- }
338
- }
339
-
340
- if (isDirectInvocation()) {
326
+ if (isDirectInvocation(import.meta.url)) {
341
327
  process.exitCode = await main();
342
328
  }