ruvnet-brain 4.7.20 → 4.7.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +618 -104
  3. package/console/activity.js +122 -38
  4. package/kb/download-retry.mjs +93 -21
  5. package/kb/forge-guard-injection.mjs +277 -0
  6. package/kb/forge-update.mjs +166 -38
  7. package/kb/lifecycle-evidence-retention.mjs +151 -14
  8. package/kb/model-requirements.mjs +4 -1
  9. package/kb/refresh-run.mjs +175 -27
  10. package/kb/verify-citation.mjs +15 -2
  11. package/package.json +3 -1
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/.codex-plugin/plugin.json +1 -1
  14. package/plugin/mcp/managed-cli-interface.mjs +16 -8
  15. package/plugin/scripts/adr-currency-gate.mjs +2 -1
  16. package/plugin/scripts/advisory-contract.mjs +21 -0
  17. package/plugin/scripts/advocacy-route.mjs +5 -4
  18. package/plugin/scripts/agentdb-recall-process.mjs +115 -5
  19. package/plugin/scripts/agentdb-recall-reader.mjs +44 -2
  20. package/plugin/scripts/agentdb-recall.mjs +49 -17
  21. package/plugin/scripts/automatic-update.mjs +9 -1
  22. package/plugin/scripts/brain-confirmation.mjs +2 -1
  23. package/plugin/scripts/brain-footprint.mjs +72 -11
  24. package/plugin/scripts/capability-registry.mjs +1 -1
  25. package/plugin/scripts/capacity-aware-parallel-work.mjs +2 -1
  26. package/plugin/scripts/codex-overnight-notice.mjs +33 -0
  27. package/plugin/scripts/completion-claim-evidence.mjs +241 -23
  28. package/plugin/scripts/continuation-gate.mjs +75 -11
  29. package/plugin/scripts/decision-gate.mjs +13 -63
  30. package/plugin/scripts/decision-outcomes.mjs +36 -0
  31. package/plugin/scripts/decision-recall-gate.mjs +40 -5
  32. package/plugin/scripts/decision-record.mjs +8 -3
  33. package/plugin/scripts/degradation-watch.mjs +2 -1
  34. package/plugin/scripts/developer-update-lock.mjs +172 -33
  35. package/plugin/scripts/developer-update-owner.mjs +8 -5
  36. package/plugin/scripts/developer-update-policy.mjs +4 -1
  37. package/plugin/scripts/developer-update.mjs +155 -53
  38. package/plugin/scripts/doc-currency.mjs +2 -16
  39. package/plugin/scripts/duplicate-gate.mjs +2 -1
  40. package/plugin/scripts/entrypoint.mjs +34 -0
  41. package/plugin/scripts/footprint-io.mjs +135 -8
  42. package/plugin/scripts/grok-hook-adapter.mjs +2 -1
  43. package/plugin/scripts/ground-ruvnet.sh +6 -3
  44. package/plugin/scripts/grounding-answer.mjs +2 -1
  45. package/plugin/scripts/grounding-code-projection.mjs +2 -1
  46. package/plugin/scripts/grounding-substance.mjs +2 -1
  47. package/plugin/scripts/grounding-turn-gate.mjs +2 -2
  48. package/plugin/scripts/grounding-turn-mark.mjs +2 -2
  49. package/plugin/scripts/hook-context-budget.mjs +2 -1
  50. package/plugin/scripts/hook-input.mjs +2 -2
  51. package/plugin/scripts/hook-shim.mjs +9 -2
  52. package/plugin/scripts/host-update.mjs +29 -5
  53. package/plugin/scripts/identifier-preflight.mjs +2 -1
  54. package/plugin/scripts/kb-copy-proof.mjs +57 -9
  55. package/plugin/scripts/learn-capture.mjs +2 -1
  56. package/plugin/scripts/learning-store.mjs +62 -16
  57. package/plugin/scripts/lesson-bridge.mjs +2 -2
  58. package/plugin/scripts/lesson-gate.mjs +7 -39
  59. package/plugin/scripts/lesson-promote.mjs +83 -22
  60. package/plugin/scripts/lesson-scope.mjs +51 -0
  61. package/plugin/scripts/lesson-store.mjs +1 -1
  62. package/plugin/scripts/md-stamp.mjs +2 -1
  63. package/plugin/scripts/native-turn-transcript.mjs +98 -6
  64. package/plugin/scripts/nightly-scheduler.mjs +10 -5
  65. package/plugin/scripts/package-recommender.mjs +2 -2
  66. package/plugin/scripts/project-store-resolver.mjs +19 -0
  67. package/plugin/scripts/routing-outcome-capture.mjs +2 -1
  68. package/plugin/scripts/ruflo-bin.mjs +14 -6
  69. package/plugin/scripts/ruflo-memory-hooks.mjs +157 -19
  70. package/plugin/scripts/session-snapshot-hook.mjs +25 -5
  71. package/plugin/scripts/session-start-core.mjs +7 -16
  72. package/plugin/scripts/session-start-health.mjs +98 -10
  73. package/plugin/scripts/session-start-issue-alert.mjs +63 -0
  74. package/plugin/scripts/session-start-update-plane.mjs +15 -1
  75. package/plugin/scripts/signal-watch.mjs +2 -1
  76. package/plugin/scripts/spend-guard.mjs +2 -1
  77. package/plugin/scripts/substitution-wall.mjs +2 -1
  78. package/plugin/scripts/turn-outcome-capture.mjs +7 -3
  79. package/plugin/scripts/unprompted-runtime.mjs +20 -3
  80. package/plugin/scripts/update-apply.mjs +186 -15
  81. package/plugin/skills/release-proof/scripts/release-proof.mjs +2 -1
  82. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +2 -2
  83. package/plugin/skills/ruvnet-brain/SKILL.md +3 -2
  84. package/scripts/adr-072-completion.mjs +2 -1
  85. package/scripts/agentdb-context.mjs +2 -1
  86. package/scripts/agentdb-fleet-doctor.mjs +30 -8
  87. package/scripts/approved-runtime.mjs +2 -1
  88. package/scripts/architecture-review-lock.mjs +2 -1
  89. package/scripts/ascii-drift.mjs +2 -1
  90. package/scripts/brain-latency-50.mjs +2 -1
  91. package/scripts/brain-novice-50.mjs +2 -16
  92. package/scripts/brain-score.mjs +13 -2
  93. package/scripts/build-bundle.mjs +2 -1
  94. package/scripts/build-concepts.mjs +2 -1
  95. package/scripts/candidate-ci-receipt.mjs +2 -1
  96. package/scripts/candidate-host-evidence.mjs +2 -1
  97. package/scripts/card-from-source.mjs +2 -1
  98. package/scripts/ci/warm-brain-models.mjs +2 -1
  99. package/scripts/claims-verify.mjs +7 -3
  100. package/scripts/claude-controlled-terminal.mjs +2 -1
  101. package/scripts/claude-terminal-mod.mjs +2 -1
  102. package/scripts/code-release-corpus.mjs +2 -1
  103. package/scripts/codex-boot-surface-proof.mjs +208 -0
  104. package/scripts/codex-hook-trust-reconcile.mjs +67 -1
  105. package/scripts/codex-hook-trust.mjs +40 -1
  106. package/scripts/codex-hook-wrapper-history.mjs +2 -1
  107. package/scripts/codex-host-execution-proof.mjs +2 -1
  108. package/scripts/completion-claim-replay.mjs +2 -1
  109. package/scripts/console-engine.mjs +8 -3
  110. package/scripts/corpus-canary.mjs +2 -1
  111. package/scripts/corpus-candidate.mjs +2 -2
  112. package/scripts/corpus-coverage-sidecar.mjs +2 -1
  113. package/scripts/corpus-dispatch-decision.mjs +2 -1
  114. package/scripts/corpus-dispatch-receipt.mjs +2 -1
  115. package/scripts/corpus-next-seed.mjs +2 -1
  116. package/scripts/corpus-qa.mjs +2 -1
  117. package/scripts/corpus-reconcile.mjs +2 -2
  118. package/scripts/corpus-watchdog.mjs +2 -1
  119. package/scripts/count-chunks.mjs +2 -1
  120. package/scripts/customer-state-matrix.mjs +2 -1
  121. package/scripts/derive-passage-content-map.mjs +2 -1
  122. package/scripts/development-push-check.mjs +2 -1
  123. package/scripts/dispatch-receipt.mjs +2 -1
  124. package/scripts/doc-currency.mjs +2 -1
  125. package/scripts/dream-issue-gate.mjs +2 -1
  126. package/scripts/dual-host-deliberation.mjs +2 -16
  127. package/scripts/dual-host-suggest.mjs +2 -16
  128. package/scripts/eval-brain.mjs +7 -3
  129. package/scripts/execution-policy.mjs +2 -1
  130. package/scripts/execution-preflight.mjs +2 -1
  131. package/scripts/falsify.mjs +2 -1
  132. package/scripts/fix-metaharness-memretrieve.mjs +2 -1
  133. package/scripts/fix-workstream.mjs +2 -1
  134. package/scripts/full-hints.mjs +2 -1
  135. package/scripts/full-suite-gate.mjs +2 -1
  136. package/scripts/grok-host.mjs +68 -0
  137. package/scripts/hook-qualify-core.mjs +3 -1
  138. package/scripts/hook-qualify.mjs +2 -1
  139. package/scripts/hook-retirement-check.mjs +2 -1
  140. package/scripts/host-registry.mjs +2 -1
  141. package/scripts/independent-review-receipt.mjs +2 -1
  142. package/scripts/ingest-new-repos.mjs +2 -1
  143. package/scripts/ingest-repo.mjs +12 -2
  144. package/scripts/integration-evidence.mjs +2 -1
  145. package/scripts/issue-fix.mjs +7 -24
  146. package/scripts/issue-watch.mjs +116 -40
  147. package/scripts/job-heartbeat.sh +54 -19
  148. package/scripts/key-canary.mjs +9 -3
  149. package/scripts/latency-to-surface.mjs +2 -1
  150. package/scripts/learning-replay-cli.mjs +97 -19
  151. package/scripts/learning-replay-execution.mjs +2 -0
  152. package/scripts/learning-replay-fixture.mjs +3 -0
  153. package/scripts/learning-replay-proof.mjs +14 -13
  154. package/scripts/learning-replay.mjs +3 -3
  155. package/scripts/learnings.mjs +2 -1
  156. package/scripts/lesson-ratify.mjs +88 -40
  157. package/scripts/loop-checkpoint.mjs +2 -1
  158. package/scripts/metaharness-receipts.mjs +2 -1
  159. package/scripts/model-catalog.mjs +2 -1
  160. package/scripts/model-currency-evidence.mjs +6 -4
  161. package/scripts/model-currency.mjs +33 -3
  162. package/scripts/model-native-catalog.mjs +2 -1
  163. package/scripts/model-router-agent-hook.mjs +2 -1
  164. package/scripts/model-router-dispatch.mjs +2 -1
  165. package/scripts/model-router-engine.mjs +2 -1
  166. package/scripts/model-router-outcome.mjs +2 -1
  167. package/scripts/model-router-setup.mjs +2 -1
  168. package/scripts/model-routing-eval.mjs +2 -1
  169. package/scripts/model-routing-gateway.mjs +2 -1
  170. package/scripts/model-routing-launchers.mjs +4 -1
  171. package/scripts/model-terminal-gateway.mjs +2 -1
  172. package/scripts/model-terminal-launchers.mjs +2 -1
  173. package/scripts/model-weekly-analyst.mjs +2 -1
  174. package/scripts/model-weekly-cycle.mjs +2 -1
  175. package/scripts/model-weekly-qualification.mjs +2 -1
  176. package/scripts/nightly-gists.sh +26 -20
  177. package/scripts/nightly-two-run-proof.mjs +2 -1
  178. package/scripts/nightly-watchdog.mjs +158 -47
  179. package/scripts/no-silent-substitution.mjs +2 -1
  180. package/scripts/notify.sh +11 -6
  181. package/scripts/ntfy-inbox.mjs +41 -18
  182. package/scripts/onboarding-console.mjs +123 -23
  183. package/scripts/oracle/abstain-threshold-sweep.mjs +2 -1
  184. package/scripts/oracle/abstain-trace.mjs +2 -1
  185. package/scripts/oracle/doc2query-generate.mjs +2 -1
  186. package/scripts/oracle/doc2query-reach.mjs +2 -1
  187. package/scripts/oracle/judge-train.mjs +2 -1
  188. package/scripts/oracle/need-set-split.mjs +2 -1
  189. package/scripts/oracle/produce-questions.mjs +2 -16
  190. package/scripts/oracle/repo-recall.mjs +2 -2
  191. package/scripts/oracle/retrieval-accuracy.mjs +2 -2
  192. package/scripts/oracle/sona-query-adapter-eval.mjs +2 -1
  193. package/scripts/oracle/source-tree.mjs +2 -15
  194. package/scripts/oracle/source-units.mjs +2 -16
  195. package/scripts/oracle/spike-run.mjs +2 -16
  196. package/scripts/oracle/validate-labels.mjs +2 -16
  197. package/scripts/org-repo-count.mjs +2 -1
  198. package/scripts/package-cards.mjs +2 -1
  199. package/scripts/prepublication-evidence.mjs +2 -1
  200. package/scripts/private-overlay.mjs +2 -1
  201. package/scripts/product-integrity-contract.mjs +2 -1
  202. package/scripts/protected-release-invocation.mjs +2 -1
  203. package/scripts/public-verification-abandon.mjs +2 -1
  204. package/scripts/public-verification-aggregate.mjs +2 -1
  205. package/scripts/public-verification-finalizer.mjs +2 -1
  206. package/scripts/public-verification-inputs.mjs +2 -1
  207. package/scripts/public-verification-lane.mjs +2 -1
  208. package/scripts/publication-receipt.mjs +2 -1
  209. package/scripts/published-surface-probe.mjs +2 -1
  210. package/scripts/qe/card-lane-gate.mjs +2 -16
  211. package/scripts/qe/session-start-gate.mjs +2 -16
  212. package/scripts/qe/ux-suite.mjs +2 -1
  213. package/scripts/qualified-candidate-check.mjs +2 -1
  214. package/scripts/rebuild-gists-from-receipts.mjs +2 -1
  215. package/scripts/recall-latency-probe.mjs +48 -0
  216. package/scripts/recommendation-eval.mjs +2 -1
  217. package/scripts/recommendation-floor.mjs +2 -1
  218. package/scripts/recommendation-judge-score.mjs +2 -1
  219. package/scripts/recommendation-real-host-score.mjs +2 -1
  220. package/scripts/recommendation-real-host.mjs +2 -1
  221. package/scripts/refresh-capability-only-store.mjs +2 -1
  222. package/scripts/refresh-model-catalog.mjs +2 -1
  223. package/scripts/rehearse-corpus-pipeline.mjs +2 -1
  224. package/scripts/release-authority.mjs +2 -1
  225. package/scripts/release-evidence-aggregate.mjs +2 -1
  226. package/scripts/release-payload.mjs +2 -1
  227. package/scripts/release-projection.mjs +2 -1
  228. package/scripts/release-proof.mjs +2 -1
  229. package/scripts/release-qualification-contract.mjs +82 -28
  230. package/scripts/release-qualification.mjs +8 -1
  231. package/scripts/release-vector.mjs +2 -1
  232. package/scripts/restore-local-ingests.mjs +2 -1
  233. package/scripts/retrieval-canary.mjs +2 -1
  234. package/scripts/route-cheap.mjs +2 -1
  235. package/scripts/route-gold-rank.mjs +2 -1
  236. package/scripts/route-index-memory.mjs +2 -1
  237. package/scripts/route-latency-warm.mjs +2 -1
  238. package/scripts/router-utilization.mjs +2 -1
  239. package/scripts/routing-flywheel.mjs +2 -1
  240. package/scripts/run-operational-benchmark.mjs +2 -1
  241. package/scripts/run-operational-benchmark.v3.mjs +2 -1
  242. package/scripts/rvf-index-audit.mjs +2 -16
  243. package/scripts/seal-gist-receipt.mjs +2 -1
  244. package/scripts/security-guidance-codex-compat.mjs +2 -1
  245. package/scripts/selfcheck.mjs +2 -1
  246. package/scripts/signal-watch.mjs +2 -1
  247. package/scripts/source-coverage.mjs +2 -1
  248. package/scripts/stabilization-receipt.mjs +2 -1
  249. package/scripts/stamp-sweep.mjs +2 -1
  250. package/scripts/status-honesty.mjs +2 -1
  251. package/scripts/sync-commands.mjs +2 -2
  252. package/scripts/top100-benchmark.mjs +2 -1
  253. package/scripts/top100-corpus.mjs +2 -1
  254. package/scripts/user-model-hook-install.mjs +2 -1
  255. package/scripts/user-model-prompt-hook.mjs +2 -1
  256. package/scripts/verify-bundle.mjs +3 -1
  257. package/scripts/verify-model-catalog.mjs +2 -1
  258. package/scripts/version.mjs +2 -1
  259. package/scripts/wired-check.mjs +13 -5
  260. package/scripts/worktree-integrity.mjs +2 -2
