ruvnet-brain 4.3.9 → 4.3.11
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 +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +494 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/release-proof/SKILL.md +6 -2
- package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-package-observation.mjs +67 -0
- package/scripts/nightly-two-run-proof.mjs +489 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +61 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +64 -8
- package/scripts/publication-receipt.mjs +156 -50
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
package/scripts/doc-currency.mjs
CHANGED
|
@@ -175,7 +175,8 @@ const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
|
175
175
|
|
|
176
176
|
// ── the governed set ────────────────────────────────────────────────────────────────────────────
|
|
177
177
|
|
|
178
|
-
// Resolve
|
|
178
|
+
// Resolve tracked declarations against actual working bytes. HEAD establishes membership,
|
|
179
|
+
// not the content currently being verified. Globs expand through the index; a
|
|
179
180
|
// literal entry is looked up with `git cat-file --batch-check`, which distinguishes blob / tree /
|
|
180
181
|
// missing in ONE process and — unlike `git rev-parse HEAD:<p>` — never prints the requested path
|
|
181
182
|
// back as if it were an answer.
|
|
@@ -220,8 +221,30 @@ export function resolveGoverned(root, entries) {
|
|
|
220
221
|
continue;
|
|
221
222
|
}
|
|
222
223
|
const hit = byPath.get(e.path) || { sha: null, type: onDisk ? 'untracked' : 'missing' };
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
let type = hit.type === 'missing' && onDisk ? 'untracked' : hit.type;
|
|
225
|
+
let sha = hit.sha;
|
|
226
|
+
if (type === 'blob') {
|
|
227
|
+
try {
|
|
228
|
+
const relative = path.relative(path.resolve(root), path.resolve(abs));
|
|
229
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
230
|
+
throw new Error('governed path escapes project');
|
|
231
|
+
}
|
|
232
|
+
let current = path.resolve(root);
|
|
233
|
+
for (const part of relative.split(path.sep)) {
|
|
234
|
+
current = path.join(current, part);
|
|
235
|
+
if (fs.lstatSync(current).isSymbolicLink()) throw new Error('governed path contains a symlink');
|
|
236
|
+
}
|
|
237
|
+
if (!fs.lstatSync(abs).isFile()) throw new Error('governed blob is not a regular file');
|
|
238
|
+
const bytes = fs.readFileSync(abs);
|
|
239
|
+
// Git blob identity without writing objects or invoking one process per file.
|
|
240
|
+
// Unchanged bytes retain the existing recipe; staged and unstaged edits both expire it.
|
|
241
|
+
sha = crypto.createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
|
|
242
|
+
} catch {
|
|
243
|
+
type = onDisk ? 'unsafe-working-source' : 'missing';
|
|
244
|
+
sha = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
results.push({ ...e, type, sha, onDisk, resolved: type === 'blob' });
|
|
225
248
|
}
|
|
226
249
|
return results;
|
|
227
250
|
}
|
|
@@ -290,7 +313,7 @@ export function deriveImpl(root, governed, { checkWiring = true } = {}) {
|
|
|
290
313
|
export const DRIFT_COMMITS_STALE = 2;
|
|
291
314
|
export const DRIFT_DAYS_STALE = 7;
|
|
292
315
|
|
|
293
|
-
export function deriveDrift(root, docRel, governed) {
|
|
316
|
+
export function deriveDrift(root, docRel, governed, { floorSha = null } = {}) {
|
|
294
317
|
const paths = governed.filter((g) => g.resolved).map((g) => g.path);
|
|
295
318
|
if (!paths.length) return { state: 'not-applicable', commits: 0, days: 0, reason: 'no resolvable governed paths' };
|
|
296
319
|
const doc = lastCommit(root, docRel);
|
|
@@ -310,8 +333,16 @@ export function deriveDrift(root, docRel, governed) {
|
|
|
310
333
|
// paths is version-identifier lines cannot have changed any decision, so it does not count. Any
|
|
311
334
|
// commit touching one substantive line still counts in full — this narrows the trigger, never the
|
|
312
335
|
// verdict.
|
|
336
|
+
const floor = floorSha && git(root, ['rev-parse', '--verify', `${floorSha}^{commit}`]);
|
|
337
|
+
const resolvedFloor = floor?.ok ? floor.out : null;
|
|
313
338
|
const shas = (() => {
|
|
314
|
-
|
|
339
|
+
// With --changed, only movement introduced after the candidate merge-base can consume the
|
|
340
|
+
// candidate's drift budget. Historical debt remains visible in an unscoped report, but cannot
|
|
341
|
+
// turn one new governed edit into a threshold breach merely because the ADR was already stale.
|
|
342
|
+
const range = resolvedFloor
|
|
343
|
+
? ['HEAD', `^${doc.sha}`, `^${resolvedFloor}`]
|
|
344
|
+
: [`${doc.sha}..HEAD`];
|
|
345
|
+
const r = git(root, ['rev-list', ...range, '--', ...paths]);
|
|
315
346
|
return r.ok && r.out ? r.out.split('\n').filter(Boolean) : [];
|
|
316
347
|
})();
|
|
317
348
|
const VERSION_FIELD = /^[+-]\s*"?(version|releaseTag|brainVersion|softwareVersion|tag)"?\s*[:=]/i;
|
|
@@ -327,12 +358,20 @@ export function deriveDrift(root, docRel, governed) {
|
|
|
327
358
|
const substantive = shas.filter((s) => !isVersionOnly(s));
|
|
328
359
|
const commits = substantive.length;
|
|
329
360
|
|
|
330
|
-
const codeR =
|
|
331
|
-
|
|
361
|
+
const codeR = substantive.length
|
|
362
|
+
? git(root, ['show', '-s', '--format=%ad', '--date=short', substantive[0]])
|
|
363
|
+
: null;
|
|
364
|
+
const codeDate = codeR?.ok && codeR.out ? codeR.out.split('\n')[0] : null;
|
|
365
|
+
|
|
366
|
+
let anchorDate = doc.date;
|
|
367
|
+
if (resolvedFloor && git(root, ['merge-base', '--is-ancestor', doc.sha, resolvedFloor]).ok) {
|
|
368
|
+
const floorR = git(root, ['show', '-s', '--format=%ad', '--date=short', resolvedFloor]);
|
|
369
|
+
if (floorR.ok && floorR.out) anchorDate = floorR.out.split('\n')[0];
|
|
370
|
+
}
|
|
332
371
|
|
|
333
372
|
let days = 0;
|
|
334
373
|
if (codeDate) {
|
|
335
|
-
const d0 = Date.parse(`${
|
|
374
|
+
const d0 = Date.parse(`${anchorDate}T00:00:00Z`);
|
|
336
375
|
const d1 = Date.parse(`${codeDate}T00:00:00Z`);
|
|
337
376
|
if (Number.isFinite(d0) && Number.isFinite(d1)) days = Math.max(0, Math.round((d1 - d0) / 86400000));
|
|
338
377
|
}
|
|
@@ -412,7 +451,7 @@ export function evaluateDoc(root, rel, opts = {}) {
|
|
|
412
451
|
const fm = parseFrontmatter(text);
|
|
413
452
|
const k = fm.keys;
|
|
414
453
|
|
|
415
|
-
const CONVENTION_KEYS = ['status', 'date', 'updated', 'impl', 'governs', 'verified', 'verified_digest'];
|
|
454
|
+
const CONVENTION_KEYS = ['status', 'date', 'updated', 'impl', 'governs', 'verified', 'verified_digest', 'reviewed_digest'];
|
|
416
455
|
const declared = CONVENTION_KEYS.filter((key) => k[key] !== undefined);
|
|
417
456
|
// A document predating the convention carries NONE of its keys. That is a derived property of the
|
|
418
457
|
// file, not a hardcoded list of filenames — a new doc written to the convention can never be
|
|
@@ -586,7 +625,28 @@ export function evaluateDoc(root, rel, opts = {}) {
|
|
|
586
625
|
}
|
|
587
626
|
|
|
588
627
|
// ── drift ─────────────────────────────────────────────────────────────────────────────────────
|
|
589
|
-
|
|
628
|
+
// A review records examination and findings, NOT normative agreement, acceptance, or verified
|
|
629
|
+
// implementation. Use the existing digest: frontmatter and the Currency log already exclude
|
|
630
|
+
// their own metadata, while every governed byte and normative claim remains covered unchanged.
|
|
631
|
+
// Require an explicit review row naming this digest and a governed-source referent. This checks
|
|
632
|
+
// evidence structure, never sincerity; changing a date or merely dirtying a doc proves no review.
|
|
633
|
+
const log = parseCurrencyLog(root, text);
|
|
634
|
+
const reviewedDigest = k.reviewed_digest || null;
|
|
635
|
+
const reviewMatch = Boolean(reviewedDigest && digest.digest && reviewedDigest === digest.digest);
|
|
636
|
+
const reviewRow = reviewedDigest && log.rows.find((row) => /^\d{4}-\d{2}-\d{2}$/.test(row.date)
|
|
637
|
+
&& /\breview(?:ed)?\b/i.test(row.what)
|
|
638
|
+
&& `${row.what} ${row.why}`.split(/[^a-f0-9]+/i).includes(reviewedDigest)
|
|
639
|
+
&& row.referents.some((ref) => ref.kind === 'path'
|
|
640
|
+
&& governed.some((g) => g.resolved && g.path === ref.value)));
|
|
641
|
+
const reviewed = reviewMatch && Boolean(reviewRow);
|
|
642
|
+
doc.review = { stored: reviewedDigest, computed: digest.digest, match: reviewMatch,
|
|
643
|
+
recorded: Boolean(reviewRow), current: reviewed };
|
|
644
|
+
if (reviewedDigest && !reviewed) {
|
|
645
|
+
add(WARN, 'review-unsubstantiated', !reviewMatch
|
|
646
|
+
? 'reviewed_digest does not bind the current document and governed bytes — no current review is established'
|
|
647
|
+
: 'reviewed_digest requires a dated Currency log review row naming this digest and a governed-source path — no current review is established');
|
|
648
|
+
}
|
|
649
|
+
const drift = deriveDrift(root, rel, governed, { floorSha: opts.driftFloor });
|
|
590
650
|
doc.drift = drift;
|
|
591
651
|
// A DECISION THAT IS NOT IN FORCE CANNOT DRIFT (ADR-056, 2026-07-27). Rejected / Superseded /
|
|
592
652
|
// Deprecated documents describe a path NOT taken, or one another document has since taken over.
|
|
@@ -600,15 +660,16 @@ export function evaluateDoc(root, rel, opts = {}) {
|
|
|
600
660
|
// day someone un-rejects it the finding returns to BLOCK on its own.
|
|
601
661
|
const notInForce = /^(rejected|superseded|deprecated)$/i.test(statusWord || '');
|
|
602
662
|
if (drift.state === 'presumed-stale') {
|
|
603
|
-
add(notInForce ? WARN : BLOCK, 'presumed-stale',
|
|
604
|
-
`governed code moved ${drift.commits} commit(s) (${drift.days}d) after the document's own last commit (${drift.docDate})
|
|
605
|
-
+ (
|
|
663
|
+
add(notInForce || reviewed ? WARN : BLOCK, 'presumed-stale',
|
|
664
|
+
`governed code moved ${drift.commits} commit(s) (${drift.days}d) after the document's own last commit (${drift.docDate}). Paths: ${drift.paths.join(', ')}`
|
|
665
|
+
+ (reviewed ? ' — exact current bytes have a recorded review; historical drift remains visible, but does not establish missing review. This is NOT normative agreement or verification.'
|
|
666
|
+
: notInForce ? ` — reported, NOT blocked: status "${statusWord}" means this decision is not in force, so it cannot drift from code it never governed`
|
|
667
|
+
: ' — no source-bound review is recorded since it moved.'));
|
|
606
668
|
} else if (drift.state === 'lagging') {
|
|
607
669
|
add(WARN, 'lagging', `governed code moved ${drift.commits} commit(s) after the document — normal within a session; reported, not blocked`);
|
|
608
670
|
}
|
|
609
671
|
|
|
610
672
|
// ── currency log ──────────────────────────────────────────────────────────────────────────────
|
|
611
|
-
const log = parseCurrencyLog(root, text);
|
|
612
673
|
doc.currencyLog = log;
|
|
613
674
|
if (!doc.legacy && !log.present) {
|
|
614
675
|
add(WARN, 'no-currency-log', 'no `## Currency log` section — the *why* of each change is unrecorded');
|
|
@@ -784,12 +845,23 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
784
845
|
}
|
|
785
846
|
|
|
786
847
|
const dirs = a.dirs ?? DEFAULT_DIRS;
|
|
787
|
-
|
|
848
|
+
let touched = null;
|
|
849
|
+
let driftFloor = null;
|
|
850
|
+
if (a.changed) {
|
|
851
|
+
const diff = git(a.root, ['diff', '--name-only', `${a.changed}...HEAD`]);
|
|
852
|
+
if (!diff.ok) {
|
|
853
|
+
process.stderr.write('[doc-currency] candidate base cannot be resolved.\n');
|
|
854
|
+
return 1;
|
|
855
|
+
}
|
|
856
|
+
touched = new Set(diff.ok ? diff.out.split('\n').filter(Boolean) : []);
|
|
857
|
+
const base = git(a.root, ['merge-base', a.changed, 'HEAD']);
|
|
858
|
+
if (!base.ok || !base.out) return 1;
|
|
859
|
+
if (base.ok && base.out) driftFloor = base.out.split('\n')[0];
|
|
860
|
+
}
|
|
861
|
+
const result = evaluate(a.root, { dirs, checkWiring: !a.noWiring, driftFloor });
|
|
788
862
|
|
|
789
863
|
let scope = null;
|
|
790
|
-
if (
|
|
791
|
-
const r = git(a.root, ['diff', '--name-only', `${a.changed}...HEAD`]);
|
|
792
|
-
const touched = new Set(r.ok ? r.out.split('\n').filter(Boolean) : []);
|
|
864
|
+
if (touched) {
|
|
793
865
|
scope = changedDocumentScope(result.docs, touched);
|
|
794
866
|
}
|
|
795
867
|
|
|
@@ -816,7 +888,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
816
888
|
recipe: DIGEST_RECIPE,
|
|
817
889
|
docs: result.docs.map((d) => ({
|
|
818
890
|
file: d.file, id: d.id, legacy: d.legacy, status: d.status, date: d.date, updated: d.updated,
|
|
819
|
-
implStored: d.implStored, impl: d.impl, digest: d.digest, drift: d.drift,
|
|
891
|
+
implStored: d.implStored, impl: d.impl, digest: d.digest, review: d.review, drift: d.drift,
|
|
820
892
|
governs: d.governsDeclared, findings: d.findings,
|
|
821
893
|
})),
|
|
822
894
|
}, null, 2) + '\n');
|
|
@@ -1,163 +1,5 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# and I skipped it. This hook removes the discretion: it refuses the push if the version surfaces
|
|
7
|
-
# disagree or the self-update manifest 404s. It does not care whether I remembered, or was in a
|
|
8
|
-
# hurry, or "was pretty sure it was fine." Pass or the push does not happen.
|
|
9
|
-
#
|
|
10
|
-
# It blocks ONLY on what is knowable before a push. npm-latest and the live explainer are verified
|
|
11
|
-
# post-publish by `node scripts/release.mjs --publish` (the only sanctioned ship path).
|
|
12
|
-
#
|
|
13
|
-
# Escape hatch is `git push --no-verify` — but if you use it, you are consciously overriding a gate
|
|
14
|
-
# that exists because of a specific failure that cost real user trust. Own that out loud.
|
|
15
|
-
#
|
|
16
|
-
# SECRET SCAN (2026-07-21): a buggy diagnostic echoed live OPENAI and OPENROUTER keys into a
|
|
17
|
-
# terminal, and AgentDB's session autocapture wrote them to .swarm/. Nothing reached git — but the
|
|
18
|
-
# only thing that stopped it was a GLOBAL gitignore that does not travel to a clone or another
|
|
19
|
-
# machine. Relying on "the file happened to be ignored" is not a control. This is: a real key
|
|
20
|
-
# pattern in anything being pushed refuses the push. Placeholders (your-key, abc123, xxxx,
|
|
21
|
-
# deadbeef, example) are excluded, because rUv's own docs are full of them and a gate that cries
|
|
22
|
-
# wolf on documentation is a gate people learn to bypass.
|
|
23
|
-
#
|
|
24
|
-
# SCOPE FIX (2026-07-22): this scanned `git diff --cached HEAD` — the STAGED INDEX. But a push ships
|
|
25
|
-
# COMMITS, and at push time the index is normally clean, so the scan read nothing. Proven both ways
|
|
26
|
-
# in a scratch repo: a key COMMITTED then pushed sailed through, while the same key merely STAGED was
|
|
27
|
-
# blocked. The gate could only catch the one case that never happens. It is the same defect class the
|
|
28
|
-
# 7e715bb commit fixed in the *test* — a check that cannot fail in the situation it was written for —
|
|
29
|
-
# which survived here in the *scan*. Now it reads the actual push range from the hook's stdin.
|
|
30
|
-
RANGE_DIFF=""
|
|
31
|
-
CURRENCY_BASE=""
|
|
32
|
-
while read -r _local_ref local_sha _remote_ref remote_sha; do
|
|
33
|
-
[ -z "$local_sha" ] && continue
|
|
34
|
-
case "$local_sha" in *[!0]*) ;; *) continue ;; esac # all-zeros = branch deletion, nothing to scan
|
|
35
|
-
case "$remote_sha" in
|
|
36
|
-
""|*[!0-9a-f]*|0000000000000000000000000000000000000000) # new branch: scan its whole history vs other refs
|
|
37
|
-
RANGE_DIFF="$RANGE_DIFF$(git diff --no-color -U0 "$(git merge-base HEAD origin/main 2>/dev/null || echo '')" "$local_sha" 2>/dev/null)"
|
|
38
|
-
# ADR-056 §5: a new branch has no remote counterpart, so the merge-base with main is the only
|
|
39
|
-
# honest base. If even that cannot resolve, CURRENCY_BASE stays empty and the currency check is
|
|
40
|
-
# SKIPPED rather than run against a guess — never a scope invented from nothing.
|
|
41
|
-
[ -z "$CURRENCY_BASE" ] && CURRENCY_BASE="$(git merge-base HEAD origin/main 2>/dev/null || true)" ;;
|
|
42
|
-
*)
|
|
43
|
-
RANGE_DIFF="$RANGE_DIFF$(git diff --no-color -U0 "$remote_sha" "$local_sha" 2>/dev/null)"
|
|
44
|
-
# ADR-056: the remote tip is only a valid "what did I change" base when it is an ANCESTOR of
|
|
45
|
-
# what we are pushing. After a rebase or squash it is NOT — the range then spans the entire
|
|
46
|
-
# rewrite and drags in every document the upstream branch already carried, red ones included.
|
|
47
|
-
# Measured 2026-07-27: a force-push after rebasing onto main was refused for ADR-054 and
|
|
48
|
-
# ADR-055 — two documents on main that this branch never touched. GPT-5.6 predicted exactly
|
|
49
|
-
# this in the ADR-056 duel ("rebase or squash changes the touched-file set without changing
|
|
50
|
-
# whether a violation is genuinely new"). So: ancestor => use it; otherwise fall back to the
|
|
51
|
-
# merge-base with main, which is the honest answer to "what is mine".
|
|
52
|
-
if [ -z "$CURRENCY_BASE" ]; then
|
|
53
|
-
if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
|
|
54
|
-
CURRENCY_BASE="$remote_sha"
|
|
55
|
-
else
|
|
56
|
-
CURRENCY_BASE="$(git merge-base "$local_sha" origin/main 2>/dev/null || true)"
|
|
57
|
-
fi
|
|
58
|
-
fi ;;
|
|
59
|
-
esac
|
|
60
|
-
done
|
|
61
|
-
# Fall back to the staged index too, so a pre-commit-style invocation still gets covered.
|
|
62
|
-
RANGE_DIFF="$RANGE_DIFF
|
|
63
|
-
$(git diff --cached --no-color -U0 HEAD 2>/dev/null)"
|
|
64
|
-
|
|
65
|
-
SECRET_HITS=$(printf '%s' "$RANGE_DIFF" | grep -E "^\+" \
|
|
66
|
-
| grep -oE "sk-(proj|ant|or-v1)-[A-Za-z0-9_-]{20,}" \
|
|
67
|
-
| grep -vEi "your-key|abc123|xxxx+|deadbeef|example|placeholder|REDACTED" | head -3)
|
|
68
|
-
if [ -n "$SECRET_HITS" ]; then
|
|
69
|
-
echo ""
|
|
70
|
-
echo " ⛔ PUSH BLOCKED — what looks like a REAL API key is in this push."
|
|
71
|
-
echo " matched (prefix only): $(echo "$SECRET_HITS" | cut -c1-14 | tr '\n' ' ')"
|
|
72
|
-
echo " Remove it, rotate the key, and push again. Never --no-verify past this one:"
|
|
73
|
-
echo " a key that reaches a remote is compromised the moment it lands."
|
|
74
|
-
echo ""
|
|
75
|
-
exit 1
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
# Hooks may be shared by several git worktrees. Resolving from this script's own path is wrong when
|
|
79
|
-
# core.hooksPath names another checkout: the hook then validates that checkout while pushing the
|
|
80
|
-
# current one (measured 2026-07-28: announced 3.9.128, pushed 3.9.129). Git already knows the
|
|
81
|
-
# worktree whose refs are being pushed, so derive the shipping root from that live repository.
|
|
82
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
83
|
-
if [ -z "$ROOT" ] || [ ! -f "$ROOT/scripts/verify-channels.mjs" ]; then
|
|
84
|
-
echo " ⛔ pre-push gate cannot resolve the worktree being pushed."
|
|
85
|
-
exit 1
|
|
86
|
-
fi
|
|
87
|
-
node "$ROOT/scripts/verify-channels.mjs" --pre-push
|
|
88
|
-
status=$?
|
|
89
|
-
if [ "$status" -ne 0 ]; then
|
|
90
|
-
echo ""
|
|
91
|
-
echo " ⛔ pre-push gate refused this push. A user on some path would get a broken build."
|
|
92
|
-
echo " Fix the channel(s) above, then push again. Do not route around this."
|
|
93
|
-
exit 1
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
# Generated public census and command aliases have one source of truth each. Both writers have a
|
|
97
|
-
# `--check` mode specifically for CI/pre-push; until 2026-08-22 nothing invoked either check, so the
|
|
98
|
-
# writers were built and tested but their promised gate did not exist.
|
|
99
|
-
node "$ROOT/scripts/sync-census.mjs" --check
|
|
100
|
-
census_status=$?
|
|
101
|
-
if [ "$census_status" -ne 0 ]; then
|
|
102
|
-
echo ""
|
|
103
|
-
echo " ⛔ pre-push gate refused this push — public census claims have drifted."
|
|
104
|
-
echo " Run: node scripts/sync-census.mjs"
|
|
105
|
-
exit 1
|
|
106
|
-
fi
|
|
107
|
-
|
|
108
|
-
node "$ROOT/scripts/sync-commands.mjs" --check
|
|
109
|
-
commands_status=$?
|
|
110
|
-
if [ "$commands_status" -ne 0 ]; then
|
|
111
|
-
echo ""
|
|
112
|
-
echo " ⛔ pre-push gate refused this push — command aliases no longer share one body."
|
|
113
|
-
echo " Run: node scripts/sync-commands.mjs"
|
|
114
|
-
exit 1
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
# The convergence manifest is the exact source/version/ADR identity consumed by hosted QA.
|
|
118
|
-
# Check it here so a push cannot publish a commit whose generated identity was produced before
|
|
119
|
-
# the final source edit (the failure mode that repeatedly escaped local pre-push validation).
|
|
120
|
-
if [ -f "$ROOT/scripts/convergence-manifest.mjs" ]; then
|
|
121
|
-
node "$ROOT/scripts/convergence-manifest.mjs"
|
|
122
|
-
convergence_status=$?
|
|
123
|
-
else
|
|
124
|
-
convergence_status=0
|
|
125
|
-
fi
|
|
126
|
-
if [ "$convergence_status" -ne 0 ]; then
|
|
127
|
-
echo ""
|
|
128
|
-
echo " ⛔ pre-push gate refused this push — convergence identity is stale."
|
|
129
|
-
echo " Run: npm run convergence:write"
|
|
130
|
-
exit 1
|
|
131
|
-
fi
|
|
132
|
-
|
|
133
|
-
# ── DOCUMENT CURRENCY (ADR-056 §5) ─────────────────────────────────────────────────────────────────
|
|
134
|
-
# The tool was written and tested on 2026-07-22 and then invoked by NOTHING for five days — absent
|
|
135
|
-
# from this hook, gate.sh, gates.mjs and every workflow, while `wired-check` reported it "wired"
|
|
136
|
-
# because package.json DEFINED an npm alias for it. This line is the whole fix.
|
|
137
|
-
#
|
|
138
|
-
# NO RATCHET, DELIBERATELY. The first draft of ADR-056 proposed a stateful new-vs-pre-existing
|
|
139
|
-
# violation tracker so this gate could coexist with 32 pre-existing BLOCKs forever. Two independent
|
|
140
|
-
# adversarial reviews killed it: a committed baseline is gamed by the same commit that adds the
|
|
141
|
-
# violation, re-deriving at the remote SHA needs a second worktree, and "report the rest forever"
|
|
142
|
-
# is the wolf-cry doc-currency.mjs's own header names as its designed failure mode. The debt was
|
|
143
|
-
# PAID instead (32 BLOCKs -> 4, all four genuine re-reads), which makes the plain gate safe.
|
|
144
|
-
#
|
|
145
|
-
# SCOPED to what this push touches. A pre-existing red document must not block someone who merely
|
|
146
|
-
# walked past it — that teaches people never to touch the red documents, preserving exactly the drift
|
|
147
|
-
# this exists to end. Scope is empty => nothing to check => pass; never a scope invented from nothing.
|
|
148
|
-
if [ -n "$CURRENCY_BASE" ]; then
|
|
149
|
-
node "$ROOT/scripts/doc-currency.mjs" --check --changed "$CURRENCY_BASE"
|
|
150
|
-
dc_status=$?
|
|
151
|
-
if [ "$dc_status" -ne 0 ]; then
|
|
152
|
-
echo ""
|
|
153
|
-
echo " ⛔ pre-push gate refused this push — a document you touched disagrees with its code."
|
|
154
|
-
echo " An ADR that describes a world the code left is worse than no ADR."
|
|
155
|
-
echo " \`node scripts/doc-currency.mjs --fix\` backfills every date git can prove;"
|
|
156
|
-
echo " everything else is a claim only you can make."
|
|
157
|
-
exit 1
|
|
158
|
-
fi
|
|
159
|
-
fi
|
|
160
|
-
echo ""
|
|
161
|
-
echo " ✓ pre-push gate passed — version, channels, secrets, and document currency are clean."
|
|
162
|
-
echo ""
|
|
163
|
-
exit 0
|
|
2
|
+
# Select the active checkout's development check. Qualification is explicit and hosted once.
|
|
3
|
+
set -eu
|
|
4
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
5
|
+
exec node "$ROOT/scripts/development-push-check.mjs"
|
|
@@ -26,6 +26,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
26
26
|
import { findStores, diagnose } from './memory-doctor.mjs';
|
|
27
27
|
import { learnerCwd, loadRuntimePreferences } from '../plugin/scripts/runtime-preferences.mjs';
|
|
28
28
|
import { projectDirectory } from '../plugin/scripts/project-identity.mjs';
|
|
29
|
+
import { newestSnapshot, snapshotInventory, MTIME_GRACE_MS } from './snapshot-freshness.mjs';
|
|
29
30
|
|
|
30
31
|
const HOME = os.homedir();
|
|
31
32
|
// The SAME project root learn-flush.mjs computes, by the same rule — the two halves of the flush
|
|
@@ -263,22 +264,39 @@ function distillFleet() {
|
|
|
263
264
|
const receiptPath = argv.includes('--receipt') ? argv[argv.indexOf('--receipt') + 1] : null;
|
|
264
265
|
const receipt = [];
|
|
265
266
|
const writeReceipt = () => {
|
|
266
|
-
if (!receiptPath) return;
|
|
267
|
+
if (!receiptPath) return true;
|
|
267
268
|
try {
|
|
268
269
|
fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
|
|
269
270
|
fs.writeFileSync(receiptPath, JSON.stringify({ at: new Date().toISOString(), stores: receipt }, null, 2));
|
|
270
|
-
|
|
271
|
+
return true;
|
|
272
|
+
} catch { return false; }
|
|
271
273
|
};
|
|
272
274
|
|
|
273
275
|
for (const t of targets) {
|
|
274
276
|
const dir = path.join(path.dirname(t.db), 'backups');
|
|
277
|
+
const backupStartedAt = Date.now();
|
|
278
|
+
let priorSnapshots;
|
|
279
|
+
try { priorSnapshots = snapshotInventory(dir); }
|
|
280
|
+
catch (error) { failed.push(`${t.name}: refused to distill — ${error.message}`); continue; }
|
|
275
281
|
try { execFileSync(RUFLO, ['memory', 'backup', '--db', t.db, '--dir', dir], { env: RUFLO_ENV, stdio: 'ignore', timeout: 300_000 }); }
|
|
276
282
|
catch (e) { failed.push(`${t.name}: refused to distill — snapshot failed (${String(e.message).slice(0, 80)})`); continue; }
|
|
283
|
+
// `memory backup` exiting 0 is not proof a NEW file landed in `dir` — a stale snapshot from a
|
|
284
|
+
// prior fleet run sitting there already would otherwise be silently accepted as this run's undo
|
|
285
|
+
// (the exact gap `distill-project.mjs` was fixed for in PR #192, applied here to fleet's own,
|
|
286
|
+
// separate call site — see the 2026-08-29 Dream Cycle report, "Next steps #1").
|
|
287
|
+
const snapshot = newestSnapshot(dir, backupStartedAt, MTIME_GRACE_MS, priorSnapshots);
|
|
288
|
+
if (!snapshot) {
|
|
289
|
+
failed.push(`${t.name}: refused to distill — snapshot reported success but no fresh file landed in ${dir}`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
277
292
|
// Record the snapshot BEFORE distilling, and flush after every store. If the process is killed
|
|
278
293
|
// mid-fleet, the receipt still names every store already modified — a partial receipt is
|
|
279
294
|
// recoverable, a missing one is not.
|
|
280
|
-
receipt.push({ db: t.db, name: t.name, backupDir: dir });
|
|
281
|
-
writeReceipt()
|
|
295
|
+
receipt.push({ db: t.db, name: t.name, backupDir: dir, snapshot });
|
|
296
|
+
if (!writeReceipt()) {
|
|
297
|
+
failed.push(`${t.name}: refused to distill — requested undo receipt could not be written`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
282
300
|
|
|
283
301
|
try {
|
|
284
302
|
execFileSync(RUFLO, ['memory', 'distill', 'run', '--db', t.db, '--judge', 'structural', '--budget-usd', '0'],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { automaticHookRetirementStatus } from '../bin/install.mjs';
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
|
|
8
|
+
export function main() {
|
|
9
|
+
const result = automaticHookRetirementStatus(ROOT);
|
|
10
|
+
if (!result.ok) {
|
|
11
|
+
console.error(`hook-retirement-check: FAIL — ${result.registrations.length} automatic registration(s), ${result.errors.length} manifest error(s)`);
|
|
12
|
+
for (const error of result.errors) console.error(` ${error}`);
|
|
13
|
+
for (const item of result.registrations) console.error(` ${item.file} ${item.event}: ${item.command || '(empty command)'}`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
console.log(`hook-retirement-check: PASS — zero automatic Brain registrations across ${result.files.length} source, contract, and host-pointer surfaces`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
21
|
+
process.exitCode = main();
|
|
22
|
+
}
|