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
@@ -220,14 +220,14 @@ function currency(artifact, repo, maxAgeDays) {
220
220
  const changed = diff.status === 0
221
221
  ? diff.stdout.split('\n').map((value) => value.trim()).filter(Boolean)
222
222
  : [];
223
- if (changed.length) return { ok: false, why: `load-bearing files changed: ${changed.join(', ')}` };
223
+ if (changed.length) return { ok: false, stale: true, why: `load-bearing files changed: ${changed.join(', ')}` };
224
224
  }
225
225
  const age = artifact.at ? (Date.now() - Date.parse(artifact.at)) / 86_400_000 : Infinity;
226
- if (!(age <= maxAgeDays)) return { ok: false, why: `artifact is older than ${maxAgeDays} days` };
226
+ if (!(age <= maxAgeDays)) return { ok: false, stale: true, why: `artifact is older than ${maxAgeDays} days` };
227
227
  return { ok: true };
228
228
  }
229
229
 
230
- export function checkArtifact({ file = RESULT_FILE, repo = ROOT, maxAgeDays = 14 } = {}) {
230
+ export function checkArtifact({ file = RESULT_FILE, repo = ROOT, maxAgeDays = 14, tolerateStale = false } = {}) {
231
231
  if (!fs.existsSync(file)) return { status: VERDICT.UNKNOWN, why: 'result artifact does not exist' };
232
232
  let artifact;
233
233
  try { artifact = JSON.parse(fs.readFileSync(file, 'utf8')); }
@@ -236,7 +236,8 @@ export function checkArtifact({ file = RESULT_FILE, repo = ROOT, maxAgeDays = 14
236
236
  const source = checkSourceIdentity({ repo });
237
237
  if (!source.clean) return { status: VERDICT.UNKNOWN, why: source.why, artifact };
238
238
  const current = currency(artifact, repo, maxAgeDays);
239
- if (!current.ok) return { status: VERDICT.UNKNOWN, why: current.why, artifact };
239
+ // `stale`: the one UNKNOWN a re-measurement fixes. Waiving it still verifies and recomputes everything below.
240
+ if (!current.ok && !(tolerateStale && current.stale)) return { status: VERDICT.UNKNOWN, why: current.why, ...(current.stale ? { stale: true } : {}), artifact };
240
241
  const evidence = verifyRuns(artifact, repo);
241
242
  if (!evidence.ok) return { status: VERDICT.UNKNOWN, why: evidence.why, artifact };
242
243
  const recomputed = aggregate(evidence.inputs);
@@ -244,7 +245,7 @@ export function checkArtifact({ file = RESULT_FILE, repo = ROOT, maxAgeDays = 14
244
245
  return { status: VERDICT.UNKNOWN, why: recomputed.why, artifact, recomputed };
245
246
  }
246
247
  const mismatch = aggregateMismatch(artifact, recomputed);
247
- if (mismatch) return { status: VERDICT.FAIL, why: mismatch, artifact, recomputed };
248
+ if (mismatch) return { status: VERDICT.FAIL, inconsistent: true, why: mismatch, artifact, recomputed }; // claims contradict own evidence
248
249
  return {
249
250
  status: recomputed.verdict,
250
251
  why: `${recomputed.verdict} — ${recomputed.passes}/${recomputed.n} recomputed runs on ${artifact.sha.slice(0, 8)}`,
@@ -279,18 +280,18 @@ function causalMutantCheck(artifact, recomputed, mutant) {
279
280
  export function checkMutantArtifacts({
280
281
  files = MUTANT_RESULT_FILES,
281
282
  repo = ROOT,
282
- maxAgeDays = 14,
283
+ maxAgeDays = 14, tolerateStale = false,
283
284
  } = {}) {
284
285
  const checked = [];
285
286
  for (const trap of [TRAP.MEMORY_SEARCH, TRAP.POST_TASK]) {
286
287
  for (const mutant of ['delete-lesson', 'brain-off-treated']) {
287
288
  const file = files[trap]?.[mutant];
288
289
  if (!file) return { status: VERDICT.UNKNOWN, why: `${trap}/${mutant}: artifact absent`, checked };
289
- const result = checkArtifact({ file, repo, maxAgeDays });
290
+ const result = checkArtifact({ file, repo, maxAgeDays, tolerateStale });
290
291
  if (result.status === VERDICT.UNKNOWN) return { ...result, why: `${trap}/${mutant}: ${result.why}`, checked };
291
- if (result.artifact?.mutant !== mutant || result.artifact?.trap !== trap) {
292
- return { status: VERDICT.FAIL, why: `${trap}/${mutant}: artifact identity mismatch`, checked };
293
- }
292
+ // Only a FAIL recomputed from self-consistent evidence is the expected one; claims that contradict the hashed transcripts are a forged receipt (Codex 4.7.21 r5 #1).
293
+ if (result.inconsistent) return { status: VERDICT.FAIL, why: `${trap}/${mutant}: mutant evidence is inconsistent: ${result.why}`, checked };
294
+ if (result.artifact?.mutant !== mutant || result.artifact?.trap !== trap) return { status: VERDICT.FAIL, why: `${trap}/${mutant}: artifact identity mismatch`, checked };
294
295
  const causal = causalMutantCheck(result.artifact, result.recomputed, mutant);
295
296
  if (causal) return { status: VERDICT.FAIL, why: `${trap}/${mutant}: ${causal}`, checked };
296
297
  checked.push(`${trap}/${mutant}`);
@@ -303,11 +304,11 @@ export function checkPortfolio({
303
304
  files = PORTFOLIO_RESULT_FILES,
304
305
  mutantFiles = MUTANT_RESULT_FILES,
305
306
  repo = ROOT,
306
- maxAgeDays = 14,
307
+ maxAgeDays = 14, tolerateStale = false,
307
308
  } = {}) {
308
309
  const artifacts = [];
309
310
  for (const trap of [TRAP.MEMORY_SEARCH, TRAP.POST_TASK]) {
310
- const result = checkArtifact({ file: files[trap], repo, maxAgeDays });
311
+ const result = checkArtifact({ file: files[trap], repo, maxAgeDays, tolerateStale });
311
312
  if (result.status !== VERDICT.PASS) return { ...result, why: `${trap}: ${result.why}`, artifacts };
312
313
  const metrics = result.recomputed;
313
314
  const artifact = result.artifact;
@@ -328,7 +329,7 @@ export function checkPortfolio({
328
329
  || artifacts[0].artifact.taskHash === artifacts[1].artifact.taskHash) {
329
330
  return { status: VERDICT.FAIL, why: 'portfolio traps are not independent', artifacts };
330
331
  }
331
- const mutants = checkMutantArtifacts({ files: mutantFiles, repo, maxAgeDays });
332
+ const mutants = checkMutantArtifacts({ files: mutantFiles, repo, maxAgeDays, tolerateStale });
332
333
  if (mutants.status !== VERDICT.PASS) return { ...mutants, why: `portfolio mutants: ${mutants.why}`, artifacts };
333
334
  return { status: VERDICT.PASS, why: 'portfolio recomputed both traps and all causal mutants', artifacts, mutants };
334
335
  }
@@ -3,13 +3,13 @@
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { main } from './learning-replay-cli.mjs';
6
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
6
7
 
7
8
  export * from './learning-replay-contract.mjs';
8
9
  export * from './learning-replay-execution.mjs';
9
10
  export * from './learning-replay-fixture.mjs';
10
11
  export * from './learning-replay-proof.mjs';
11
- export { main, measurePortfolio } from './learning-replay-cli.mjs';
12
+ export { main, measurePortfolio, checkWire } from './learning-replay-cli.mjs';
12
13
 
13
- const invokedDirectly = process.argv[1]
14
- && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
14
+ const invokedDirectly = isDirectInvocation(import.meta.url);
15
15
  if (invokedDirectly) process.exit(await main());
@@ -9,6 +9,7 @@ import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { learningScope, learnerCwd } from '../plugin/scripts/runtime-preferences.mjs';
12
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
12
13
 
13
14
  const HOME = os.homedir();
14
15
 
@@ -73,4 +74,4 @@ export function learnings({ statsPath, queueDir, now = Date.now(), cwd, env, hom
73
74
 
74
75
  export function printLearnings(l) { console.log(JSON.stringify(l, null, 2)); }
75
76
 
76
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) printLearnings(learnings());
77
+ if (isDirectInvocation(import.meta.url)) printLearnings(learnings());
@@ -24,23 +24,27 @@
24
24
  * Demotion is STICKY — it survives every future mining run. A one-click reject that the next
25
25
  * nightly quietly undoes is worse than no control at all, because the user stops trusting it and,
26
26
  * correctly, stops using it.
27
+ *
28
+ * MUTATIONS GO THROUGH `updateLessons()`, NOT `loadLessons()` + `saveLessons()`. The earlier version
29
+ * read `lessons` once at module load, then handed that same in-memory snapshot to `saveLessons()`
30
+ * after transforming it — the exact unlocked read-modify-write `lesson-store.mjs`'s own header
31
+ * documents destroying three ratified rules on 2026-07-22, and the exact race `updateLessons()` was
32
+ * written to close. `saveLessons()` taking a lock around the WRITE never protected the READ that
33
+ * preceded it: two ratify/demote invocations overlapping (a double-click, a script driving two calls
34
+ * back to back) could both load the same pre-change snapshot, and the second writer's save would
35
+ * silently discard the first writer's change — on the one file this CLI exists to make trustworthy.
36
+ * `updateLessons()` holds the lock across read → transform → write, so a concurrent writer's change
37
+ * is never lost.
27
38
  */
39
+ import fs from 'node:fs';
28
40
  import os from 'node:os';
29
- import { loadLessons, saveLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, SOURCE_CLASS, TRIGGERS } from './lesson-store.mjs';
30
-
31
- const argv = process.argv.slice(2);
32
- const arg = (f) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; };
33
- const has = (f) => argv.includes(f);
34
-
35
- const lessons = loadLessons();
36
- if (!lessons.length) {
37
- console.log('\n No personal lessons stored yet. They appear after an explicit user correction is captured.\n');
38
- process.exit(0);
39
- }
41
+ import { fileURLToPath } from 'node:url';
42
+ import { loadLessons, updateLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, SOURCE_CLASS, TRIGGERS } from './lesson-store.mjs';
43
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
40
44
 
41
45
  const FORCE = { block: '⛔ BLOCKS', checklist: '☑ checklist', inject: '· context', review: '👁 review only' };
42
46
 
43
- function list() {
47
+ export function list(lessons) {
44
48
  const pend = pending(lessons);
45
49
  console.log(`\n ${lessons.length} lessons — ${pend.length} awaiting your decision.\n`);
46
50
  console.log(' Nothing here refuses your work until YOU ratify it. The model does not get to');
@@ -67,35 +71,79 @@ function list() {
67
71
  console.log(' node scripts/lesson-ratify.mjs --ratify-all-user-stated\n');
68
72
  }
69
73
 
70
- function show(next, id, verb) {
71
- const l = next.find((x) => x.id === id);
72
- if (!l) { console.log(`\n No lesson with id "${id}". Run --list to see them.\n`); process.exit(1); }
73
- saveLessons(next);
74
- console.log(`\n ✓ ${verb} ${l.id}`);
75
- console.log(` now: ${FORCE[l.enforcement] || l.enforcement}${l.demoted ? ' (demoted — will never fire again, including after future mining runs)' : ''}`);
74
+ /**
75
+ * Apply `mutate(id, lessons)` to the lesson `id`, holding the store lock across the read and the
76
+ * write (via `updateLessons`) so a concurrent ratify/demote can never be read as stale and clobbered.
77
+ * Returns the lesson's new state, or null if no lesson with `id` exists (the store is left untouched).
78
+ */
79
+ export function applyMutation(id, mutate, file) {
80
+ let result = null;
81
+ updateLessons((fresh) => {
82
+ if (!fresh.find((x) => x.id === id)) return fresh;
83
+ const next = mutate(id, fresh);
84
+ result = next.find((x) => x.id === id);
85
+ return next;
86
+ }, file);
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * Bulk convenience, deliberately scoped: it can only touch lessons the USER stated. Model-inferred
92
+ * lessons are never swept up by a bulk action — that would be exactly the hole the boundary closes.
93
+ * Also lock-protected end to end, for the same reason as `applyMutation`.
94
+ */
95
+ export function applyRatifyAllUserStated(file) {
96
+ let ratifiedIds = [];
97
+ let nowBlocking = 0;
98
+ updateLessons((fresh) => {
99
+ const targets = fresh.filter((l) => l.origin === ORIGIN.USER_STATED
100
+ && l.sourceClass === SOURCE_CLASS.CURRENT_USER
101
+ && l.status === STATUS.CANDIDATE
102
+ && !l.demoted);
103
+ let next = fresh;
104
+ for (const l of targets) next = ratify(l.id, next);
105
+ ratifiedIds = targets.map((l) => l.id);
106
+ nowBlocking = next.filter((l) => l.enforcement === ENFORCEMENT.BLOCK).length;
107
+ return next;
108
+ }, file);
109
+ return { ratifiedCount: ratifiedIds.length, nowBlocking };
110
+ }
111
+
112
+ function printResult(result, id, verb) {
113
+ if (!result) { console.log(`\n No lesson with id "${id}". Run --list to see them.\n`); process.exit(1); }
114
+ console.log(`\n ✓ ${verb} ${result.id}`);
115
+ console.log(` now: ${FORCE[result.enforcement] || result.enforcement}${result.demoted ? ' (demoted — will never fire again, including after future mining runs)' : ''}`);
76
116
  console.log(` stored at ${(process.env.RUVNET_LESSON_STORE || '~/.config/ruvnet-brain/lessons.json').replace(os.homedir(), '~')}\n`);
77
117
  }
78
118
 
79
- if (has('--ratify')) {
80
- const id = arg('--ratify');
81
- show(ratify(id, lessons), id, 'ratified');
82
- } else if (has('--demote')) {
83
- const id = arg('--demote');
84
- show(demote(id, lessons), id, 'demoted');
85
- } else if (has('--ratify-all-user-stated')) {
86
- // Bulk convenience, deliberately scoped: it can only touch lessons the USER stated. Model-inferred
87
- // lessons are never swept up by a bulk action — that would be exactly the hole the boundary closes.
88
- let next = lessons;
89
- const targets = lessons.filter((l) => l.origin === ORIGIN.USER_STATED
90
- && l.sourceClass === SOURCE_CLASS.CURRENT_USER
91
- && l.status === STATUS.CANDIDATE
92
- && !l.demoted);
93
- for (const l of targets) next = ratify(l.id, next);
94
- saveLessons(next);
95
- const nowBlocking = next.filter((l) => l.enforcement === ENFORCEMENT.BLOCK).length;
96
- console.log(`\n ✓ ratified ${targets.length} lesson(s) you stated yourself.`);
97
- console.log(` ${nowBlocking} now BLOCK at their decision point. Model-inferred lessons were left`);
98
- console.log(` as candidates — a bulk action may never promote something the model inferred.\n`);
99
- } else {
100
- list();
119
+ /** am I the entrypoint? — `fileURLToPath`, never `.pathname` (Windows); `realpathSync` guarded (vitest). */
120
+ function isEntrypoint() {
121
+ return isDirectInvocation(import.meta.url);
122
+ }
123
+
124
+ if (isEntrypoint()) {
125
+ const argv = process.argv.slice(2);
126
+ const arg = (f) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; };
127
+ const has = (f) => argv.includes(f);
128
+
129
+ const lessons = loadLessons();
130
+ if (!lessons.length) {
131
+ console.log('\n No personal lessons stored yet. They appear after an explicit user correction is captured.\n');
132
+ process.exit(0);
133
+ }
134
+
135
+ if (has('--ratify')) {
136
+ const id = arg('--ratify');
137
+ printResult(applyMutation(id, ratify), id, 'ratified');
138
+ } else if (has('--demote')) {
139
+ const id = arg('--demote');
140
+ printResult(applyMutation(id, demote), id, 'demoted');
141
+ } else if (has('--ratify-all-user-stated')) {
142
+ const { ratifiedCount, nowBlocking } = applyRatifyAllUserStated();
143
+ console.log(`\n ✓ ratified ${ratifiedCount} lesson(s) you stated yourself.`);
144
+ console.log(` ${nowBlocking} now BLOCK at their decision point. Model-inferred lessons were left`);
145
+ console.log(` as candidates — a bulk action may never promote something the model inferred.\n`);
146
+ } else {
147
+ list(lessons);
148
+ }
101
149
  }
@@ -26,6 +26,7 @@ import fs from 'node:fs';
26
26
  import path from 'node:path';
27
27
  import { spawnSync } from 'node:child_process';
28
28
  import { fileURLToPath } from 'node:url';
29
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
29
30
 
30
31
  const argv = process.argv.slice(2);
31
32
  const cmd = argv[0];
@@ -91,7 +92,7 @@ export function checkCheckpoint(file = FILE, runner = (c) => spawnSync('sh', ['-
91
92
  return { code: 0, verdict: `continue — iteration ${cp.iteration}, next: ${cp.next}` };
92
93
  }
93
94
 
94
- const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
95
+ const invokedDirectly = isDirectInvocation(import.meta.url);
95
96
  if (invokedDirectly) {
96
97
  if (cmd === 'read') {
97
98
  console.log(JSON.stringify(readCheckpoint() ?? {}, null, 2));
@@ -12,6 +12,7 @@ import path from 'node:path';
12
12
  import os from 'node:os';
13
13
  import { pathToFileURL } from 'node:url';
14
14
  import { CLAUDE_MODEL_IDS } from './route-cheap.mjs';
15
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
15
16
 
16
17
  export function receiptsPath() {
17
18
  return (
@@ -165,4 +166,4 @@ function main() {
165
166
  if (skipped) console.log(`(${skipped} corrupt line(s) skipped)`);
166
167
  }
167
168
 
168
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
169
+ if (isDirectInvocation(import.meta.url)) main();
@@ -8,6 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
11
12
 
12
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
14
  const ROOT = path.resolve(__dirname, '..');
@@ -70,7 +71,7 @@ export function providerChoices(catalog) {
70
71
  return Object.entries(catalog.providers || {}).map(([id, p]) => ({ id, label: p.label || id }));
71
72
  }
72
73
 
73
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
74
+ if (isDirectInvocation(import.meta.url)) {
74
75
  const c = loadCatalog();
75
76
  const det = detectProvider(c, {});
76
77
  console.log(`detected house: ${det.provider} (${det.source})`);
@@ -96,7 +96,9 @@ export function parseCodingAgentEvidence(html, methodology, { url, checkedAt, id
96
96
  const walk = (value) => {
97
97
  if (Array.isArray(value)) { for (const child of value) walk(child); return; }
98
98
  if (!value || typeof value !== 'object') return;
99
- if (typeof value.hostModelSlug === 'string' && typeof value.id === 'string' && value.indexScore !== undefined) { rows.set(value.id, value); return; }
99
+ // A row is an agent/configuration record: harness name + index score. AA dropped `hostModelSlug` and
100
+ // `modelRelease` from the page in October 2026, so neither may be a key; both stay optional evidence.
101
+ if (typeof value.agentName === 'string' && typeof value.id === 'string' && value.indexScore !== undefined) { rows.set(value.id, value); return; }
100
102
  for (const child of Object.values(value)) walk(child);
101
103
  };
102
104
  for (const match of html.matchAll(/self\.__next_f\.push\((.*?)\)<\/script>/gs)) {
@@ -104,12 +106,12 @@ export function parseCodingAgentEvidence(html, methodology, { url, checkedAt, id
104
106
  const envelope = JSON.parse(match[1]);
105
107
  if (typeof envelope[1] !== 'string') continue;
106
108
  for (const line of envelope[1].split('\n')) {
107
- if (!line.includes('hostModelSlug')) continue;
109
+ if (!line.includes('agentName')) continue;
108
110
  try { walk(JSON.parse(line.slice(line.indexOf(':') + 1))); } catch { /* unrelated frame */ }
109
111
  }
110
112
  } catch { /* required contract checked below */ }
111
113
  }
112
- const source = { url, checkedAt, sha256: digest(html), parser: 'aa-coding-agents-flight-v1' };
114
+ const source = { url, checkedAt, sha256: digest(html), parser: 'aa-coding-agents-flight-v2' };
113
115
  const records = [];
114
116
  const skippedIncomplete = [];
115
117
  for (const row of rows.values()) {
@@ -129,7 +131,7 @@ export function parseCodingAgentEvidence(html, methodology, { url, checkedAt, id
129
131
  if (binding && (!binding.model || !binding.evidence || !binding.checkedAt)) throw new Error('Invalid coding-agent native identity binding');
130
132
  const effort = nativeHost ? row.display.model.match(/\((low|medium|high|xhigh|max|minimal|none)\)/i)?.[1]?.toLowerCase() ?? null : null;
131
133
  records.push({ sourceId: row.id, harness: row.agentName, provider: row.provider, nativeHost,
132
- sourceModelSlug: row.hostModelSlug, sourceReleaseSlug: row.modelRelease?.slug ?? null,
134
+ sourceModelSlug: typeof row.hostModelSlug === 'string' ? row.hostModelSlug : null, sourceReleaseSlug: row.modelRelease?.slug ?? null,
133
135
  displayModel: row.display.model, configurationLabel: row.displayLabel, effort,
134
136
  model: binding?.model ?? null, identityEvidence: binding ?? null,
135
137
  fallback: /fallback/i.test(row.displayLabel ?? row.display.model),
@@ -8,6 +8,7 @@ import { randomUUID } from 'node:crypto';
8
8
  import { buildWeeklyAssessment } from './model-weekly-assessment.mjs';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { WEEK_MS, digest, parseInventory, parseArtificialAnalysis, parseCodingAgentEvidence, currencyStatus } from './model-currency-evidence.mjs';
11
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
11
12
 
12
13
  export const DEFAULT_ROUTER_DIR = path.join(os.homedir(), '.claude', 'model-router');
13
14
  export const AA_URLS = [
@@ -218,13 +219,42 @@ export async function refreshModelCurrency({ routerDir = DEFAULT_ROUTER_DIR, now
218
219
  } finally { release(routerDir, lock); }
219
220
  }
220
221
 
221
- if (process.argv[1] && path.resolve(process.argv[1]) === SELF) {
222
+ /** The weekly CI verdict. Core evidence is the inventory, the effort evaluations and the local instruction; a
223
+ * third-party scrape that only feeds the advisory sections (coding agents, official docs, extra benchmark
224
+ * archives) failing on its own is `partial`: exit 0 with a warning, because the router's own facts are fresh. */
225
+ const ANCILLARY_ERROR = /^(coding agents|official |coding benchmark )/;
226
+ export function refreshOutcome(result) {
227
+ const errors = result?.lastAttempt?.errors ?? result?.errors ?? [];
228
+ const core = errors.filter((error) => !ANCILLARY_ERROR.test(error));
229
+ if (result?.action !== 'refreshed' || result.inventoryFresh !== true || result.evaluationsFresh !== true || core.length) {
230
+ return { status: 'failed', exitCode: 1, errors: core.length ? core : [...errors, result?.reason ?? 'core evidence not fresh'] };
231
+ }
232
+ return errors.length ? { status: 'partial', exitCode: 0, warnings: errors } : { status: 'complete', exitCode: 0, warnings: [] };
233
+ }
234
+ export function outcomeSummary(outcome) {
235
+ const lines = [`### model-currency refresh: ${outcome.status}`, ''];
236
+ for (const message of outcome.errors ?? []) lines.push(`- error: ${message}`);
237
+ for (const message of outcome.warnings ?? []) lines.push(`- warning: ${message}`);
238
+ if (outcome.status === 'partial') lines.push('', 'Inventory and evaluations are fresh; only a third-party advisory source failed.');
239
+ return `${lines.join('\n')}\n`;
240
+ }
241
+
242
+ if (isDirectInvocation(import.meta.url)) {
222
243
  const dirIndex = process.argv.indexOf('--router-dir');
223
244
  const routerDir = dirIndex >= 0 ? process.argv[dirIndex + 1] : DEFAULT_ROUTER_DIR;
224
245
  if (!routerDir || !path.isAbsolute(routerDir)) throw new Error('--router-dir must be absolute');
225
- if (process.argv.includes('--refresh')) {
246
+ if (process.argv.includes('--summarize')) {
247
+ // Markdown for the CI job summary from a captured `--refresh` stdout; a missing or unreadable capture says so.
248
+ let outcome;
249
+ try { outcome = JSON.parse(fs.readFileSync(process.argv[process.argv.indexOf('--summarize') + 1], 'utf8').split('\n')[0]).outcome; }
250
+ catch { outcome = null; }
251
+ process.stdout.write(outcome ? outcomeSummary(outcome) : '### model-currency refresh: no result (the refresh step crashed before reporting)\n');
252
+ } else if (process.argv.includes('--refresh')) {
226
253
  refreshModelCurrency({ routerDir, claimToken: process.argv.includes('--claim-token') ? process.argv[process.argv.indexOf('--claim-token') + 1] : null }).then((result) => {
227
- console.log(JSON.stringify(result)); if (result.status === 'stale') process.exitCode = 1;
254
+ const outcome = refreshOutcome(result);
255
+ console.log(JSON.stringify({ ...result, outcome }));
256
+ if (outcome.status === 'partial') console.log(`::warning title=model-currency partial::${outcome.warnings.join('; ').replace(/[\r\n]+/g, ' ')}`);
257
+ process.exitCode = outcome.exitCode;
228
258
  }).catch((error) => { console.error(error.message); process.exitCode = 1; });
229
259
  } else console.log(JSON.stringify(process.argv.includes('--catch-up') ? maybeLaunchCurrencyRefresh({ routerDir }) : readCurrencyStatus({ routerDir })));
230
260
  }
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import os from 'node:os';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
8
9
  const HOSTS = { codex: 'openai', 'claude-code': 'anthropic' };
9
10
  const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
10
11
  const read = (file, optional = false) => {
@@ -104,7 +105,7 @@ export async function refreshNativeCatalog({ routerDir = path.join(os.homedir(),
104
105
  if (acquired) { try { fs.rmdirSync(guard); } catch { /* not acquired */ } }
105
106
  }
106
107
  }
107
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
108
+ if (isDirectInvocation(import.meta.url)) {
108
109
  const option = name => { const at = process.argv.indexOf(name); return at < 0 ? undefined : process.argv[at + 1]; };
109
110
  refreshNativeCatalog({ routerDir: option('--router-dir'), deadline: option('--deadline') === undefined ? undefined : Number(option('--deadline')) })
110
111
  .then(result => { console.log(JSON.stringify(result)); if (result.status !== 'current') process.exitCode = 1; });
@@ -10,6 +10,7 @@ import { execFileSync } from 'node:child_process';
10
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
11
 
12
12
  import { readCodexAllowance } from './native-subscription-usage.mjs';
13
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
13
14
 
14
15
  const DIR = path.dirname(fileURLToPath(import.meta.url));
15
16
  const TASK_CLASSES = new Set(['fast', 'medium', 'substantial', 'hard', 'exceptional']);
@@ -126,7 +127,7 @@ async function main(argv) {
126
127
  // The host's normal transcript records its tool input. This hook persists no task text.
127
128
  process.stdout.write(JSON.stringify(output) + '\n');
128
129
  }
129
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
130
+ if (isDirectInvocation(import.meta.url)) {
130
131
  main(process.argv.slice(2)).catch(() => {
131
132
  const args = process.argv.slice(2); const harness = args[args.indexOf('--harness') + 1];
132
133
  const result = harness === 'codex' && loadScope(harness).strict ? deny('Agent routing hook failed; managed dispatch required.')
@@ -9,6 +9,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { assertCurrentSelection, selectionEvidenceStatus, loadSelection, loadCatalog, loadProfile, applyProfile, eligibleCandidates, TASK_CLASSES } from './model-router-engine.mjs';
10
10
 
11
11
  import { readCodexAllowance } from './native-subscription-usage.mjs';
12
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
12
13
 
13
14
  const DIR = path.dirname(fileURLToPath(import.meta.url));
14
15
  const BILLING_ENV = /^(OPENAI_API_KEY|OPENAI_BASE_URL|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|ANTHROPIC_CUSTOM_HEADERS|CLAUDE_CODE_USE_BEDROCK|CLAUDE_CODE_USE_VERTEX|CLAUDE_CODE_USE_FOUNDRY|CODEX_API_KEY)$/;
@@ -156,6 +157,6 @@ async function main(argv) {
156
157
  taskClass: decision.taskClass, executed: false }) + '\n');
157
158
  } else process.exitCode = await dispatch(decision, prompt, { interactive });
158
159
  }
159
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
160
+ if (isDirectInvocation(import.meta.url)) {
160
161
  main(process.argv.slice(2)).catch((e) => { process.stderr.write(`model-router-dispatch: ${e.message}\n`); process.exitCode = 1; });
161
162
  }
@@ -12,6 +12,7 @@ import path from 'node:path';
12
12
  import os from 'node:os';
13
13
  import crypto from 'node:crypto';
14
14
  import { pathToFileURL, fileURLToPath } from 'node:url';
15
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
15
16
  import { estTokens } from './route-cheap.mjs'; // reuse the verified char/4 estimator (DRY)
16
17
 
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -309,6 +310,6 @@ async function main() {
309
310
  }
310
311
  }
311
312
 
312
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
313
+ if (isDirectInvocation(import.meta.url)) {
313
314
  main().catch((e) => { process.stderr.write(`model-router-engine: ${e.stack || e.message}\n`); process.exit(1); });
314
315
  }
@@ -25,6 +25,7 @@ import fs from 'node:fs';
25
25
  import path from 'node:path';
26
26
  import os from 'node:os';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
28
29
 
29
30
  const OUTCOMES_LOG =
30
31
  process.env.MODEL_ROUTER_OUTCOMES ||
@@ -105,6 +106,6 @@ function main() {
105
106
  console.log(`[model-router-outcome] logged: ${JSON.stringify(entry)}`);
106
107
  }
107
108
 
108
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
109
+ if (isDirectInvocation(import.meta.url)) {
109
110
  try { main(); } catch (e) { process.stderr.write(`model-router-outcome: ${e.message}\n`); process.exit(1); }
110
111
  }
@@ -27,6 +27,7 @@ import path from 'node:path';
27
27
  import os from 'node:os';
28
28
  import { execFileSync } from 'node:child_process';
29
29
  import readline from 'node:readline/promises';
30
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
30
31
 
31
32
  const CONFIG_DIR = path.join(os.homedir(), '.claude', 'model-router');
32
33
  export const PROFILE_PATH = process.env.MODEL_ROUTER_PROFILE || path.join(CONFIG_DIR, 'profile.json');
@@ -136,6 +137,6 @@ async function main() {
136
137
  }
137
138
 
138
139
  const { pathToFileURL } = await import('node:url');
139
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
140
+ if (isDirectInvocation(import.meta.url)) {
140
141
  main().catch((e) => { console.error(`model-router-setup: ${e.message}`); process.exit(1); });
141
142
  }
@@ -8,6 +8,7 @@ import { performance } from 'node:perf_hooks';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { classify } from '../config/model-router/policy.default.mjs';
10
10
  import { extractFeatures } from './model-router-engine.mjs';
11
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
11
12
 
12
13
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
14
  const DEFAULT_CASES = path.join(ROOT, 'config/model-router/routing-eval-cases.json');
@@ -96,7 +97,7 @@ export function runEvaluation({ caseFile = DEFAULT_CASES, generatedAt } = {}) {
96
97
  return report;
97
98
  }
98
99
 
99
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
100
+ if (isDirectInvocation(import.meta.url)) {
100
101
  const args = process.argv.slice(2);
101
102
  if (args.length && (args.length !== 2 || args[0] !== '--out')) throw new Error('Usage: node scripts/model-routing-eval.mjs [--out <report.json>]');
102
103
  const report = runEvaluation();
@@ -11,6 +11,7 @@ import { StringDecoder } from 'node:string_decoder';
11
11
  import { spawn, execFileSync } from 'node:child_process';
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
13
  import { subscriptionEnvironment, assertSubscriptionAuth, validateDispatchDecision, loadNativeCodexModels } from './model-router-dispatch.mjs';
14
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
14
15
 
15
16
  const HERE = path.dirname(fileURLToPath(import.meta.url));
16
17
  const REFUSED = 'Current reviewed native model/effort allocation unavailable; new turn was not forwarded.';
@@ -448,6 +449,6 @@ async function main(argv) {
448
449
  child.once('exit', (code, signal) => { process.stdin.pause(); process.stdin.unref?.(); process.exitCode = code ?? (signal ? 1 : 0); });
449
450
  child.once('error', () => { process.stderr.write('[native-model-routing] Native host transport unavailable.\n'); process.exitCode = 1; });
450
451
  }
451
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
452
+ if (isDirectInvocation(import.meta.url)) {
452
453
  main(process.argv.slice(2)).catch(() => { process.stderr.write('[native-model-routing] Explicit native executable or subscription authentication unavailable.\n'); process.exitCode = 1; });
453
454
  }
@@ -218,7 +218,10 @@ export function updateLauncherSettings(file, requested, { apply = false } = {})
218
218
  return receipt;
219
219
  }
220
220
 
221
- if (process.argv[1] && path.resolve(process.argv[1]) === SELF) {
221
+ // installNativeLaunchers() copies THIS file alone to ~/.cache/ruvnet-brain/model-routing/ as the
222
+ // launcher runner, so it cannot import plugin/scripts/entrypoint.mjs; this is that helper's
223
+ // comparison, inline: real paths on both sides.
224
+ if ((() => { try { return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(SELF); } catch { return false; } })()) {
222
225
  const argv = process.argv.slice(2);
223
226
  if (argv[0] === '--launch') {
224
227
  try {
@@ -11,6 +11,7 @@ import { spawn } from 'node:child_process';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import { connectNativeGateway, decideNativeTurn, nativeGatewayLaunch } from './model-routing-gateway.mjs';
13
13
  import { subscriptionEnvironment, assertSubscriptionAuth } from './model-router-dispatch.mjs';
14
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
14
15
 
15
16
  export const MAX_TERMINAL_BYTES = 64 * 1024 * 1024;
16
17
  const REFUSED = 'Native terminal routing unavailable; request was not sent.';
@@ -302,7 +303,7 @@ function exitOf(child) {
302
303
  });
303
304
  }
304
305
 
305
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
306
+ if (isDirectInvocation(import.meta.url)) {
306
307
  runTerminalGateway(parseTerminalInvocation(process.argv.slice(2))).then(({ code, signal }) => {
307
308
  if (signal) { process.removeAllListeners(signal); process.kill(process.pid, signal); }
308
309
  else process.exitCode = code ?? 1;
@@ -9,6 +9,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { classifyTerminalArguments, validateUpstreamSocket, terminalTempRoot } from './model-terminal-gateway.mjs';
10
10
  import { nativeGatewayLaunch } from './model-routing-gateway.mjs';
11
11
  import { subscriptionEnvironment, assertSubscriptionAuth } from './model-router-dispatch.mjs';
12
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
12
13
 
13
14
  const SELF = fileURLToPath(import.meta.url);
14
15
  const MARKER = '# Managed RuvNet Brain terminal launcher';
@@ -343,7 +344,7 @@ export async function runTerminalLauncher({ host, args = [], config, env = proce
343
344
  const unforward = forwardSignals(child, signalSource);
344
345
  try { return await exitOf(child); } finally { unforward(); }
345
346
  }
346
- if (process.argv[1] && path.resolve(process.argv[1]) === SELF) {
347
+ if (isDirectInvocation(import.meta.url)) {
347
348
  const argv = process.argv.slice(2);
348
349
  Promise.resolve().then(() => {
349
350
  if (argv[0] !== '--launch' || argv[2] !== '--config' || argv[4] !== '--') throw new Error('Explicit host/config terminal launcher required');
@@ -11,6 +11,7 @@ import { subscriptionOnlyEnv } from './subscription-hosts.mjs';
11
11
  import { applyProfile, loadCatalog, selectionEvidenceStatus } from './model-router-engine.mjs';
12
12
  import { createAnalystHome, trustAnalystDenial } from './model-analyst-sandbox.mjs';
13
13
  import { digest, currencyStatus, WEEK_MS } from './model-currency-evidence.mjs';
14
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
14
15
 
15
16
  const text = { type: 'string', maxLength: 320 };
16
17
  const sourceId = { type: 'string', maxLength: 64 };
@@ -328,7 +329,7 @@ export function maybeLaunchWeeklyAnalyst({ routerDir = path.join(os.homedir(), '
328
329
  } catch (error) { release(routerDir, token); throw error; }
329
330
  } catch (error) { return { status: 'blocked', launched: false, reason: error.message.slice(0, 240) }; }
330
331
  }
331
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
332
+ if (isDirectInvocation(import.meta.url)) {
332
333
  const index = process.argv.indexOf('--router-dir'); const routerDir = index >= 0 ? process.argv[index + 1] : undefined;
333
334
  const claimIndex = process.argv.indexOf('--claim-token');
334
335
  if (!process.argv.includes('--run')) { console.log(JSON.stringify(maybeLaunchWeeklyAnalyst({ routerDir }))); } else runWeeklyAnalyst({ routerDir, claimToken: claimIndex >= 0 ? process.argv[claimIndex + 1] : null, ...(process.argv.includes('--timeout-ms') ? { timeoutMs: Number(process.argv[process.argv.indexOf('--timeout-ms') + 1]) } : {}) }).then((result) => { console.log(JSON.stringify(result)); if (result.status === 'failed') process.exitCode = 1; }).catch((error) => { console.error(error.message); process.exitCode = 1; });