@@ -40,13 +40,17 @@ import { GPT_5_6_SOL_ID } from './review-model-defaults.mjs';
40
40
  const usage = () => `Usage:
41
41
  node scripts/learning-replay.mjs [--trap ${TRAP.MEMORY_SEARCH}|${TRAP.POST_TASK}] [--n N] [--host codex|claude-code] [--model MODEL]
42
42
  node scripts/learning-replay.mjs --check
43
- node scripts/learning-replay.mjs --check-portfolio
43
+ node scripts/learning-replay.mjs --check-portfolio [--tolerate-stale]
44
44
  node scripts/learning-replay.mjs --measure-portfolio --host codex --model MODEL
45
45
  node scripts/learning-replay.mjs --check-mutants
46
+ node scripts/learning-replay.mjs --check-wire (token-free; needs the global ruflo)
46
47
  node scripts/learning-replay.mjs --dry-run
47
48
  node scripts/learning-replay.mjs --mutant <${Object.keys(MUTANTS).join('|')}>
48
49
 
49
- Exit: 0=PASS, 1=FAIL, 3=INCONCLUSIVE, 4=UNKNOWN.`;
50
+ Exit: 0=PASS, 1=FAIL, 3=INCONCLUSIVE, 4=UNKNOWN. --tolerate-stale exits 0 for an UNKNOWN caused only by
51
+ out-of-date evidence (load-bearing files changed since it was measured, or past the age limit) and prints
52
+ STALE-TOLERATED, and only when every trap and mutant artifact still verifies and recomputes to its expected
53
+ result; every FAIL, INCONCLUSIVE and other UNKNOWN in any artifact keeps its exit code.`;
50
54
 
51
55
  function parse(argv) {
52
56
  const has = (flag) => argv.includes(flag);
@@ -57,29 +61,85 @@ function parse(argv) {
57
61
  return { has, arg };
58
62
  }
59
63
 
60
- function wireProbe(dirs, spec, stateDir) {
61
- const result = spawnSync(process.execPath, [
62
- path.join(ROOT, 'plugin', 'scripts', 'hook-shim.mjs'),
63
- 'unprompted-speech', 'UserPromptSubmit',
64
- ], {
64
+ // Fire project B's prompt hook exactly as each host registers it: Claude through hook-shim.mjs
65
+ // (plugin/hooks/hooks.json), Codex through the Brain home's stable codex-hook.mjs wrapper. The user's
66
+ // nudge setting and the runtime's test seams are pinned to the DEFAULT install, so an ambient
67
+ // RUVNET_STOP_NUDGES=full on the measuring machine can never hide a regression that every user has.
68
+ export function wireOutput(dirs, spec, stateDir, host = 'claude-code') {
69
+ const argv = host === 'codex'
70
+ ? [path.join(dirs.brainHome, 'codex-hook.mjs'), 'unprompted-speech', 'UserPromptSubmit']
71
+ : [path.join(ROOT, 'plugin', 'scripts', 'hook-shim.mjs'), 'unprompted-speech', 'UserPromptSubmit'];
72
+ const env = {
73
+ ...process.env,
74
+ RUVNET_BRAIN_HOME: dirs.brainHome,
75
+ RUVNET_BRAIN_STATE_DIR: stateDir,
76
+ RUVNET_CONFIG_ROOT: path.join(dirs.base, 'config'),
77
+ RUVNET_LESSON_STORE: dirs.lessons,
78
+ RUVNET_LESSON_GATE_STATE: dirs.gateState,
79
+ CLAUDE_PLUGIN_ROOT: path.join(ROOT, 'plugin'),
80
+ RUVNET_STOP_NUDGES: '',
81
+ };
82
+ for (const seam of ['RUVNET_UNPROMPTED_CHANNELS', 'RUVNET_UNPROMPTED_PRODUCERS', 'RUVNET_REPLAY_SEQUENCE_FILE']) delete env[seam];
83
+ const result = spawnSync(process.execPath, argv, {
65
84
  input: JSON.stringify({
85
+ hook_event_name: 'UserPromptSubmit',
66
86
  prompt: spec.prompt,
67
- session_id: `dry-${Date.now()}`,
87
+ session_id: `wire-${Date.now()}`,
68
88
  cwd: dirs.projectB,
69
89
  }),
70
90
  encoding: 'utf8',
71
91
  cwd: dirs.projectB,
72
- env: {
73
- ...process.env,
74
- RUVNET_BRAIN_HOME: dirs.brainHome,
75
- RUVNET_BRAIN_STATE_DIR: stateDir,
76
- RUVNET_CONFIG_ROOT: path.join(dirs.base, 'config'),
77
- RUVNET_LESSON_STORE: dirs.lessons,
78
- RUVNET_LESSON_GATE_STATE: dirs.gateState,
79
- CLAUDE_PLUGIN_ROOT: path.join(ROOT, 'plugin'),
80
- },
92
+ timeout: 60_000,
93
+ env,
81
94
  });
82
- return (result.stdout || '').length;
95
+ return result.stdout || '';
96
+ }
97
+
98
+ const wireProbe = (dirs, spec, stateDir) => wireOutput(dirs, spec, stateDir).length;
99
+
100
+ // THE TOKEN-FREE HALF OF D4, measured on every schedule. No model is called, so this does NOT prove a
101
+ // model changed its behaviour (that is --check-portfolio, priced in tokens). It proves the brain's half
102
+ // of the loop on the current code with the real global ruflo: a lesson recorded independently in two
103
+ // projects is promoted, survives a refresh, and reaches project B's prompt on BOTH hosts — while the
104
+ // brain-off control never receives it and deleting the lesson removes it (the causal mutant).
105
+ export function checkWire({ fire = wireOutput, premise = verifyRufloFlag, hosts = ['claude-code', 'codex'] } = {}) {
106
+ const ruflo = premise();
107
+ if (!ruflo.ok) return { status: VERDICT.UNKNOWN, why: `ruflo premise not verified: ${ruflo.why}` };
108
+ const checked = [];
109
+ for (const trap of [TRAP.MEMORY_SEARCH, TRAP.POST_TASK]) {
110
+ const spec = trapSpec(trap);
111
+ const probe = spec.lesson.slice(0, 60);
112
+ const base = allocateRunBase();
113
+ const dirs = buildFixtures(base);
114
+ try {
115
+ const record = recordInProjectA(dirs, { trap });
116
+ if (!record.sources.every((source) => source.recorded)) {
117
+ return { status: VERDICT.UNKNOWN, why: `${trap}: ruflo could not record the lesson in project A`, checked };
118
+ }
119
+ if (!record.ok || !record.promoted) return { status: VERDICT.FAIL, why: `${trap}: lesson was not promoted across two projects`, checked };
120
+ const refresh = nightlyRefresh(dirs);
121
+ if (!refresh.lessonSurvived) return { status: VERDICT.FAIL, why: `${trap}: lesson did not survive refresh`, checked };
122
+ for (const host of hosts) {
123
+ if (fire(dirs, spec, dirs.stateOff, host).includes(probe)) {
124
+ return { status: VERDICT.INCONCLUSIVE, why: `${trap}/${host}: the brain-off control also received the lesson`, checked };
125
+ }
126
+ if (!fire(dirs, spec, dirs.stateOn, host).includes(probe)) {
127
+ return { status: VERDICT.FAIL, why: `${trap}/${host}: the promoted lesson did not reach project B's prompt`, checked };
128
+ }
129
+ }
130
+ saveLessons([], dirs.lessons);
131
+ for (const host of hosts) {
132
+ if (fire(dirs, spec, dirs.stateOn, host).includes(probe)) {
133
+ return { status: VERDICT.FAIL, why: `${trap}/${host}: delete-lesson mutant still delivered it — delivery is not caused by the lesson`, checked };
134
+ }
135
+ }
136
+ checked.push(trap);
137
+ } finally {
138
+ cleanupFixtureDaemons(dirs);
139
+ removeFixture(base);
140
+ }
141
+ }
142
+ return { status: VERDICT.PASS, why: `both lessons reached project B on ${hosts.join(' and ')}; control and delete-lesson mutant stayed silent`, checked };
83
143
  }
