ruvnet-brain 4.7.20 → 4.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5 -15
- package/plugin/scripts/session-start-health.mjs +97 -10
- package/plugin/scripts/session-start-issue-alert.mjs +63 -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 +116 -40
- package/scripts/job-heartbeat.sh +54 -19
- 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 +26 -20
- 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 +41 -18
- 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 +64 -28
- 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
|
@@ -14,7 +14,7 @@ import { json, exists, mtimeMs, read } from './session-start-fsutil.mjs';
|
|
|
14
14
|
import { unmountedNotice } from './brain-location.mjs';
|
|
15
15
|
import { assessMoveLeftovers } from './footprint-io.mjs';
|
|
16
16
|
import {
|
|
17
|
-
describeFailedRefreshRun, readNightlyRegistration, refreshHistory, updateOwnedByAgenticKit,
|
|
17
|
+
describeFailedRefreshRun, developerRunHealth, readNightlyRegistration, refreshHistory, updateOwnedByAgenticKit,
|
|
18
18
|
} from './nightly-scheduler.mjs';
|
|
19
19
|
|
|
20
20
|
export const brainState = (env, home) => {
|
|
@@ -93,7 +93,7 @@ export const autoUpdatePaths = (brainHome) => ({
|
|
|
93
93
|
checkFile: path.join(brainHome, 'corpus-check.json'),
|
|
94
94
|
checkResultFile: path.join(brainHome, '.last-kb-check-result.json'),
|
|
95
95
|
});
|
|
96
|
-
export const AUTO_UPDATE_LOCK_STALE_MS =
|
|
96
|
+
export const AUTO_UPDATE_LOCK_STALE_MS = 160 * 60_000; // detach TTL (session-start-update-plane ttlSec, 155 min) + slack
|
|
97
97
|
|
|
98
98
|
/** The inputs every currency decision reads — one reader, shared by the line and the auto-update. */
|
|
99
99
|
export const knowledgeFacts = ({ env = process.env, home, now = Date.now() } = {}) => {
|
|
@@ -108,7 +108,9 @@ export const knowledgeFacts = ({ env = process.env, home, now = Date.now() } = {
|
|
|
108
108
|
const auto = autoUpdatePaths(brainHome);
|
|
109
109
|
const provenWithin = (h) => Boolean((history.lastSuccess && hours(history.lastSuccess.at) <= h)
|
|
110
110
|
|| (check?.currencyVerdict === 'CURRENT' && Number.isFinite(checkMs) && hours(checkMs) <= h));
|
|
111
|
-
|
|
111
|
+
// brain-confirmation.mjs SIGNATURE_RECORD (not imported: that module imports this one).
|
|
112
|
+
const installedAtMs = Date.parse(json(path.join(brainHome, 'knowledge-signature.json'))?.verifiedAt || '');
|
|
113
|
+
return { brainHome, kbDir, source, builtMs, history, hours, provenWithin, auto, installedAtMs,
|
|
112
114
|
attempt: json(auto.attemptFile), corpusCheck: json(auto.checkFile),
|
|
113
115
|
lockMs: Date.parse(json(auto.lockFile)?.at || '') || mtimeMs(auto.lockFile) };
|
|
114
116
|
};
|
|
@@ -146,15 +148,87 @@ export const autoUpdateOptOut = ({ env = process.env, home, facts }) => {
|
|
|
146
148
|
* installed tree carries no generation stamp) and that tag is not what SOURCE.json now names. A check
|
|
147
149
|
* recorded before the update landed therefore stops reading as pending the moment B is installed.
|
|
148
150
|
*/
|
|
151
|
+
const SEMVER_TAG = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
152
|
+
/** Semver order of two release tags (>0 a newer, <0 older, 0 same); null when either is not semver. */
|
|
153
|
+
export const compareReleaseTags = (a, b) => {
|
|
154
|
+
const A = SEMVER_TAG.exec(String(a ?? '')), B = SEMVER_TAG.exec(String(b ?? ''));
|
|
155
|
+
if (!A || !B) return null;
|
|
156
|
+
for (let i = 1; i <= 3; i++) if (Number(A[i]) !== Number(B[i])) return Number(A[i]) > Number(B[i]) ? 1 : -1;
|
|
157
|
+
if ((A[4] || '') === (B[4] || '')) return 0;
|
|
158
|
+
if (!A[4]) return 1;
|
|
159
|
+
if (!B[4]) return -1;
|
|
160
|
+
// Semver §11.4: dot-separated identifiers left to right; numeric ones numerically (alpha.10 > alpha.9),
|
|
161
|
+
// numeric below alphanumeric, alphanumeric in ASCII order; a longer set wins when all before it are equal.
|
|
162
|
+
const X = A[4].split('.'), Y = B[4].split('.');
|
|
163
|
+
for (let i = 0; i < Math.max(X.length, Y.length); i++) {
|
|
164
|
+
if (X[i] === undefined) return -1;
|
|
165
|
+
if (Y[i] === undefined) return 1;
|
|
166
|
+
const xn = /^\d+$/.test(X[i]), yn = /^\d+$/.test(Y[i]);
|
|
167
|
+
if (xn && yn) { if (Number(X[i]) !== Number(Y[i])) return Number(X[i]) > Number(Y[i]) ? 1 : -1; continue; }
|
|
168
|
+
if (xn !== yn) return xn ? -1 : 1;
|
|
169
|
+
if (X[i] !== Y[i]) return X[i] > Y[i] ? 1 : -1;
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
172
|
+
};
|
|
173
|
+
|
|
149
174
|
export const newerCorpusPending = (facts) => {
|
|
150
175
|
const c = facts.corpusCheck;
|
|
151
176
|
if (!c?.candidateTag || !['UPDATE_AVAILABLE', 'UNKNOWN'].includes(c.verdict)) return null;
|
|
152
177
|
const installed = [facts.source?.corpusReleaseTag, facts.source?.releaseTag].filter(Boolean);
|
|
153
178
|
if (installed.includes(c.candidateTag)) return null;
|
|
179
|
+
// #401: a different tag is not a newer one. When both are semver release tags, order decides: a
|
|
180
|
+
// retained check naming v4.5.7 must never call an installed v4.5.9 out of date, and a v4.5.10
|
|
181
|
+
// candidate still reads as pending however recently something was installed.
|
|
182
|
+
const order = compareReleaseTags(c.candidateTag, facts.source?.releaseTag);
|
|
183
|
+
if (order !== null) { if (order <= 0) return null; }
|
|
184
|
+
else {
|
|
185
|
+
// Opaque (non-orderable) identities: a check recorded before the installed bytes were
|
|
186
|
+
// signature-verified is stale evidence the install superseded, not proof of a newer corpus.
|
|
187
|
+
const checkedMs = Date.parse(c.checkedAt || c.launchedAt || '');
|
|
188
|
+
if (Number.isFinite(facts.installedAtMs) && Number.isFinite(checkedMs) && checkedMs < facts.installedAtMs) return null;
|
|
189
|
+
}
|
|
154
190
|
return { tag: c.candidateTag, installed: installed[0] || null, checkedAt: c.checkedAt || c.launchedAt || null, outcome: c.outcome };
|
|
155
191
|
};
|
|
156
192
|
const shortTag = (tag) => (tag && tag.length > 28 ? `${tag.slice(0, 26)}…` : tag || 'unknown');
|
|
157
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Issue #391 / closure gap G-015: a FAILED run whose `update` phase PASSED left the knowledge current
|
|
196
|
+
* (the ledger stops at the first required failure, so every corpus phase before it passed too). When
|
|
197
|
+
* host-convergence is the ONLY failed phase, the host plugin sync is what did not finish: a host
|
|
198
|
+
* problem, not a knowledge failure, and it is reported as one. Any other failed phase (cleanup-pending,
|
|
199
|
+
* retention recovery-required) keeps its own reason through describeFailedRefreshRun.
|
|
200
|
+
* Returns { state, why } from the host-convergence evidence, or null for any other receipt.
|
|
201
|
+
*/
|
|
202
|
+
export const hostSyncOnlyFailure = (receipt) => {
|
|
203
|
+
if (receipt?.status !== 'FAILED' || !Array.isArray(receipt.phases)) return null;
|
|
204
|
+
if (!receipt.phases.some((entry) => entry?.phase === 'update' && entry.status === 'PASS')) return null;
|
|
205
|
+
const failed = receipt.phases.filter((entry) => entry?.status === 'FAIL');
|
|
206
|
+
if (failed.length !== 1 || failed[0].phase !== 'host-convergence') return null;
|
|
207
|
+
const host = failed[0];
|
|
208
|
+
const state = host?.evidence?.state || null;
|
|
209
|
+
const why = String(host?.evidence?.error || state || '').split('\n')[0].trim().slice(0, 200);
|
|
210
|
+
return { state, why: why || 'no reason recorded (run npx ruvnet-brain --doctor)' };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A coordinated (developer-suite) nightly reports in nightly-suite-update.json, never in refresh-runs, so a failing or stale
|
|
215
|
+
* suite stayed silent while the knowledge was current (AuditC #26). One line then; '' when it is healthy, never ran, or absent.
|
|
216
|
+
*/
|
|
217
|
+
const coordinatedSuiteLine = ({ brainHome, now, builtMs, age }) => {
|
|
218
|
+
let health;
|
|
219
|
+
try {
|
|
220
|
+
const registration = readNightlyRegistration({ brainHome });
|
|
221
|
+
if (!registration.ok || registration.record.mode !== 'developer-suite') return '';
|
|
222
|
+
health = developerRunHealth({ brainHome, registration: registration.record, now });
|
|
223
|
+
} catch { return ''; }
|
|
224
|
+
if (!['failed', 'stale'].includes(health.state)) return '';
|
|
225
|
+
const what = health.state === 'stale' ? `has not completed in ${Math.round(health.ageHours)}h`
|
|
226
|
+
: `FAILED${Number.isFinite(health.ageHours) ? ` ${age(now - health.ageHours * 3_600_000)}` : ''}: ${String(health.evidence || 'no reason recorded').split('\n')[0].slice(0, 200)}`;
|
|
227
|
+
return `${KNOWLEDGE_LINE_PREFIX}CURRENT, NIGHTLY SUITE ${health.state === 'stale' ? 'STALE' : 'FAILING'}] knowledge base built `
|
|
228
|
+
+ `${Number.isFinite(builtMs) ? age(builtMs) : 'at an UNKNOWN time'} and current; the coordinated nightly update ${what}. `
|
|
229
|
+
+ `Details: ${path.join(brainHome, 'scheduler', 'last-suite-attempt.json')} (verify: npx ruvnet-brain --doctor).`;
|
|
230
|
+
};
|
|
231
|
+
|
|
158
232
|
export const knowledgeCurrency = ({ env = process.env, home, now = Date.now(), windowHours = 48 } = {}) => {
|
|
159
233
|
const facts = knowledgeFacts({ env, home, now });
|
|
160
234
|
const { brainHome, builtMs, history, hours, attempt } = facts;
|
|
@@ -169,7 +243,8 @@ export const knowledgeCurrency = ({ env = process.env, home, now = Date.now(), w
|
|
|
169
243
|
const autoAbandoned = attempt?.outcome === 'launched' && !autoRunning;
|
|
170
244
|
const autoFailed = Number.isFinite(launchedMs) && (attempt.outcome === 'failed' || autoAbandoned)
|
|
171
245
|
&& !(history.latest && history.latest.at >= launchedMs);
|
|
172
|
-
const
|
|
246
|
+
const hostOnly = hostSyncOnlyFailure(latest);
|
|
247
|
+
const failing = (latest?.status === 'FAILED' && !hostOnly) || autoFailed;
|
|
173
248
|
const ageKnown = Number.isFinite(builtMs);
|
|
174
249
|
const pending = newerCorpusPending(facts);
|
|
175
250
|
if (!failing && pending && (proven || (ageKnown && hours(builtMs) <= windowHours))) {
|
|
@@ -181,8 +256,18 @@ export const knowledgeCurrency = ({ env = process.env, home, now = Date.now(), w
|
|
|
181
256
|
return `${KNOWLEDGE_LINE_PREFIX}UPDATE PENDING] a newer corpus ${shortTag(pending.tag)} is published `
|
|
182
257
|
+ `(this machine has ${shortTag(pending.installed)}, built ${ageKnown ? age(builtMs) : 'at an UNKNOWN time'}); ${what}.`;
|
|
183
258
|
}
|
|
184
|
-
|
|
185
|
-
if (!failing &&
|
|
259
|
+
const fresh = proven || (ageKnown && hours(builtMs) <= windowHours);
|
|
260
|
+
if (!failing && hostOnly && fresh) {
|
|
261
|
+
const restart = hostOnly.state === 'host-restart-required';
|
|
262
|
+
return `${KNOWLEDGE_LINE_PREFIX}CURRENT, HOST ${restart ? 'RESTART' : 'SYNC'} PENDING] `
|
|
263
|
+
+ `knowledge base built ${ageKnown ? `${day(builtMs)} (${age(builtMs)})` : 'at an UNKNOWN time'}; `
|
|
264
|
+
+ `the last refresh (${latest.action || 'unknown'}) ${age(history.latest.at)} passed its knowledge update phase, `
|
|
265
|
+
+ `only the host plugin sync after it did not finish: ${hostOnly.why}. `
|
|
266
|
+
// A restart writes no refresh receipt, so this line stays until the next refresh records the sync.
|
|
267
|
+
+ (restart ? 'Fix: restart Claude Code or Codex (`claude --continue` keeps a Claude conversation), then record it: npx ruvnet-brain@latest --update.'
|
|
268
|
+
: 'Fix: npx ruvnet-brain@latest --update (verify: npx ruvnet-brain --doctor).');
|
|
269
|
+
}
|
|
270
|
+
if (!failing && fresh) return coordinatedSuiteLine({ brainHome, now, builtMs, age });
|
|
186
271
|
const kit = agenticKitUpdates({ home, facts });
|
|
187
272
|
const agentKit = kit === 'delivering';
|
|
188
273
|
// An agentic-kit machine must never be told to also --enable-nightly (one owner per machine).
|
|
@@ -190,17 +275,19 @@ export const knowledgeCurrency = ({ env = process.env, home, now = Date.now(), w
|
|
|
190
275
|
const parts = [ageKnown ? `knowledge base built ${day(builtMs)} (${age(builtMs)})`
|
|
191
276
|
: 'knowledge base age UNKNOWN (SOURCE.json missing or unreadable)'];
|
|
192
277
|
if (autoFailed) {
|
|
193
|
-
const why = autoAbandoned ? 'it never recorded an outcome (killed at its
|
|
278
|
+
const why = autoAbandoned ? 'it never recorded an outcome (killed at its time limit, or the machine slept)'
|
|
194
279
|
: `exit ${attempt.code ?? 'unknown'}: ${attempt.reason || 'no reason recorded'}`;
|
|
195
280
|
parts.push(`automatic update launched ${age(launchedMs)} FAILED — ${why}`);
|
|
281
|
+
} else if (hostOnly) {
|
|
282
|
+
parts.push(`last refresh (${latest.action || 'unknown'}) passed its knowledge update phase ${age(history.latest.at)} but its host sync did not finish: ${hostOnly.why}`);
|
|
196
283
|
} else if (latest?.status === 'FAILED') {
|
|
197
284
|
const why = describeFailedRefreshRun(latest) || 'failed';
|
|
198
285
|
parts.push(`last refresh (${latest.action || 'unknown'}) FAILED ${age(history.latest.at)}: ${why}`);
|
|
199
286
|
}
|
|
200
287
|
if (autoRunning) parts.push(`an automatic update is running now (started ${age(facts.lockMs)})`);
|
|
201
|
-
parts.push(history.receipts
|
|
202
|
-
? `${history.failuresSinceSuccess} failed run(s) since the last success (${
|
|
203
|
-
|
|
288
|
+
parts.push(!history.receipts ? 'no refresh has ever run on this machine'
|
|
289
|
+
: history.lastSuccess ? `${history.failuresSinceSuccess} failed run(s) since the last success (${day(history.lastSuccess.at)})`
|
|
290
|
+
: `${history.failuresSinceSuccess} failed run(s) and no successful run on record`);
|
|
204
291
|
if (kit === 'not-delivering') {
|
|
205
292
|
parts.push(`agentic-kit claims updates (kit.json ruvnetBrain:true) but no update is proven in ${AGENTIC_KIT_PROOF_HOURS}h, so the Brain's own self-heal runs instead`);
|
|
206
293
|
}
|
|
@@ -84,6 +84,7 @@ export function surfaceIssuePointer({ stateDir, emit, now, env, home, platform,
|
|
|
84
84
|
export function surfaceNtfyAlerts({ stateDir, emit, env, home, platform, cwd, now = Date.now() }) {
|
|
85
85
|
try {
|
|
86
86
|
if (!maintainerIssueEntitlement(env, home, 'stuinfla/ruvnet-brain', platform) || !isMaintainerRepo(cwd)) return;
|
|
87
|
+
surfaceJobHealth({ stateDir, emit, now, cwd });
|
|
87
88
|
// The reader writes a few-byte summary (scripts/ntfy-inbox.mjs writeSummary) so a large alert list can never
|
|
88
89
|
// push this banner past its read limit; the detailed list is only a fallback for an older reader.
|
|
89
90
|
let total = 0; let kinds = {};
|
|
@@ -117,3 +118,65 @@ export function surfaceNtfyAlerts({ stateDir, emit, env, home, platform, cwd, no
|
|
|
117
118
|
emit(`[RuvNet Brain — NTFY: ${total} open alert(s) (${summaryText || 'none listed'})${over}${age}. Details: node scripts/ntfy-inbox.mjs --open]`);
|
|
118
119
|
} catch { /* advisory only */ }
|
|
119
120
|
}
|
|
121
|
+
|
|
122
|
+
const WATCHDOG_LABEL = 'com.ruvnet.nightly-watchdog';
|
|
123
|
+
const WATCHDOG_MAX_AGE_H = 30;
|
|
124
|
+
const MAX_RECEIPTS = 64;
|
|
125
|
+
const MAX_RECEIPT_BYTES = 64 * 1024;
|
|
126
|
+
|
|
127
|
+
/** The job labels config/scheduled-jobs.json registers, found by walking up from the session's project directory (the
|
|
128
|
+
* maintainer gate already requires this checkout to be the repo), or null when there is no readable registry. */
|
|
129
|
+
export function registeredJobLabels(cwd) {
|
|
130
|
+
if (!cwd) return null;
|
|
131
|
+
let dir = path.resolve(cwd);
|
|
132
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
133
|
+
const file = path.join(dir, 'config', 'scheduled-jobs.json');
|
|
134
|
+
try {
|
|
135
|
+
if (fs.statSync(file).size <= MAX_STATUS_BYTES) {
|
|
136
|
+
const jobs = json(file)?.jobs;
|
|
137
|
+
if (Array.isArray(jobs)) return new Set(jobs.map((j) => j?.label).filter((l) => typeof l === 'string'));
|
|
138
|
+
}
|
|
139
|
+
} catch { /* not here: look one level up */ }
|
|
140
|
+
const up = path.dirname(dir);
|
|
141
|
+
if (up === dir) break;
|
|
142
|
+
dir = up;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* WHO WATCHES THE WATCHDOG (4.7.21). nightly-watchdog.mjs cannot judge its own run, and a watchdog that stopped
|
|
149
|
+
* firing would be silent by construction. This reads the local heartbeat receipts (scripts/job-heartbeat.sh) —
|
|
150
|
+
* no network, bounded file count and size — and says ONE line when any receipt is failed/killed or the
|
|
151
|
+
* watchdog's own receipt is older than 30 h. Machines without heartbeats (every end user) get nothing.
|
|
152
|
+
* Only receipts of jobs the registry still lists count (review fix): a retired job's last failed receipt left in the
|
|
153
|
+
* heartbeat directory must not read as "failing" forever. Without a readable registry every receipt counts.
|
|
154
|
+
*/
|
|
155
|
+
export function surfaceJobHealth({ stateDir, emit, now = Date.now(), cwd }) {
|
|
156
|
+
try {
|
|
157
|
+
const dir = path.join(stateDir, 'heartbeats');
|
|
158
|
+
let names;
|
|
159
|
+
try { names = fs.readdirSync(dir).filter((n) => n.endsWith('.json')).slice(0, MAX_RECEIPTS); } catch { return; }
|
|
160
|
+
if (!names.length) return;
|
|
161
|
+
const registered = registeredJobLabels(cwd);
|
|
162
|
+
const red = [];
|
|
163
|
+
let watchdogAt = null;
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
const file = path.join(dir, name);
|
|
166
|
+
try { if (fs.statSync(file).size > MAX_RECEIPT_BYTES) continue; } catch { continue; }
|
|
167
|
+
const r = json(file);
|
|
168
|
+
if (!r || typeof r !== 'object') continue;
|
|
169
|
+
const label = name.slice(0, -'.json'.length);
|
|
170
|
+
if (registered && !registered.has(label)) continue;
|
|
171
|
+
if (r.state === 'failed' || r.state === 'killed') red.push(label);
|
|
172
|
+
if (label === WATCHDOG_LABEL) watchdogAt = Date.parse(r.ended_at || r.started_at || '');
|
|
173
|
+
}
|
|
174
|
+
const parts = [];
|
|
175
|
+
if (red.length) parts.push(`${red.length} scheduled job(s) failing (${red.slice(0, 3).join(', ')}${red.length > 3 ? ', …' : ''})`);
|
|
176
|
+
if (watchdogAt !== null) {
|
|
177
|
+
const ageH = Number.isFinite(watchdogAt) ? (now - watchdogAt) / 3_600_000 : Infinity;
|
|
178
|
+
if (ageH > WATCHDOG_MAX_AGE_H) parts.push(`the nightly watchdog last ran ${Number.isFinite(ageH) ? `${Math.floor(ageH)}h ago` : 'at an unreadable time'}`);
|
|
179
|
+
}
|
|
180
|
+
if (parts.length) emit(`[RuvNet Brain — JOBS: ${parts.join('; ')}. Details: node scripts/nightly-watchdog.mjs --no-push]`);
|
|
181
|
+
} catch { /* advisory only */ }
|
|
182
|
+
}
|
|
@@ -11,6 +11,8 @@ import { read, write, exists, json, firstVersion, dispatchDetached, mtimeMs } fr
|
|
|
11
11
|
import { KNOWLEDGE_LINE_PREFIX, AUTO_UPDATE_LOCK_STALE_MS, autoUpdateOptOut, knowledgeFacts } from './session-start-health.mjs';
|
|
12
12
|
import { inventoryFootprint } from './brain-footprint.mjs';
|
|
13
13
|
import { confirm, footprintAlarm } from './brain-confirmation.mjs';
|
|
14
|
+
import { claimCodexOvernightNotice } from './codex-overnight-notice.mjs';
|
|
15
|
+
import { developerCoordinatorOwner } from './developer-update-owner.mjs';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* KNOWLEDGE SELF-HEAL (owner invariant 2026-09-30: no brain may ever be more than 48h old) AND
|
|
@@ -31,7 +33,9 @@ import { confirm, footprintAlarm } from './brain-confirmation.mjs';
|
|
|
31
33
|
* (plugin/mcp/server.mjs, announce:false so it never consumes the once-per-session line). The outcome is
|
|
32
34
|
* recorded by the worker and spoken by the NEXT session's knowledge line.
|
|
33
35
|
*/
|
|
34
|
-
|
|
36
|
+
// ttlSec sits above host-update.mjs's knowledge limit (150 min), which sits above the coordinator's installer limit
|
|
37
|
+
// (135 min = the 90-minute bundle download deadline plus the apply): each kill lands on a level that records an outcome.
|
|
38
|
+
export const AUTO_UPDATE_POLICY = Object.freeze({ staleHours: 24, retryHours: 6, offlineRetryMinutes: 30, ttlSec: 9300, checkMinutes: 60 });
|
|
35
39
|
export const corpusCheckMinutes = (env = {}) => {
|
|
36
40
|
const raw = env.RUVNET_CORPUS_CHECK_MINUTES;
|
|
37
41
|
const n = Number(raw);
|
|
@@ -159,6 +163,8 @@ export const compareVersions = (a, b) => {
|
|
|
159
163
|
};
|
|
160
164
|
|
|
161
165
|
export const stableSpine = ({ env, hookDir, stateDir, home, pluginVersion, emit, now }) => {
|
|
166
|
+
const overnight = claimCodexOvernightNotice(stateDir); // the unattended nightly's deferred Codex reload, said once
|
|
167
|
+
if (overnight) emit(overnight);
|
|
162
168
|
const activeFile = path.join(stateDir, 'active.json');
|
|
163
169
|
const stamp = path.join(stateDir, '.last-update-check');
|
|
164
170
|
let seedDispatched = false;
|
|
@@ -240,6 +246,14 @@ export const heartbeat = ({ env, hookDir, stateDir, home, running, seedDispatche
|
|
|
240
246
|
for (let i = 0; i < 3 && active; i += 1) { if ((r[i] || 0) !== (a[i] || 0)) { newer = (r[i] || 0) > (a[i] || 0); break; } }
|
|
241
247
|
if (active && !newer) return;
|
|
242
248
|
} catch { /* unknown active generation: keep the existing announcement */ }
|
|
249
|
+
let coordinated = false;
|
|
250
|
+
try { coordinated = developerCoordinatorOwner({ home, brainHome: env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain') }).active; } catch { /* legacy path */ }
|
|
251
|
+
if (coordinated) {
|
|
252
|
+
// Coordinated machines install new code only in the scheduled nightly; saying "installing now" would be false.
|
|
253
|
+
emit(`[RuvNet Brain — v${remoteVersion} is published; this machine's coordinated nightly update installs it at its next scheduled run]`);
|
|
254
|
+
emit('');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
243
257
|
if (pref === 'yes' && exists(path.join(hookDir, 'host-update.mjs'))) {
|
|
244
258
|
dispatchDetached(hookDir, 600, path.join(stateDir, '.last-auto-update.log'),
|
|
245
259
|
process.execPath, [path.join(hookDir, 'host-update.mjs')], env);
|
|
@@ -60,6 +60,7 @@ import os from 'node:os';
|
|
|
60
60
|
import { spawnSync } from 'node:child_process';
|
|
61
61
|
import { fileURLToPath } from 'node:url';
|
|
62
62
|
import { parseHookEvent, toolName, commandOf, findInvocations, rawToolResponse, readStdinBounded } from './hook-input.mjs';
|
|
63
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
63
64
|
|
|
64
65
|
const MANAGED_TOOLS = ['gh', 'vercel', 'netlify', 'npm', 'git'];
|
|
65
66
|
|
|
@@ -183,7 +184,7 @@ async function main() {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
function isMain() {
|
|
186
|
-
try { return
|
|
187
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
187
188
|
catch { return false; }
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import fs from 'node:fs';
|
|
49
49
|
import { fileURLToPath } from 'node:url';
|
|
50
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
50
51
|
|
|
51
52
|
/** Env vars that bill per token. OPENROUTER is deliberately absent — see below. */
|
|
52
53
|
export const METERED_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];
|
|
@@ -113,7 +114,7 @@ export function check(command, env = process.env) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
const isMain = (() => {
|
|
116
|
-
try { return
|
|
117
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
117
118
|
catch { return false; }
|
|
118
119
|
})();
|
|
119
120
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import fs from 'node:fs';
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
31
32
|
|
|
32
33
|
export const RUV_TOOL = 'ruflo memory store|retrieve|search (or `ruflo hooks session-end|post-task`)';
|
|
33
34
|
/** Repo-relative scope: our own executable code. Tests and fixtures are never in scope. */
|
|
@@ -197,7 +198,7 @@ function repoRootOf(file) {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
function isMain() {
|
|
200
|
-
try { return
|
|
201
|
+
try { return isDirectInvocation(import.meta.url); } catch { return false; }
|
|
201
202
|
}
|
|
202
203
|
if (isMain()) {
|
|
203
204
|
let code = 0;
|
|
@@ -42,7 +42,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
42
42
|
import { nativeTurnLines } from './native-turn-transcript.mjs';
|
|
43
43
|
import { resolveRuflo, rufloInvocation, rufloCwdFor, rufloScratchRoot } from './ruflo-bin.mjs';
|
|
44
44
|
import { redactText } from './continuity-events.mjs';
|
|
45
|
-
import { resolveProjectStore, globalEnrollmentAllowed } from './project-store-resolver.mjs';
|
|
45
|
+
import { resolveProjectStore, globalEnrollmentAllowed, storeEncryptedAtRest, ENCRYPTED_STORE_REASON } from './project-store-resolver.mjs';
|
|
46
46
|
import { recordHookFailure } from './hook-failures.mjs';
|
|
47
47
|
|
|
48
48
|
const digest = (value) => crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
@@ -53,6 +53,7 @@ function appendReceipt(file, row) {
|
|
|
53
53
|
}
|
|
54
54
|
const flagValue = (args, flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
|
|
55
55
|
import { contentPathExcludes, pathIsExcluded, privateTurn, maskExcludedPaths, captureFailureReason, firstTurnCaptureNotice } from './turn-capture-privacy.mjs';
|
|
56
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
56
57
|
|
|
57
58
|
export const TURN_NAMESPACE = 'turns';
|
|
58
59
|
export const MIN_OUTCOME_CHARS = 200;
|
|
@@ -315,7 +316,8 @@ export function captureTurnOutcome({
|
|
|
315
316
|
if (!sessionKey) report.skipped = 'no session identity in the host payload';
|
|
316
317
|
else if (turn.finalText.length < MIN_OUTCOME_CHARS && !turn.files.length) {
|
|
317
318
|
report.skipped = turn.excludedResource ? 'turn content withheld: explicitly excluded resource' : host === 'codex' && !message ? 'codex payload carried no last_assistant_message' : 'trivial turn';
|
|
318
|
-
} else {
|
|
319
|
+
} else if (storeEncryptedAtRest(db)) report.skipped = loud(`${ENCRYPTED_STORE_REASON}; turn not written (opt out: RUVNET_TURN_CAPTURE=off)`, db); // #439: no ruflo spawn that can only be refused
|
|
320
|
+
else {
|
|
319
321
|
const fingerprint = crypto.createHash('sha256').update(JSON.stringify([turn.finalText, turn.files])).digest('hex');
|
|
320
322
|
const stateFile = path.join(brainHome, 'turn-capture', 'last-turn.json');
|
|
321
323
|
const identity = crypto.createHash('sha256').update(`${host}:${db}:${sessionKey}`).digest('hex');
|
|
@@ -402,6 +404,7 @@ export function runSteps({ steps = [], receipts, event = '' } = {}, { run: suppl
|
|
|
402
404
|
const data = { key: flagValue(queued.args, '--key'), value: flagValue(queued.args, '--value'), db: flagValue(queued.args, '--path') };
|
|
403
405
|
if (typeof data.key !== 'string' || typeof data.value !== 'string') throw new Error('invalid turn store step');
|
|
404
406
|
if (data.db !== db) throw new Error('foreign turn store rejected');
|
|
407
|
+
if (storeEncryptedAtRest(db)) throw new Error(ENCRYPTED_STORE_REASON);
|
|
405
408
|
if (queued.brainHome !== brainHome) throw new Error('turn consent settings differ from trusted caller');
|
|
406
409
|
binding = queued;
|
|
407
410
|
key = data.key; value = maskExcludedPaths(redactText(data.value), target.contentPathExcludes, target.capturePath);
|
|
@@ -471,6 +474,7 @@ export function turnRecordingStatus({ projectDir = process.cwd(), env = process.
|
|
|
471
474
|
let target;
|
|
472
475
|
try { target = resolveTurnDb({ projectDir, brainHome }); } catch { return { state: 'unknown', line: 'turn recording unavailable: canonical store resolution failed' }; }
|
|
473
476
|
if (String(env.RUVNET_TURN_CAPTURE || '').toLowerCase() === 'off' || target.skipped) return { state: 'unknown', line: `turn recording n/a — ${target.skipped || 'RUVNET_TURN_CAPTURE=off'}` };
|
|
477
|
+
if (storeEncryptedAtRest(target.db)) return { state: 'warn', line: `turn recording off for this store — ${ENCRYPTED_STORE_REASON}. Keep this project's store unencrypted, or set RUVNET_TURN_CAPTURE=off to stop trying` };
|
|
474
478
|
const notice = noticeOnFirstUse ? firstTurnCaptureNotice({ target, brainHome, policyFile: turnCapturePolicyFile(brainHome), env }) : null;
|
|
475
479
|
const status = (value) => ({ ...value, ...(notice ? { notice } : {}) });
|
|
476
480
|
let rows = [];
|
|
@@ -494,7 +498,7 @@ export function turnRecordingStatus({ projectDir = process.cwd(), env = process.
|
|
|
494
498
|
: { state: 'unknown', line: 'turn recording not yet proven — no exact readback receipt' });
|
|
495
499
|
}
|
|
496
500
|
|
|
497
|
-
if (
|
|
501
|
+
if (isDirectInvocation(import.meta.url) && process.argv[2] === '--run-steps') {
|
|
498
502
|
try { runSteps(JSON.parse(process.argv[3] || '{}')); } catch { /* a detached worker has no one to report to */ }
|
|
499
503
|
process.exit(0);
|
|
500
504
|
}
|
|
@@ -89,6 +89,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
89
89
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
90
90
|
import { readStdinBounded, isHarnessGenerated } from './hook-input.mjs';
|
|
91
91
|
import { resolveBash } from './hook-shim-bash.mjs';
|
|
92
|
+
import { ADVISORY_CONTRACT } from './advisory-contract.mjs';
|
|
92
93
|
|
|
93
94
|
// WHERE THIS FILE'S SIBLINGS LIVE. Resolved from THIS file's own location so it is correct under the
|
|
94
95
|
// Stable Spine (an immutable versions/<gen> tree) and in a dev checkout alike — hook-shim.mjs has
|
|
@@ -127,6 +128,17 @@ const MAX_BUFFER = 1 << 20;
|
|
|
127
128
|
|
|
128
129
|
const VALID_CHANNELS = new Set(['advocacy', 'promotion', 'lesson', 'alarm']);
|
|
129
130
|
const VALID_EFFECTS = new Set(['advisory', 'block']);
|
|
131
|
+
// QUIET BY DEFAULT, ENFORCED HERE (4.7.21 review F1). Unless RUVNET_STOP_NUDGES=full, this run is narrowed
|
|
132
|
+
// to lesson + alarm: lesson producers still run and deliver, advocacy/promotion producers are never spawned.
|
|
133
|
+
// The shim also exports RUVNET_UNPROMPTED_CHANNELS, but the shim is boot-frozen and this body is hot-updated,
|
|
134
|
+
// so the quiet promise must not depend on which shim generation happens to dispatch it. An explicit
|
|
135
|
+
// RUVNET_UNPROMPTED_CHANNELS still wins (the shim's own narrowing, or a caller that names channels).
|
|
136
|
+
const QUIET_DEFAULT_CHANNELS = 'lesson,alarm';
|
|
137
|
+
const NUDGES_FULL = String(process.env.RUVNET_STOP_NUDGES || '').toLowerCase() === 'full';
|
|
138
|
+
const CHANNEL_LIST = process.env.RUVNET_UNPROMPTED_CHANNELS || (NUDGES_FULL ? '' : QUIET_DEFAULT_CHANNELS);
|
|
139
|
+
const ALLOWED_CHANNELS = CHANNEL_LIST
|
|
140
|
+
? new Set(CHANNEL_LIST.split(',').map((c) => c.trim()).filter((c) => VALID_CHANNELS.has(c)))
|
|
141
|
+
: VALID_CHANNELS;
|
|
130
142
|
|
|
131
143
|
/** Exit 0 with byte-empty stdout — the fail-safe and the "nothing to say" path. */
|
|
132
144
|
function silent() { process.exit(0); }
|
|
@@ -242,7 +254,7 @@ if (!event) silent();
|
|
|
242
254
|
if (typeof promptText === 'string' && isHarnessGenerated(promptText)) silent();
|
|
243
255
|
}
|
|
244
256
|
|
|
245
|
-
const producers = resolveProducers(EVENT);
|
|
257
|
+
const producers = resolveProducers(EVENT).filter((p) => p.channels.some((c) => ALLOWED_CHANNELS.has(c)));
|
|
246
258
|
if (!producers.length) silent(); // unknown event, or nothing wired for it — never speak on a guess
|
|
247
259
|
|
|
248
260
|
// ── Spawn producers as CAPTURED children, collect candidate lines ──────────────────────────────────
|
|
@@ -299,6 +311,7 @@ for (const { s, channels } of rawLines) {
|
|
|
299
311
|
try { c = JSON.parse(s); } catch { continue; } // not JSON → drop (a rogue's raw line)
|
|
300
312
|
if (!c || typeof c !== 'object' || Array.isArray(c)) continue;
|
|
301
313
|
if (!VALID_CHANNELS.has(c.channel)) continue; // unknown/absent channel → drop
|
|
314
|
+
if (!ALLOWED_CHANNELS.has(c.channel)) continue; // narrowed by the shim's quiet default → drop
|
|
302
315
|
if (!channels.includes(c.channel)) continue; // ANTI-SPOOF (GPT-5.6-Sol): this producer is
|
|
303
316
|
// not authorised for this channel → drop
|
|
304
317
|
if (!VALID_EFFECTS.has(c.effect)) continue; // unknown/absent effect → drop
|
|
@@ -471,9 +484,13 @@ const hookEventName = EVENT ? EVENT.split('-')[0] : 'UserPromptSubmit';
|
|
|
471
484
|
|
|
472
485
|
// SYNCHRONOUS write — the truncation fix on the byte-owner itself (see the block path above).
|
|
473
486
|
if (!advisories.length) silent();
|
|
474
|
-
|
|
487
|
+
// #489: optional recommendations carry the advisory contract into the model-visible context, once,
|
|
488
|
+
// so a suggestion is never promoted into a question/approval dialog. Alarms and lessons are not offers.
|
|
489
|
+
const delivered = advisories.some((a) => a.channel === 'advocacy')
|
|
490
|
+
? [...advisories, { copy: ADVISORY_CONTRACT, channel: 'advisory-contract' }] : advisories;
|
|
491
|
+
const additionalContext = delivered.map((a) => a.copy).join('\n\n');
|
|
475
492
|
try { fs.writeSync(1, process.env.RUVNET_HOOK_CONTEXT_BUDGET === '1'
|
|
476
|
-
? emitOwnedContext(contextFrame('unprompted-speech', hookEventName,
|
|
493
|
+
? emitOwnedContext(contextFrame('unprompted-speech', hookEventName, delivered.map((a) => contextBlock(a.copy, { id: a.channel, critical: a.channel === 'alarm' }))))
|
|
477
494
|
: JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } })); }
|
|
478
495
|
catch { /* if even the final write fails, silence is the fail-safe */ }
|
|
479
496
|
process.exit(0);
|