ruvnet-brain 4.7.19 → 4.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +8 -16
  64. package/plugin/scripts/session-start-health.mjs +97 -10
  65. package/plugin/scripts/session-start-issue-alert.mjs +105 -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 +164 -47
  138. package/scripts/job-heartbeat.sh +145 -13
  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 +30 -21
  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 +549 -0
  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 +71 -26
  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
@@ -83,23 +83,169 @@ function receipt(event, data) {
83
83
  try { fs.appendFileSync(RECEIPTS, JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + '\n'); } catch { /* ledger must not break the engine */ }
84
84
  }
85
85
 
86
- // ── the lock (finding 5): atomic mkdir; stale-pid reclaim (issue-fix.mjs house pattern) ──────────
87
- function acquireLock(target) {
86
+ // ── the lock (finding 5): atomic mkdir; the owner is judged by PROCESS IDENTITY, not pid alone ────
87
+ // 4.7.21 (audit C-opus, reproduced): a pid-only check let any unrelated process that reused the pid hold
88
+ // the spine forever, and an empty owner.json (the window between mkdir and the owner write) was deleted out
89
+ // from under a live writer. The owner now records its process start time; a live pid whose start differs is
90
+ // a reused pid. A lock held by a genuinely live owner is BUSY (exit 75), never success: host convergence
91
+ // must not read "nothing applied" as "applied".
92
+ export const BUSY_EXIT = 75; // EX_TEMPFAIL
93
+ const OWNER_PUBLISH_GRACE_MS = 60_000;
94
+ function processStartOf(pid) {
95
+ try {
96
+ const r = process.platform === 'win32'
97
+ ? spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command',
98
+ `(Get-Process -Id ${Number(pid)} -ErrorAction Stop).StartTime.ToUniversalTime().ToString('o')`], { encoding: 'utf8' })
99
+ : spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8', env: { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' } });
100
+ return r.status === 0 ? String(r.stdout || '').trim() || null : null;
101
+ } catch { return null; }
102
+ }
103
+ const startMs = (text) => Date.parse(/T.*Z$/.test(text) ? text : `${text} UTC`);
104
+ function lockOwnerState(owner) {
105
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid < 1) return 'incomplete';
106
+ try { process.kill(owner.pid, 0); } catch (error) { if (error?.code !== 'EPERM') return 'stale'; }
107
+ const observed = processStartOf(owner.pid);
108
+ if (!observed) return 'live'; // alive and unprovable: never steal it
109
+ if (owner.processStart) return observed === owner.processStart ? 'live' : 'stale';
110
+ // A pre-4.7.21 owner has only startedAt. Its process existed when it wrote that, so a process that
111
+ // started later (lstart has 1 s resolution) is a different process holding a reused pid.
112
+ const written = Date.parse(owner.startedAt || '');
113
+ return Number.isFinite(written) && startMs(observed) > written + 2000 ? 'stale' : 'live';
114
+ }
115
+ function lockAgeMs() {
116
+ let newest = 0;
117
+ for (const p of [LOCK, path.join(LOCK, 'owner.json')]) { try { newest = Math.max(newest, fs.lstatSync(p).mtimeMs); } catch { /* absent */ } }
118
+ return Date.now() - newest;
119
+ }
120
+ // 4.7.21 review (Codex #1, the quarantine path): judging an owner and then moving LOCK aside are two steps, so a
121
+ // contender could move a lock that had changed since it was judged. Moving it back on a mismatch left a window in which
122
+ // a third run's fresh lock blocked the restore; and an owner-less lock (raw null) was indistinguishable from another
123
+ // run's directory between its mkdir and its owner write, so the restore never ran and both runs applied. A takeover now
124
+ // holds an exclusive claim file inside the judged directory and re-checks it (dev/ino + owner bytes) before moving it;
125
+ // a run releases only the lock that carries its own token.
126
+ // Round 4 (Codex #1): mkdir-then-publish left an owner-less moment; a run paused there past the grace was judged debris,
127
+ // claimed, re-checked (still owner-less: "the same") and moved aside with the owner it published meanwhile: two runs
128
+ // applied. LOCK is now built complete in a staging sibling and published by ONE rename, so it never exists without its
129
+ // owner; a rename onto an existing lock fails (ENOTEMPTY/EEXIST, EPERM/EACCES on Windows) and that lock is judged.
130
+ const CLAIM_STALE_MS = 5 * 60_000;
131
+ const CLAIM_FILE = /^takeover(?:-(\d+))?\.json$/;
132
+ function observeLock() {
133
+ const stat = fs.lstatSync(LOCK, { bigint: true });
134
+ if (!stat.isDirectory()) throw Object.assign(new Error('the update lock is not a directory'), { code: 'ENOTDIR' });
135
+ let raw = null;
136
+ try { raw = fs.readFileSync(path.join(LOCK, 'owner.json'), 'utf8'); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
137
+ return { identity: `${stat.dev}:${stat.ino}`, raw };
138
+ }
139
+ const readRaw = (file) => { try { return fs.readFileSync(file, 'utf8'); } catch { return null; } };
140
+ // A claim file must not restart the publication grace (Opus r2 #6): writing or removing one bumps LOCK's mtime, which
141
+ // lockAgeMs reads, so LOCK's own times are put back.
142
+ function keepLockTimes(change) {
143
+ let before = null;
144
+ try { before = fs.lstatSync(LOCK); } catch { /* judged below */ }
145
+ try { return change(); } finally { if (before) try { fs.lutimesSync(LOCK, before.atime, before.mtime); } catch { /* gone */ } }
146
+ }
147
+ const GONE = Symbol('judged lock vanished');
148
+ function claimTakeover() {
149
+ let highest = -1; let highestName = null; let names;
150
+ // The judged lock vanished (another run moved it): re-judge rather than throw out of main (Fable r2 P3-1).
151
+ try { names = fs.readdirSync(LOCK); } catch (error) { if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return GONE; throw error; }
152
+ for (const name of names) {
153
+ const match = CLAIM_FILE.exec(name);
154
+ const level = match ? (match[1] ? Number(match[1]) : 0) : -1;
155
+ if (level > highest) { highest = level; highestName = name; }
156
+ }
157
+ if (highestName) {
158
+ let ageMs = 0;
159
+ try { ageMs = Date.now() - fs.lstatSync(path.join(LOCK, highestName)).mtimeMs; } catch { return null; }
160
+ if (!(ageMs > CLAIM_STALE_MS)) return null; // another run is mid-takeover
161
+ }
162
+ const level = highest + 1;
163
+ const file = path.join(LOCK, level ? `takeover-${level}.json` : 'takeover.json');
164
+ const token = crypto.randomBytes(16).toString('hex');
165
+ try { keepLockTimes(() => fs.writeFileSync(file, JSON.stringify({ token, pid: process.pid, claimedAt: new Date().toISOString() }), { flag: 'wx' })); }
166
+ catch (error) { if (error?.code === 'EEXIST') return null; if (error?.code === 'ENOENT') return GONE; throw error; }
167
+ return { file, token };
168
+ }
169
+ const dropClaim = (claim) => { try { if (readRaw(claim.file)?.includes(claim.token)) keepLockTimes(() => fs.unlinkSync(claim.file)); } catch { /* gone with its directory */ } };
170
+ function removeObservedLock(observed) {
171
+ const claim = claimTakeover();
172
+ if (claim === GONE) return 'changed';
173
+ if (!claim) return 'busy';
174
+ let same = false;
175
+ try {
176
+ const now = observeLock();
177
+ same = now.identity === observed.identity && now.raw === observed.raw && Boolean(readRaw(claim.file)?.includes(claim.token));
178
+ } catch { same = false; }
179
+ if (!same) { dropClaim(claim); return 'changed'; }
180
+ const quarantine = `${LOCK}.stale-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
181
+ try { fs.renameSync(LOCK, quarantine); } catch (error) { if (error?.code === 'ENOENT') return 'changed'; dropClaim(claim); throw error; }
182
+ if (!readRaw(path.join(quarantine, path.basename(claim.file)))?.includes(claim.token)) {
183
+ try { if (!fs.existsSync(LOCK)) fs.renameSync(quarantine, LOCK); } catch { /* keep as evidence */ }
184
+ return 'changed';
185
+ }
186
+ try { fs.rmSync(quarantine, { recursive: true, force: true }); } catch { /* best effort */ }
187
+ return 'removed';
188
+ }
189
+ /** LOCK appears with its owner already inside, by one rename that fails when a populated lock is there. */
190
+ const CONTENDED = new Set(['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES']);
191
+ function publishLock(content) {
192
+ const staged = `${LOCK}.acquire-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
193
+ const dropStaged = () => { try { fs.rmSync(staged, { recursive: true, force: true }); } catch { /* best effort */ } };
194
+ try {
195
+ fs.mkdirSync(staged);
196
+ fs.writeFileSync(path.join(staged, 'owner.json'), content, { flag: 'wx' });
197
+ } catch (error) { dropStaged(); throw error; } // staging failures are real errors, never contention
198
+ try { fs.renameSync(staged, LOCK); } catch (error) {
199
+ dropStaged();
200
+ // A refused publication is contention even if LOCK is gone a moment later: its holder can release in between, and
201
+ // re-checking existsSync here rethrew a raw ENOTEMPTY (exit 1, not BUSY_EXIT) (Opus r3 P2-1). The caller re-observes
202
+ // and tries again when the lock has vanished, as kb/refresh-run.mjs does.
203
+ if (CONTENDED.has(error?.code)) throw Object.assign(new Error('the update lock is held'), { code: 'EEXIST' });
204
+ throw error;
205
+ }
206
+ }
207
+ export function acquireLock(target) {
88
208
  fs.mkdirSync(BRAIN_HOME, { recursive: true });
89
- for (let attempt = 0; attempt < 2; attempt++) {
209
+ const processStart = processStartOf(process.pid);
210
+ for (let attempt = 0; attempt < 3; attempt++) {
211
+ const token = crypto.randomBytes(16).toString('hex');
90
212
  try {
91
- fs.mkdirSync(LOCK); // atomic: exists → EEXIST
92
- writeAtomic(path.join(LOCK, 'owner.json'), JSON.stringify({ pid: process.pid, host: os.hostname(), target, startedAt: new Date().toISOString() }));
93
- return true;
94
- } catch {
95
- const owner = readJSON(path.join(LOCK, 'owner.json'));
96
- try { if (owner && owner.pid) { process.kill(owner.pid, 0); return false; } } catch { /* stale */ }
97
- try { fs.rmSync(LOCK, { recursive: true, force: true }); } catch { /* race — retry once */ }
213
+ publishLock(JSON.stringify({ pid: process.pid, host: os.hostname(), target, token,
214
+ processStart, startedAt: new Date().toISOString() }));
215
+ } catch (error) {
216
+ if (error?.code !== 'EEXIST') throw error;
217
+ let observed;
218
+ try { observed = observeLock(); } catch (observeError) { if (observeError?.code === 'ENOENT') continue; throw observeError; }
219
+ let owner = null;
220
+ try { owner = observed.raw === null ? null : JSON.parse(observed.raw); } catch { /* incomplete */ }
221
+ const state = lockOwnerState(owner);
222
+ if (state === 'live') return { acquired: false, owner };
223
+ if (state === 'incomplete' && lockAgeMs() < OWNER_PUBLISH_GRACE_MS) return { acquired: false, owner: null };
224
+ const outcome = removeObservedLock(observed);
225
+ if (outcome === 'busy') return { acquired: false, owner: null };
226
+ if (outcome === 'removed') {
227
+ receipt('StaleLockReclaimed', { owner: owner ? { pid: owner.pid, host: owner.host, startedAt: owner.startedAt } : null, state });
228
+ }
229
+ continue; // 'changed': judge the directory that is there now
98
230
  }
231
+ return { acquired: true, token };
99
232
  }
100
- return false;
233
+ return { acquired: false, owner: null };
234
+ }
235
+ /** Releases LOCK only while it still carries this run's token: never another run's lock. */
236
+ export function releaseLock(lock) {
237
+ if (!lock?.acquired || !lock.token) return false;
238
+ if (!readRaw(path.join(LOCK, 'owner.json'))?.includes(`"token":"${lock.token}"`)) return false;
239
+ const released = `${LOCK}.released-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
240
+ try { fs.renameSync(LOCK, released); } catch { return false; }
241
+ const moved = readRaw(path.join(released, 'owner.json'));
242
+ if (!moved?.includes(`"token":"${lock.token}"`)) {
243
+ try { if (!fs.existsSync(LOCK)) fs.renameSync(released, LOCK); } catch { /* keep as evidence */ }
244
+ return false;
245
+ }
246
+ try { fs.rmSync(released, { recursive: true, force: true }); } catch { /* gone */ }
247
+ return true;
101
248
  }
102
- function releaseLock() { try { fs.rmSync(LOCK, { recursive: true, force: true }); } catch { /* gone */ } }
103
249
 
104
250
  // ── crash recovery (finding 21): resolve any incomplete transaction BEFORE new work ─────────────
105
251
  function recoverTxn() {
@@ -377,7 +523,11 @@ function main() {
377
523
  return 0;
378
524
  }
379
525
 
380
- if (!acquireLock(argv.join(' '))) { console.log('another update is in progress — exiting (concurrency 1).'); return 0; }
526
+ const lock = acquireLock(argv.join(' '));
527
+ if (!lock.acquired) {
528
+ console.log(`another update is in progress${lock.owner?.pid ? ` (pid ${lock.owner.pid})` : ''} — nothing was applied; exiting busy (${BUSY_EXIT}, concurrency 1).`);
529
+ return BUSY_EXIT;
530
+ }
381
531
  try {
382
532
  recoverTxn();
383
533
 
@@ -456,9 +606,30 @@ function main() {
456
606
  receipt('UpdateChecked', { active: active?.version ?? null, staged: stagedVersion });
457
607
  return applyFromDir(staged.dir, stagedVersion, active ? 'auto-update' : 'auto-seed') ? 0 : 1;
458
608
  } finally {
459
- releaseLock();
609
+ releaseLock(lock);
460
610
  if (has('--gc') || !has('--no-gc')) { try { gc(); } catch { /* GC never fails the run */ } }
461
611
  }
462
612
  }
463
613
 
464
- if (process.env.RUVNET_BRAIN_IMPORT_ONLY !== 'update-shell-test') process.exit(main());
614
+ // #420 item 6: `--help` used to fall through to the default action (take the update lock and apply). Usage is derived from
615
+ // the flags main() reads; help and unknown flags answer BEFORE main() so they touch no state (no BRAIN_HOME mkdir, lock, network).
616
+ const USAGE = [
617
+ 'usage: node update-apply.mjs [--auto [--expected-version <v>] | --seed | --from-dir <path> | --rollback | --dev [pluginDir] | --dev-off | --doctor] [--gc | --no-gc]',
618
+ ' (no flag = --auto: newest host-staged plugin version, applied only if newer than active)',
619
+ ' --auto [--expected-version <v>] unattended: apply the host-staged payload, optionally pinned to exactly <v>',
620
+ ' --seed seed the spine from the running plugin install',
621
+ ' --from-dir <path> apply a specific plugin payload dir',
622
+ ' --rollback flip back to the previous generation',
623
+ ' --dev [pluginDir] dev mode: point codeRoot at a checkout\'s plugin/',
624
+ ' --dev-off leave dev mode',
625
+ ' --doctor report spine state (read-only)',
626
+ ' --gc / --no-gc force / skip collecting unreferenced generations after the run',
627
+ ' --help, -h print this usage and exit without touching any state',
628
+ ].join('\n');
629
+ const KNOWN_FLAGS = new Set(['--auto', '--seed', '--from-dir', '--rollback', '--dev', '--dev-off', '--doctor', '--gc', '--no-gc', '--expected-version']);
630
+ if (process.env.RUVNET_BRAIN_IMPORT_ONLY !== 'update-shell-test') {
631
+ if (has('--help') || has('-h')) { console.log(USAGE); process.exit(0); }
632
+ const unknown = argv.filter((a) => a.startsWith('-') && !KNOWN_FLAGS.has(a));
633
+ if (unknown.length) { console.error(`✗ unknown argument: ${unknown.join(' ')}\n${USAGE}`); process.exit(2); }
634
+ process.exit(main());
635
+ }
@@ -5,6 +5,7 @@ import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { spawnSync } from 'node:child_process';
8
+ import { isDirectInvocation } from '../../../scripts/entrypoint.mjs';
8
9
 
9
10
  export const REQUIRED_CHECKS = ['ci', 'integration-linux', 'stranger-matrix', 'ux-qe', 'release-qe'];
10
11
 
@@ -323,4 +324,4 @@ export function main(args = process.argv.slice(2)) {
323
324
  }
324
325
  }
325
326
 
326
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main();
327
+ if (isDirectInvocation(import.meta.url)) process.exitCode = main();
@@ -41,8 +41,8 @@ went by (web searches, throwaway installs, scratch builds) before a single tool
41
41
  So, before the DO FIRST list and before beat 1:
42
42
 
43
43
  - **Say the one capability that would materially help, in ONE sentence, within ~2 minutes** —
44
- *"Consider `<capability>` — `<concrete benefit for THIS task>`. Say 'use `<capability>`' and I'll
45
- wire it, or ignore this."* Then carry straight on. If nothing genuinely fits, **say nothing about
44
+ *"`<capability>` fits here — `<concrete benefit for THIS task>`; 'use `<capability>`' wires it."*
45
+ A statement, never a question or approval dialog. Then carry straight on. If nothing genuinely fits, **say nothing about
46
46
  tooling at all**; silence is the correct and common answer, and a forced tie-in is worse than none.
47
47
  - **Budget: at most THREE `search_ruvnet` calls to get there.** Enough to confirm the capability
48
48
  exists and what it is called — not enough to design the integration, and not meant to be.
@@ -23,8 +23,9 @@ missing. A colleague who knows the right tool and tells you twenty minutes in di
23
23
  So, on any build/plan/fix request, in this order:
24
24
 
25
25
  1. **Within ~2 minutes, say the one thing that would help — or say nothing.** One or two sentences:
26
- *"Consider `<capability>` — `<the concrete benefit for THIS task>`. Say 'use `<capability>`' and
27
- I'll wire it, or ignore this and I'll carry on."* Then get straight on with the actual work. If
26
+ *"`<capability>` fits here — `<the concrete benefit for THIS task>`; 'use `<capability>`' wires it."*
27
+ It is a statement, not a question: never open a question or approval dialog for an optional
28
+ recommendation, and it authorizes no install. Then get straight on with the actual work. If
28
29
  nothing genuinely fits, say nothing at all about tooling — silence is the correct and common
29
30
  answer, and a forced tie-in is worse than no recommendation (rule 4 below).
30
31
  2. **At most THREE `search_ruvnet` calls before that first recommendation.** Three is enough to
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { verifyCapabilityClaimAggregate } from '../plugin/scripts/capability-claim-evidence.mjs';
7
7
  import { LEGACY_REVIEW_MODEL_IDS } from './review-model-defaults.mjs';
8
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
8
9
 
9
10
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
11
  const REQUIRED_REVIEWERS = LEGACY_REVIEW_MODEL_IDS;
@@ -91,7 +92,7 @@ export function evaluateCompletion({ root = ROOT, run = command,
91
92
  return { ok: failures.length === 0, head, version, receiptFile, failures };
92
93
  }
93
94
 
94
- if (((() => { try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })())) {
95
+ if (isDirectInvocation(import.meta.url)) {
95
96
  const receiptIndex = process.argv.indexOf('--receipt');
96
97
  const result = evaluateCompletion({ receiptFile: receiptIndex >= 0 ? path.resolve(process.argv[receiptIndex + 1]) : undefined });
97
98
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -39,6 +39,7 @@ import fs from 'node:fs';
39
39
  import path from 'node:path';
40
40
  import { spawnSync } from 'node:child_process';
41
41
  import { fileURLToPath } from 'node:url';
42
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
42
43
 
43
44
  const HERE = path.dirname(fileURLToPath(import.meta.url));
44
45
  export const REPO_ROOT = path.resolve(HERE, '..');
@@ -210,7 +211,7 @@ function main() {
210
211
  }
211
212
 
212
213
  function isMain() {
213
- try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
214
+ try { return isDirectInvocation(import.meta.url); }
214
215
  catch { return false; }
215
216
  }
216
217
  if (isMain()) process.exit(main());
@@ -67,9 +67,25 @@ async function doctor(proj) {
67
67
  let gitlog = '';
68
68
  try { gitlog = execFileSync('git', ['-C', proj, 'log', '--oneline', '-8'], { encoding: 'utf8', timeout: 10000 }).trim(); } catch { /* not a repo */ }
69
69
  const val = `SEEDED CHECKPOINT ${new Date().toISOString()} (agentdb-fleet-doctor — no canonical checkpoint existed; the session-start hook had nothing to surface). Recent git history:\n${gitlog || '(not a git repo)'}\nMaintain from now on: append a NEW project-state-current-<epochms> row after meaningful work; never overwrite.`;
70
+ const seedKey = `project-state-current-${Date.now()}`;
70
71
  fs.copyFileSync(db, `${db}.bak-fleet-doctor-${Date.now()}`);
71
- const r = ruflo(proj, ['memory', 'store', '-k', `project-state-current-${Date.now()}`, '--value', val, '-n', name]);
72
- row.seeded = r.status === 0 && /stored successfully/i.test(r.out);
72
+ const r = ruflo(proj, ['memory', 'store', '-k', seedKey, '--value', val, '-n', name]);
73
+ // ADR-063 / 2026-08-13 incident: `ruflo memory store`'s own "[OK] ... stored successfully"
74
+ // wording is not evidence of a write — that exact line was on stdout throughout the incident
75
+ // that left three days of memory unrecoverable. The only accepted proof is retrieving the SAME
76
+ // key back through the managed interface and reading the VALUE (degradation-watch.mjs's
77
+ // proveMemoryDurable() / record-lesson.mjs precedent). The FIX-2 canary below round-trips a
78
+ // DIFFERENT key later in this function — it proves the namespace is writable in general, not
79
+ // that THIS seeded checkpoint's write specifically persisted.
80
+ row.seeded = false;
81
+ if (r.status === 0) {
82
+ const back = ruflo(proj, ['memory', 'retrieve', '-k', seedKey, '-n', name, '--value-only']);
83
+ // A 60-char PREFIX match, not the full multi-line value: `val` embeds `gitlog`, and this file's
84
+ // own FIX-2 canary check (below) already establishes that this CLI's output can truncate/reflow
85
+ // multi-line content in ways a full-string match would false-negative on. The prefix carries the
86
+ // per-write ISO timestamp, so it still identifies THIS write, not a stale or unrelated one.
87
+ row.seeded = back.status === 0 && back.out.includes(val.slice(0, 60));
88
+ }
73
89
  row.checkpoint = row.seeded;
74
90
  }
75
91
 
@@ -79,16 +95,22 @@ async function doctor(proj) {
79
95
  row.episodes = Number(sql(db, 'SELECT count(*) FROM episodes;') ?? 0);
80
96
  }
81
97
 
82
- // VERIFY: real round-trip through the real CLI in the project's own namespace.
98
+ // VERIFY: real round-trip through the real CLI in the project's own namespace — EXACT KEY, never
99
+ // a semantic `memory search` match. The prior check matched a generic substring every canary this
100
+ // project has EVER written shares ("fleet-doctor-cana…" / "round-trip canary"), not THIS run's
101
+ // unique key/value, so a stale canary from a PRIOR run still ranked by `memory search` made a run
102
+ // whose CURRENT write silently failed report `roundtrip: true` anyway — the same "claimed success
103
+ // is not evidence of a write" gap already fixed this way in `record-lesson.mjs`, `distill-
104
+ // project.mjs`, and `health-repair.mjs`'s fleet receipt, and the exact discipline `ground-
105
+ // ruvnet.sh` already documents for every other write in this repo: "never infer a broken write
106
+ // from... semantic-search misses... unique-key store -> exact-key retrieve and read the returned
107
+ // VALUE."
83
108
  const canaryKey = `fleet-doctor-canary-${Date.now()}`;
84
109
  const canaryVal = `round-trip canary ${new Date().toISOString()}`;
85
110
  const stored = ruflo(proj, ['memory', 'store', '-k', canaryKey, '--value', canaryVal, '-n', name]);
86
111
  if (stored.status === 0) {
87
- const found = ruflo(proj, ['memory', 'search', '-q', 'fleet-doctor round-trip canary', '-n', name, '--limit', '3']);
88
- // The result table TRUNCATES keys (`fleet-doctor-cana...`) — asserting on the full key was a
89
- // false-negative bug (caught on first run, 2026-07-13, SpotOn: store fine, MY check wrong —
90
- // the exact lesson-agentdb-was-never-broken defect class). Match what the table can show.
91
- row.roundtrip = found.out.includes('fleet-doctor-cana') || found.out.includes('round-trip canary');
112
+ const found = ruflo(proj, ['memory', 'retrieve', '-k', canaryKey, '-n', name, '--value-only']);
113
+ row.roundtrip = found.status === 0 && found.out.trim() === canaryVal;
92
114
  }
93
115
  return row;
94
116
  }
@@ -48,6 +48,7 @@ import path from 'node:path';
48
48
  import { spawnSync } from 'node:child_process';
49
49
  import { retryTransientResult } from './transient-gh.mjs';
50
50
  import { fileURLToPath, pathToFileURL } from 'node:url';
51
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
51
52
 
52
53
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
53
54
  export const APPROVED_RUNTIME_KIND = 'ruvnet-brain-approved-runtime';
@@ -417,6 +418,6 @@ async function main() {
417
418
  return 0;
418
419
  }
419
420
 
420
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
421
+ if (isDirectInvocation(import.meta.url)) {
421
422
  main().then((code) => { process.exitCode = code; });
422
423
  }
@@ -3,6 +3,7 @@
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { REPO_ROOT, evaluateDoc, blockingFindings } from './doc-currency.mjs';
6
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
6
7
 
7
8
  export const GOVERNING_ADR = 'docs/adr/0103-routing-architecture-review-lock.md';
8
9
 
@@ -39,6 +40,6 @@ export function main(argv = process.argv.slice(2)) {
39
40
  return result.pass ? 0 : 1;
40
41
  }
41
42
 
42
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
43
+ if (isDirectInvocation(import.meta.url)) {
43
44
  process.exitCode = main();
44
45
  }
@@ -40,6 +40,7 @@ import path from 'node:path';
40
40
  import crypto from 'node:crypto';
41
41
  import { spawnSync } from 'node:child_process';
42
42
  import { fileURLToPath } from 'node:url';
43
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
43
44
 
44
45
  const HERE = path.dirname(fileURLToPath(import.meta.url));
45
46
  export const REPO_ROOT = path.resolve(HERE, '..');
@@ -230,7 +231,7 @@ function main() {
230
231
  }
231
232
 
232
233
  function isMain() {
233
- try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
234
+ try { return isDirectInvocation(import.meta.url); }
234
235
  catch { return false; }
235
236
  }
236
237
  if (isMain()) process.exit(main());
@@ -5,6 +5,7 @@ import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
8
9
 
9
10
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
11
 
@@ -222,6 +223,6 @@ export async function main(argv = process.argv.slice(2)) {
222
223
  return summary.passed === summary.questions && summary.timedOut === 0 ? 0 : 1;
223
224
  }
224
225
 
225
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
226
+ if (isDirectInvocation(import.meta.url)) {
226
227
  process.exitCode = await main();
227
228
  }
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import readline from 'node:readline';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { performance } from 'node:perf_hooks';
9
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
9
10
 
10
11
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
12
 
@@ -182,21 +183,6 @@ async function main() {
182
183
  console.log(`REPORT ${output}`);
183
184
  }
184
185
 
185
- // Entry-point guard. Compares REALPATHS on both sides: path.resolve() normalizes a path but does
186
- // NOT follow symlinks, while import.meta.url IS symlink-resolved by Node. Through a symlink (npm bin
187
- // shims, wrapper scripts, and every os.tmpdir() path on macOS) the two sides disagree, so main()
188
- // never runs -- and because nothing throws, the process exits 0. A silent exit 0 is indistinguishable
189
- // from "ran, found nothing", which is how prepareCorpusCandidate once reported SUCCESS with no
190
- // archive on disk. Reproduced live 2026-07-27; pinned by tests/unit/entrypoint-symlink.test.mjs.
191
- function isDirectInvocation() {
192
- try {
193
- if (!process.argv[1]) return false;
194
- return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
195
- } catch {
196
- return false;
197
- }
198
- }
199
-
200
- if (isDirectInvocation()) {
186
+ if (isDirectInvocation(import.meta.url)) {
201
187
  main().catch((error) => { console.error(error.stack || error.message); process.exitCode = 1; });
202
188
  }
@@ -30,6 +30,7 @@
30
30
  import fs from 'node:fs';
31
31
  import path from 'node:path';
32
32
  import { fileURLToPath } from 'node:url';
33
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
33
34
 
34
35
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
35
36
  const DAY = 86_400_000;
@@ -203,12 +204,22 @@ export async function readCoverage(rootOverride) {
203
204
  };
204
205
  }
205
206
 
206
- /** A reading past its dimension's `maxAgeDays`, or with no timestamp at all, is not current. */
207
+ /**
208
+ * A reading past its dimension's `maxAgeDays`, or with no timestamp at all, is not current.
209
+ *
210
+ * MEASURED 2026-10-16: a reading dated AFTER `now` (clock skew between the writer and this read, or
211
+ * a corrupt/hand-edited `recorded`/`generatedAt` stamp) produced a NEGATIVE `ageDays` — e.g. `-3` —
212
+ * which is less than any positive `maxAgeDays`, so `stale` came back `false` with `why: null`. A
213
+ * corrupted-into-the-future timestamp is exactly as untrustworthy as a corrupted-into-the-past one;
214
+ * this file's own stated purpose (see header, ISSUE #258) is that a reading nobody can vouch for is
215
+ * never silently used, and a future stamp was sailing through as BETTER than fresh.
216
+ */
207
217
  export function staleness(at, maxAgeDays, now = Date.now()) {
208
218
  if (!at) return { stale: true, ageDays: null, why: 'no timestamp recorded' };
209
219
  const ms = now - Date.parse(at);
210
220
  if (!Number.isFinite(ms)) return { stale: true, ageDays: null, why: 'unparseable timestamp' };
211
221
  const ageDays = Math.round((ms / DAY) * 10) / 10;
222
+ if (ageDays < 0) return { stale: true, ageDays, why: `${ageDays}d — recorded in the future (clock skew or corrupt timestamp)` };
212
223
  return { stale: ageDays > maxAgeDays, ageDays, why: ageDays > maxAgeDays ? `${ageDays}d > ${maxAgeDays}d budget` : null };
213
224
  }
214
225
 
@@ -250,7 +261,7 @@ export function composite(dims, kind) {
250
261
  }
251
262
 
252
263
  const isMain = (() => {
253
- try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); }
264
+ try { return isDirectInvocation(import.meta.url); }
254
265
  catch { return false; }
255
266
  })();