84
144
 
85
145
  function printed(label, result) {
@@ -87,6 +147,23 @@ function printed(label, result) {
87
147
  return EXIT[result.status] ?? EXIT.UNKNOWN;
88
148
  }
89
149
 
150
+ // The scheduled workflow reports out-of-date model evidence without paging on it (nothing scheduled can
151
+ // re-measure it); a recomputed FAIL, a forged or missing artifact, or any other UNKNOWN stays non-zero.
152
+ // The strict check stops at the first stale artifact, so tolerance re-checks the WHOLE portfolio (both traps
153
+ // and all four mutants, every transcript verified and recomputed) with only staleness waived, and softens
154
+ // nothing unless that passes: staleness never hides a defect behind it (Codex 4.7.21 r4 #2, Opus r2 #3).
155
+ function portfolioExit(result, tolerateStale) {
156
+ if (!tolerateStale || result.status !== VERDICT.UNKNOWN || result.stale !== true) {
157
+ return printed(`${INVARIANT}-PORTFOLIO`, result);
158
+ }
159
+ const waived = checkPortfolio({ tolerateStale: true });
160
+ if (waived.status !== VERDICT.PASS) return printed(`${INVARIANT}-PORTFOLIO`, waived);
161
+ printed(`${INVARIANT}-PORTFOLIO`, result);
162
+ console.log(' STALE-TOLERATED: every artifact verified and recomputed, but the committed model evidence predates'
163
+ + ' the current code; re-measure to restore a verdict.\n');
164
+ return EXIT.PASS;
165
+ }
166
+
90
167
  // A portfolio includes both positive traps AND all four causal mutants. Run every scenario even
91
168
  // after a failure, then let the unchanged evidence checker decide the result; child exit 1 is the
92
169
  // expected outcome of a causal mutant, and is never by itself a successful portfolio measurement.
@@ -125,8 +202,9 @@ export async function main(argv = process.argv.slice(2)) {
125
202
  if (has('--check')) {
126
203
  return printed(INVARIANT, checkArtifact({ file: PORTFOLIO_RESULT_FILES[trap] }));
127
204
  }
128
- if (has('--check-portfolio')) return printed(`${INVARIANT}-PORTFOLIO`, checkPortfolio());
205
+ if (has('--check-portfolio')) return portfolioExit(checkPortfolio(), has('--tolerate-stale'));
129
206
  if (has('--check-mutants')) return printed(`${INVARIANT}-MUTANTS`, checkMutantArtifacts());
207
+ if (has('--check-wire')) return printed(`${INVARIANT}-WIRE`, checkWire());
130
208
  if (has('--measure-portfolio')) return measurePortfolio({ host, model });
131
209
 
132
210
  const source = checkSourceIdentity();
@@ -151,6 +151,8 @@ export function verifyRufloFlag(bin = RUFLO_BIN) {
151
151
  const result = spawnSync(bin, ['memory', 'search', '--help'], {
152
152
  encoding: 'utf8',
153
153
  timeout: 30_000,
154
+ cwd: os.tmpdir(), // ruflo writes .claude-flow/ into its cwd; never the checkout
155
+
154
156
  env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
155
157
  });
156
158
  const output = `${result.stdout || ''}${result.stderr || ''}`;
@@ -31,6 +31,9 @@ const sh = (cmd, args, options = {}) => {
31
31
  return spawnSync(binary, argv, {
32
32
  encoding: 'utf8',
33
33
  timeout: 120_000,
34
+ // Never the checkout: ruflo writes `.claude-flow/` and `agentdb.rvf.idmap.json` into its cwd, and release
35
+ // qualification requires the checkout to stay byte-identical.
36
+ cwd: os.tmpdir(),
34
37
  ...options,
35
38
  // A ruflo started here would auto-start a daemon and run its in-cwd self-update (`npm install @claude-flow/cli`,
36
39
  // 1.3 GB measured 2026-10-22) in the fixture project; a caller's own env still wins.
@@ -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.')