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.
- package/README.md +2 -2
- package/bin/install.mjs +494 -97
- package/kb/download-retry.mjs +93 -21
- package/kb/forge-guard-injection.mjs +277 -0
- package/kb/forge-update.mjs +166 -38
- package/kb/lifecycle-evidence-retention.mjs +151 -14
- package/kb/model-requirements.mjs +4 -1
- package/kb/refresh-run.mjs +175 -27
- package/kb/verify-citation.mjs +15 -2
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +16 -8
- package/plugin/scripts/adr-currency-gate.mjs +2 -1
- package/plugin/scripts/advisory-contract.mjs +21 -0
- package/plugin/scripts/advocacy-route.mjs +5 -4
- package/plugin/scripts/agentdb-recall.mjs +15 -5
- package/plugin/scripts/automatic-update.mjs +9 -1
- package/plugin/scripts/brain-confirmation.mjs +2 -1
- package/plugin/scripts/brain-footprint.mjs +72 -11
- package/plugin/scripts/capability-registry.mjs +1 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +2 -1
- package/plugin/scripts/codex-overnight-notice.mjs +33 -0
- package/plugin/scripts/completion-claim-evidence.mjs +56 -12
- package/plugin/scripts/continuation-gate.mjs +28 -9
- package/plugin/scripts/decision-gate.mjs +13 -63
- package/plugin/scripts/decision-outcomes.mjs +36 -0
- package/plugin/scripts/decision-recall-gate.mjs +40 -5
- package/plugin/scripts/decision-record.mjs +8 -3
- package/plugin/scripts/degradation-watch.mjs +2 -1
- package/plugin/scripts/developer-update-lock.mjs +172 -33
- package/plugin/scripts/developer-update-owner.mjs +8 -5
- package/plugin/scripts/developer-update-policy.mjs +4 -1
- package/plugin/scripts/developer-update.mjs +155 -53
- package/plugin/scripts/doc-currency.mjs +2 -16
- package/plugin/scripts/duplicate-gate.mjs +2 -1
- package/plugin/scripts/entrypoint.mjs +34 -0
- package/plugin/scripts/footprint-io.mjs +135 -8
- package/plugin/scripts/grok-hook-adapter.mjs +2 -1
- package/plugin/scripts/ground-ruvnet.sh +6 -3
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-code-projection.mjs +2 -1
- package/plugin/scripts/grounding-substance.mjs +2 -1
- package/plugin/scripts/grounding-turn-gate.mjs +2 -2
- package/plugin/scripts/grounding-turn-mark.mjs +2 -2
- package/plugin/scripts/hook-context-budget.mjs +2 -1
- package/plugin/scripts/hook-input.mjs +2 -2
- package/plugin/scripts/hook-shim.mjs +9 -2
- package/plugin/scripts/host-update.mjs +29 -5
- package/plugin/scripts/identifier-preflight.mjs +2 -1
- package/plugin/scripts/kb-copy-proof.mjs +57 -9
- package/plugin/scripts/learn-capture.mjs +2 -1
- package/plugin/scripts/learning-store.mjs +62 -16
- package/plugin/scripts/lesson-bridge.mjs +2 -2
- package/plugin/scripts/lesson-promote.mjs +83 -22
- package/plugin/scripts/md-stamp.mjs +2 -1
- package/plugin/scripts/native-turn-transcript.mjs +98 -6
- package/plugin/scripts/nightly-scheduler.mjs +10 -5
- package/plugin/scripts/package-recommender.mjs +2 -2
- package/plugin/scripts/project-store-resolver.mjs +19 -0
- package/plugin/scripts/routing-outcome-capture.mjs +2 -1
- package/plugin/scripts/session-snapshot-hook.mjs +24 -4
- package/plugin/scripts/session-start-core.mjs +8 -16
- package/plugin/scripts/session-start-health.mjs +97 -10
- package/plugin/scripts/session-start-issue-alert.mjs +105 -0
- package/plugin/scripts/session-start-update-plane.mjs +15 -1
- package/plugin/scripts/signal-watch.mjs +2 -1
- package/plugin/scripts/spend-guard.mjs +2 -1
- package/plugin/scripts/substitution-wall.mjs +2 -1
- package/plugin/scripts/turn-outcome-capture.mjs +7 -3
- package/plugin/scripts/unprompted-runtime.mjs +20 -3
- package/plugin/scripts/update-apply.mjs +186 -15
- package/plugin/skills/release-proof/scripts/release-proof.mjs +2 -1
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +2 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +3 -2
- package/scripts/adr-072-completion.mjs +2 -1
- package/scripts/agentdb-context.mjs +2 -1
- package/scripts/agentdb-fleet-doctor.mjs +30 -8
- package/scripts/approved-runtime.mjs +2 -1
- package/scripts/architecture-review-lock.mjs +2 -1
- package/scripts/ascii-drift.mjs +2 -1
- package/scripts/brain-latency-50.mjs +2 -1
- package/scripts/brain-novice-50.mjs +2 -16
- package/scripts/brain-score.mjs +13 -2
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/build-concepts.mjs +2 -1
- package/scripts/candidate-ci-receipt.mjs +2 -1
- package/scripts/candidate-host-evidence.mjs +2 -1
- package/scripts/card-from-source.mjs +2 -1
- package/scripts/ci/warm-brain-models.mjs +2 -1
- package/scripts/claims-verify.mjs +7 -3
- package/scripts/claude-controlled-terminal.mjs +2 -1
- package/scripts/claude-terminal-mod.mjs +2 -1
- package/scripts/code-release-corpus.mjs +2 -1
- package/scripts/codex-hook-trust-reconcile.mjs +67 -1
- package/scripts/codex-hook-trust.mjs +40 -1
- package/scripts/codex-hook-wrapper-history.mjs +2 -1
- package/scripts/codex-host-execution-proof.mjs +2 -1
- package/scripts/completion-claim-replay.mjs +2 -1
- package/scripts/console-engine.mjs +8 -3
- package/scripts/corpus-canary.mjs +2 -1
- package/scripts/corpus-candidate.mjs +2 -2
- package/scripts/corpus-coverage-sidecar.mjs +2 -1
- package/scripts/corpus-dispatch-decision.mjs +2 -1
- package/scripts/corpus-dispatch-receipt.mjs +2 -1
- package/scripts/corpus-next-seed.mjs +2 -1
- package/scripts/corpus-qa.mjs +2 -1
- package/scripts/corpus-reconcile.mjs +2 -2
- package/scripts/corpus-watchdog.mjs +2 -1
- package/scripts/count-chunks.mjs +2 -1
- package/scripts/customer-state-matrix.mjs +2 -1
- package/scripts/derive-passage-content-map.mjs +2 -1
- package/scripts/development-push-check.mjs +2 -1
- package/scripts/dispatch-receipt.mjs +2 -1
- package/scripts/doc-currency.mjs +2 -1
- package/scripts/dream-issue-gate.mjs +2 -1
- package/scripts/dual-host-deliberation.mjs +2 -16
- package/scripts/dual-host-suggest.mjs +2 -16
- package/scripts/eval-brain.mjs +7 -3
- package/scripts/execution-policy.mjs +2 -1
- package/scripts/execution-preflight.mjs +2 -1
- package/scripts/falsify.mjs +2 -1
- package/scripts/fix-metaharness-memretrieve.mjs +2 -1
- package/scripts/fix-workstream.mjs +2 -1
- package/scripts/full-hints.mjs +2 -1
- package/scripts/full-suite-gate.mjs +2 -1
- package/scripts/grok-host.mjs +68 -0
- package/scripts/hook-qualify-core.mjs +3 -1
- package/scripts/hook-qualify.mjs +2 -1
- package/scripts/hook-retirement-check.mjs +2 -1
- package/scripts/host-registry.mjs +2 -1
- package/scripts/independent-review-receipt.mjs +2 -1
- package/scripts/ingest-new-repos.mjs +2 -1
- package/scripts/ingest-repo.mjs +12 -2
- package/scripts/integration-evidence.mjs +2 -1
- package/scripts/issue-fix.mjs +7 -24
- package/scripts/issue-watch.mjs +164 -47
- package/scripts/job-heartbeat.sh +145 -13
- package/scripts/key-canary.mjs +9 -3
- package/scripts/latency-to-surface.mjs +2 -1
- package/scripts/learning-replay-cli.mjs +97 -19
- package/scripts/learning-replay-execution.mjs +2 -0
- package/scripts/learning-replay-fixture.mjs +3 -0
- package/scripts/learning-replay-proof.mjs +14 -13
- package/scripts/learning-replay.mjs +3 -3
- package/scripts/learnings.mjs +2 -1
- package/scripts/lesson-ratify.mjs +88 -40
- package/scripts/loop-checkpoint.mjs +2 -1
- package/scripts/metaharness-receipts.mjs +2 -1
- package/scripts/model-catalog.mjs +2 -1
- package/scripts/model-currency-evidence.mjs +6 -4
- package/scripts/model-currency.mjs +33 -3
- package/scripts/model-native-catalog.mjs +2 -1
- package/scripts/model-router-agent-hook.mjs +2 -1
- package/scripts/model-router-dispatch.mjs +2 -1
- package/scripts/model-router-engine.mjs +2 -1
- package/scripts/model-router-outcome.mjs +2 -1
- package/scripts/model-router-setup.mjs +2 -1
- package/scripts/model-routing-eval.mjs +2 -1
- package/scripts/model-routing-gateway.mjs +2 -1
- package/scripts/model-routing-launchers.mjs +4 -1
- package/scripts/model-terminal-gateway.mjs +2 -1
- package/scripts/model-terminal-launchers.mjs +2 -1
- package/scripts/model-weekly-analyst.mjs +2 -1
- package/scripts/model-weekly-cycle.mjs +2 -1
- package/scripts/model-weekly-qualification.mjs +2 -1
- package/scripts/nightly-gists.sh +30 -21
- package/scripts/nightly-two-run-proof.mjs +2 -1
- package/scripts/nightly-watchdog.mjs +158 -47
- package/scripts/no-silent-substitution.mjs +2 -1
- package/scripts/notify.sh +11 -6
- package/scripts/ntfy-inbox.mjs +549 -0
- package/scripts/onboarding-console.mjs +47 -8
- package/scripts/oracle/abstain-threshold-sweep.mjs +2 -1
- package/scripts/oracle/abstain-trace.mjs +2 -1
- package/scripts/oracle/doc2query-generate.mjs +2 -1
- package/scripts/oracle/doc2query-reach.mjs +2 -1
- package/scripts/oracle/judge-train.mjs +2 -1
- package/scripts/oracle/need-set-split.mjs +2 -1
- package/scripts/oracle/produce-questions.mjs +2 -16
- package/scripts/oracle/repo-recall.mjs +2 -2
- package/scripts/oracle/retrieval-accuracy.mjs +2 -2
- package/scripts/oracle/sona-query-adapter-eval.mjs +2 -1
- package/scripts/oracle/source-tree.mjs +2 -15
- package/scripts/oracle/source-units.mjs +2 -16
- package/scripts/oracle/spike-run.mjs +2 -16
- package/scripts/oracle/validate-labels.mjs +2 -16
- package/scripts/org-repo-count.mjs +2 -1
- package/scripts/package-cards.mjs +2 -1
- package/scripts/prepublication-evidence.mjs +2 -1
- package/scripts/private-overlay.mjs +2 -1
- package/scripts/product-integrity-contract.mjs +2 -1
- package/scripts/protected-release-invocation.mjs +2 -1
- package/scripts/public-verification-abandon.mjs +2 -1
- package/scripts/public-verification-aggregate.mjs +2 -1
- package/scripts/public-verification-finalizer.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +2 -1
- package/scripts/public-verification-lane.mjs +2 -1
- package/scripts/publication-receipt.mjs +2 -1
- package/scripts/published-surface-probe.mjs +2 -1
- package/scripts/qe/card-lane-gate.mjs +2 -16
- package/scripts/qe/session-start-gate.mjs +2 -16
- package/scripts/qe/ux-suite.mjs +2 -1
- package/scripts/qualified-candidate-check.mjs +2 -1
- package/scripts/rebuild-gists-from-receipts.mjs +2 -1
- package/scripts/recommendation-eval.mjs +2 -1
- package/scripts/recommendation-floor.mjs +2 -1
- package/scripts/recommendation-judge-score.mjs +2 -1
- package/scripts/recommendation-real-host-score.mjs +2 -1
- package/scripts/recommendation-real-host.mjs +2 -1
- package/scripts/refresh-capability-only-store.mjs +2 -1
- package/scripts/refresh-model-catalog.mjs +2 -1
- package/scripts/rehearse-corpus-pipeline.mjs +2 -1
- package/scripts/release-authority.mjs +2 -1
- package/scripts/release-evidence-aggregate.mjs +2 -1
- package/scripts/release-payload.mjs +2 -1
- package/scripts/release-projection.mjs +2 -1
- package/scripts/release-proof.mjs +2 -1
- package/scripts/release-qualification-contract.mjs +71 -26
- package/scripts/release-qualification.mjs +8 -1
- package/scripts/release-vector.mjs +2 -1
- package/scripts/restore-local-ingests.mjs +2 -1
- package/scripts/retrieval-canary.mjs +2 -1
- package/scripts/route-cheap.mjs +2 -1
- package/scripts/route-gold-rank.mjs +2 -1
- package/scripts/route-index-memory.mjs +2 -1
- package/scripts/route-latency-warm.mjs +2 -1
- package/scripts/router-utilization.mjs +2 -1
- package/scripts/routing-flywheel.mjs +2 -1
- package/scripts/run-operational-benchmark.mjs +2 -1
- package/scripts/run-operational-benchmark.v3.mjs +2 -1
- package/scripts/rvf-index-audit.mjs +2 -16
- package/scripts/seal-gist-receipt.mjs +2 -1
- package/scripts/security-guidance-codex-compat.mjs +2 -1
- package/scripts/selfcheck.mjs +2 -1
- package/scripts/signal-watch.mjs +2 -1
- package/scripts/source-coverage.mjs +2 -1
- package/scripts/stabilization-receipt.mjs +2 -1
- package/scripts/stamp-sweep.mjs +2 -1
- package/scripts/status-honesty.mjs +2 -1
- package/scripts/sync-commands.mjs +2 -2
- package/scripts/top100-benchmark.mjs +2 -1
- package/scripts/top100-corpus.mjs +2 -1
- package/scripts/user-model-hook-install.mjs +2 -1
- package/scripts/user-model-prompt-hook.mjs +2 -1
- package/scripts/verify-bundle.mjs +3 -1
- package/scripts/verify-model-catalog.mjs +2 -1
- package/scripts/version.mjs +2 -1
- package/scripts/wired-check.mjs +11 -5
- 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;
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*"
|
|
45
|
-
|
|
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
|
-
*"
|
|
27
|
-
|
|
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 ((
|
|
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
|
|
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',
|
|
72
|
-
|
|
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', '
|
|
88
|
-
|
|
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 (
|
|
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 (
|
|
43
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
43
44
|
process.exitCode = main();
|
|
44
45
|
}
|
package/scripts/ascii-drift.mjs
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|
package/scripts/brain-score.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
264
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
254
265
|
catch { return false; }
|
|
255
266
|
})();
|
|
256
267
|
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -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 ((
|
|
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 ((
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
29
|
+
if (isDirectInvocation(import.meta.url)) await main();
|