256
267
 
@@ -69,6 +69,7 @@ import { orgRepoCount } from './org-repo-count.mjs';
69
69
  import { assertCapabilityOnlyStore } from '../kb/capability-only.mjs';
70
70
  import { CURRENCY_BASIS, corpusCurrencyBlock } from './corpus-currency.mjs';
71
71
  import { loadFixture } from './oracle/repo-recall.mjs';
72
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
72
73
 
73
74
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
74
75
 
@@ -1097,7 +1098,7 @@ node forge-ask.mjs --dir . --name ruvector --variant big --q "what is the RVF co
1097
1098
  }
1098
1099
 
1099
1100
  // ---- CLI -----------------------------------------------------------------------------------------
1100
- if (((() => { try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })())) {
1101
+ if (isDirectInvocation(import.meta.url)) {
1101
1102
  const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
1102
1103
  // Corpus content is a release asset, not a Git blob. A clean worktree has zero stores unless the
1103
1104
  // operator supplies the canonical external corpus directory (or has already built one into kb/,
@@ -22,6 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
  import { observeSourceUniverse } from './source-coverage.mjs';
23
23
  import { materializePublicInputs } from './public-inputs.mjs';
24
24
  import { buildConceptAggregate } from './corpus-aggregates.mjs';
25
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
25
26
 
26
27
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
28
  const KB = path.join(ROOT, 'kb');
@@ -44,7 +45,7 @@ export async function main() {
44
45
  }
45
46
  }
46
47
 
47
- if (((() => { try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })())) {
48
+ if (isDirectInvocation(import.meta.url)) {
48
49
  main().catch((error) => {
49
50
  console.error(`[build-concepts] ${error.message}`);
50
51
  process.exitCode = 1;
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { verifyPayloadMembers } from './release-payload.mjs';
7
7
  import { validateQualificationReceipt } from './release-qualification.mjs';
8
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
8
9
 
9
10
  export const REQUIRED_CI_JOBS = ['candidate-preflight', 'release-acceptance-linux',
10
11
  'release-acceptance-windows', 'release-acceptance-macos', 'release-qe'];
@@ -50,7 +51,7 @@ export function createCandidateCiReceipt({ sha, runId, runAttempt, results, ciRe
50
51
  verdict: 'PASS', skipped: 0, unknown: 0 };
51
52
  }
52
53
 
53
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
54
+ if (isDirectInvocation(import.meta.url)) {
54
55
  const receipt = createCandidateCiReceipt({ sha: process.env.CANDIDATE_SHA,
55
56
  runId: Number(process.env.GITHUB_RUN_ID), runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
56
57
  ciResult: process.env.CI_RESULT,
@@ -6,6 +6,7 @@ import { stagedHostVerifier, readCandidateRetrieval, verifyCandidateRetrievalAss
6
6
  import { payloadIdFor } from './release-payload.mjs';
7
7
  import { validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
8
8
  import { HOST_WARMUP_TIMEOUT_MS, RELEASE_SEARCH_DEADLINE_MS } from './host-install-matrix.mjs';
9
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
9
10
 
10
11
  const arg = (name) => {
11
12
  const index = process.argv.indexOf(name);
@@ -93,7 +94,7 @@ export async function buildCandidateHostEvidence({ manifestFile, packagePath, bu
93
94
  };
94
95
  }
95
96
 
96
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
97
+ if (isDirectInvocation(import.meta.url)) {
97
98
  const evidence = await buildCandidateHostEvidence({ manifestFile: arg('--manifest'),
98
99
  packagePath: arg('--package'), bundlePath: arg('--bundle'), planFile: arg('--plan'), coverageFile: arg('--coverage'), failureFile: arg('--out') ? `${arg('--out')}.failure.json` : null },
99
100
  { sequentialSearches: process.argv.includes('--sequential-searches') });
@@ -29,6 +29,7 @@ import fs from 'node:fs';
29
29
  import path from 'node:path';
30
30
  import { fileURLToPath } from 'node:url';
31
31
  import { storeRoot, darkStores } from '../kb/store-root.mjs';
32
+ import { isDirectInvocation } from '../plugin/scripts/entrypoint.mjs';
32
33
 
33
34
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
34
35
  const OWNER = process.env.RUVNET_ORG_OWNER || 'ruvnet';
@@ -104,7 +105,7 @@ export function applyCardToFiles(files, card, name, insert = insertSorted) {
104
105
  }
105
106
 
106
107
  const isMain = (() => {
107
- try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
108
+ try { return isDirectInvocation(import.meta.url); }
108
109
  catch { return false; }
109
110
  })();
110
111
 
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { createRequire } from 'node:module';
9
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
10
  import { requiredEmbedderModels } from '../../plugin/test/model-cache.mjs';
11
+ import { isDirectInvocation } from '../../plugin/scripts/entrypoint.mjs';
11
12
 
12
13
  async function main() {
13
14
  const modelCache = process.env.KB_MODEL_CACHE;
@@ -25,4 +26,4 @@ async function main() {
25
26
  }
26
27
  }
27
28
 
28
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
29
+ if (isDirectInvocation(import.meta.url)) await main();