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
|
@@ -18,11 +18,21 @@
|
|
|
18
18
|
# is the product. This is that discipline applied at the push boundary.
|
|
19
19
|
#
|
|
20
20
|
# CONTRACT: exit 0 = allow · exit 2 + stderr = BLOCK. FAILS OPEN on anything unparseable.
|
|
21
|
-
# Opt-in (router profile.json)
|
|
21
|
+
# Opt-in (router profile.json); local Git checks and a Node maintenance-state check.
|
|
22
22
|
# ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
24
|
set -uo pipefail
|
|
25
25
|
|
|
26
|
+
# Reversible, repository-scoped development maintenance; no profile, ledger, or Git work first.
|
|
27
|
+
MAINTENANCE_HELPER="${BASH_SOURCE[0]%/*}/development-maintenance.mjs"
|
|
28
|
+
if [ -f "$MAINTENANCE_HELPER" ] && node --input-type=module -e '
|
|
29
|
+
import { pathToFileURL } from "node:url";
|
|
30
|
+
const { developmentHooksSuspended } = await import(pathToFileURL(process.argv[1]));
|
|
31
|
+
process.exit(developmentHooksSuspended() ? 0 : 1);
|
|
32
|
+
' "$MAINTENANCE_HELPER" >/dev/null 2>&1; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
26
36
|
INPUT=""
|
|
27
37
|
# BOUNDED READ (2026-07-27, ADR-055 F20): an unqualified `read` never returns on a stdin that is
|
|
28
38
|
# opened and never closed — measured across the mesh, 18 of 37 registered commands sat until the
|
|
@@ -42,7 +52,9 @@ PROFILE="${MODEL_ROUTER_PROFILE:-$HOME/.claude/model-router/profile.json}"
|
|
|
42
52
|
[ -f "$PROFILE" ] || exit 0
|
|
43
53
|
[ "${RUVNET_SKIP_VERSION_GATE:-0}" = "1" ] && exit 0
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
# Consume JSON escape pairs so quoted commit messages do not truncate the command before a
|
|
56
|
+
# following git push. This retains the existing bounded Bash parser's matching behavior.
|
|
57
|
+
field() { local re="\"$1\"[[:space:]]*:[[:space:]]*\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\""; [[ $INPUT =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; }
|
|
46
58
|
[ "$(field tool_name)" = "Bash" ] || exit 0
|
|
47
59
|
CMD=$(field command)
|
|
48
60
|
[[ $CMD == *"git push"* ]] || exit 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: release-proof
|
|
3
3
|
description: Fail-closed exact-artifact release and deployment authority. Use before saying a release is ready, pushing a release commit, publishing npm packages, creating GitHub releases, deploying production, closing release-blocking issues, or claiming all gates are green. Requires clean immutable lineage, zero labeled release blockers, exact-SHA GitHub success, nonzero no-skip QE, packed-artifact host tests, installed Brain/RVF proof, and post-publication byte verification.
|
|
4
|
-
updated: 2026-09-
|
|
4
|
+
updated: 2026-09-07
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Release Proof
|
|
@@ -23,7 +23,11 @@ green.
|
|
|
23
23
|
7. Require the source package, Claude manifest, Codex manifest, packed npm version, bundle
|
|
24
24
|
`brainVersion`/`releaseTag`, and both installed host versions to identify one exact generation.
|
|
25
25
|
8. Require the active Brain registry to contain the `ruvnet-brain` RVF store and require narrow,
|
|
26
|
-
broad, and concurrent cited searches to complete within 80% of their deadline.
|
|
26
|
+
broad, and concurrent cited candidate searches to complete within 80% of their deadline.
|
|
27
|
+
For fresh public stabilization verification, `stabilization-public-deadline-v1` requires the
|
|
28
|
+
first cited search in every host mode and the Brain probe within the enforced 30-second
|
|
29
|
+
deadline. The 24-second headroom target remains a performance follow-up, not promotion authority.
|
|
30
|
+
Receipts without this named policy retain the legacy 80% rule; failed runs are not relabeled.
|
|
27
31
|
9. Publish only through the protected release workflow. Never run `npm publish` or `gh release
|
|
28
32
|
create` locally.
|
|
29
33
|
10. After publication, download npm and GitHub artifacts, compare their bytes with the seal, install
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Receipt contract
|
|
2
2
|
|
|
3
|
-
Updated: 2026-09-
|
|
3
|
+
Updated: 2026-09-07
|
|
4
4
|
|
|
5
5
|
The authority accepts only typed receipt schemas. The candidate receipts are emitted by
|
|
6
6
|
`release-candidate-preflight` and imported by `protected-release`; publication receipts are emitted
|
|
@@ -38,9 +38,38 @@ Required bindings:
|
|
|
38
38
|
and installed Claude/Codex versions
|
|
39
39
|
- npm and GitHub release bytes matching the candidate digest
|
|
40
40
|
- clean installed Claude and Codex results from the public package
|
|
41
|
-
- installed Brain self-RVF and
|
|
41
|
+
- installed Brain self-RVF and measured first cited searches within the declared acceptance policy
|
|
42
|
+
- `acceptancePolicy: stabilization-public-deadline-v1` requires a fixed 30,000ms deadline and
|
|
43
|
+
finite, nonnegative host and Brain timings at or below it; larger budgets are rejected
|
|
44
|
+
- receipts without that policy retain the legacy 80-percent margin; unknown policies fail
|
|
42
45
|
- successful exact-SHA `published-surface-probe`
|
|
43
46
|
|
|
47
|
+
Stuart authorized a materially improved stabilization release on September 7, rather than
|
|
48
|
+
requiring every historical performance goal for promotion. The public runtime already enforces
|
|
49
|
+
a 30-second deadline. The new policy removes only the additional 24-second promotion margin.
|
|
50
|
+
It does not replace first-search timings with warm timings or reduce retrieval, citation,
|
|
51
|
+
artifact-integrity, native-update, or host-installation requirements. Fresh signed evidence is
|
|
52
|
+
required; prior failed workflows remain failed.
|
|
53
|
+
|
|
54
|
+
Native update verification suppresses optional npm command shims only during the initial
|
|
55
|
+
reader installation (`npm_config_bin_links=false`). The scheduler retains npm's default
|
|
56
|
+
command-shim behavior, which `npx` requires. The receipt records this installation configuration.
|
|
57
|
+
The default reader install creates a `semver` symlink that 4.3.10's updater rejects; fixing that
|
|
58
|
+
default-install compatibility remains a follow-up. Symlink rejection is not bypassed.
|
|
59
|
+
|
|
60
|
+
The native scheduler executes its actual production target, `ruvnet-brain@latest`. A fresh isolated
|
|
61
|
+
npm cache is prepared and its package files must match the verified public tarball installation.
|
|
62
|
+
Exactly one cached Brain package is allowed. Full package tree observations before and after each
|
|
63
|
+
of two scheduled runs, plus each run's actual desired version, bind the execution evidence to the
|
|
64
|
+
published package. The consumer validates these observations and their time bounds. A changed
|
|
65
|
+
latest version or changed package bytes fails the proof. Dependencies are outside the package-tree
|
|
66
|
+
comparison. The two runs verify the already-current update path; they do not establish fallback
|
|
67
|
+
reinstallation or new upstream corpus generation. Only empty installer-created preserved
|
|
68
|
+
directories are removed in the disposable fixture; populated or linked entries are retained and
|
|
69
|
+
still fail duplicate-copy acceptance. Storage observations wait for the refresh owner to exit
|
|
70
|
+
and its lock to be removed. The second run may add only measured receipt and bounded log bytes;
|
|
71
|
+
any other managed-storage growth remains a failure.
|
|
72
|
+
|
|
44
73
|
Before provider mutation, `protected-release` revalidates the imported candidate receipt, package
|
|
45
74
|
payload, source binding, and digest against current `origin/main`. It consumes the long-lane proof;
|
|
46
75
|
it does not rerun CI, integration, UX, or stranger qualification.
|
|
@@ -169,15 +169,24 @@ export function evaluatePublicationReceipt(candidate, publication) {
|
|
|
169
169
|
const item = publication?.[surface];
|
|
170
170
|
if (item?.sha !== sha || item?.artifactSha256 !== digest) failures.push(fail('PUBLIC_ARTIFACT_MISMATCH', `${surface} differs from candidate seal`));
|
|
171
171
|
}
|
|
172
|
+
const stabilization = publication?.acceptancePolicy === 'stabilization-public-deadline-v1';
|
|
173
|
+
const deadline = Number(publication?.brain?.deadlineMs);
|
|
174
|
+
if (publication?.acceptancePolicy !== undefined && !stabilization) {
|
|
175
|
+
failures.push(fail('PUBLIC_ACCEPTANCE_POLICY', 'unknown public acceptance policy'));
|
|
176
|
+
}
|
|
177
|
+
const withinDeadline = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
178
|
+
&& Number.isFinite(deadline) && deadline > 0
|
|
179
|
+
&& (!stabilization || deadline === 30_000)
|
|
180
|
+
&& value <= deadline * (stabilization ? 1 : 0.8);
|
|
172
181
|
for (const hostName of ['claudeOnly', 'codexOnly', 'dual']) {
|
|
173
182
|
const host = publication?.installed?.[hostName];
|
|
174
183
|
if (host?.status !== 'PASS' || host?.doctorExit !== 0 || host?.artifactSha256 !== digest
|
|
175
|
-
|| host?.functionalSearch !== true || !(host?.searchMs
|
|
184
|
+
|| host?.functionalSearch !== true || !withinDeadline(host?.searchMs)) {
|
|
176
185
|
failures.push(fail('PUBLIC_HOST_NOT_PASS', `${hostName} is not running a doctor-clean functional sealed public artifact`));
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
const brain = publication?.brain || {};
|
|
180
|
-
if (brain.status !== 'PASS' || brain.selfStore !== true || !(brain.broadMs
|
|
189
|
+
if (brain.status !== 'PASS' || brain.selfStore !== true || !withinDeadline(brain.broadMs)) failures.push(fail('PUBLIC_BRAIN_NOT_PASS', 'public installed Brain acceptance failed'));
|
|
181
190
|
const probes = Array.isArray(publication?.postPublicationChecks) ? publication.postPublicationChecks : [];
|
|
182
191
|
const probe = probes.find((check) => check?.name === 'published-surface-probe' && check?.sha === sha);
|
|
183
192
|
if (probe?.status !== 'completed' || probe?.conclusion !== 'success') failures.push(fail('POST_PUBLICATION_CHECK_NOT_GREEN', 'published-surface-probe is not green'));
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
# THE PLAYBOOK — the standing build playbook, in full
|
|
2
2
|
|
|
3
|
-
Updated: 2026-
|
|
3
|
+
Updated: 2026-09-07 11:07:28 EDT | Version 1.2.0
|
|
4
4
|
Created: 2026-07-27
|
|
5
5
|
|
|
6
|
-
**Read this
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
as *THE PLAYBOOK*.
|
|
6
|
+
**Read this when explicitly using the Brain build skill.** Automatic Brain lifecycle hooks are
|
|
7
|
+
retired: no SessionStart or UserPromptSubmit registration injects this playbook. MCP, skills and
|
|
8
|
+
explicit commands remain available; the user or lead agent invokes them when appropriate.
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
about a fifth of the bytes, and the full text stays one `Read` away for the turn that actually needs
|
|
19
|
-
it. The condensed form the hook injects keeps every operative instruction — what is here and not
|
|
20
|
-
there is elaboration, worked examples, and phrasing guidance.
|
|
10
|
+
Release work uses `scripts/release-qualification.mjs` and its reviewed requirement inventory.
|
|
11
|
+
The retained tests were individually reviewed; the historical approximately 4,332 unit cases were
|
|
12
|
+
not all individually audited and remain diagnostics. A diagnostic pass is not production proof.
|
|
13
|
+
Promote only the clean exact candidate SHA and its sealed artifacts after qualification. Public
|
|
14
|
+
acceptance still requires all nine OS/host-mode leaves plus real native installed-update proof on
|
|
15
|
+
each platform, ending at `install-verified`. Imported upstream corpus freshness remains UNKNOWN
|
|
16
|
+
until separately proven. See `docs/QA-RELEASE-PROCESS.md` and the audit records in `docs/reviews/`.
|
|
21
17
|
|
|
22
18
|
---
|
|
23
19
|
|
|
@@ -110,14 +106,10 @@ rule-compliance, cite a source the tools didn't return, or claim a check that di
|
|
|
110
106
|
A completion moves that task to completed, unblocks its dependents, and the freed slot claims the
|
|
111
107
|
first unassigned, unblocked pending task immediately. Only allow a slot to idle when no such task
|
|
112
108
|
exists. Keep dependent integration with the designated integration owner.
|
|
113
|
-
- **Claude Code:**
|
|
114
|
-
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
shared-task hook ledger. Initial fan-out and completion-notification recycling are guidance,
|
|
118
|
-
not hook enforcement: the lead must immediately dispatch the next ready ledger item when a
|
|
119
|
-
collaboration slot completes. State this degraded boundary if it affects the run; never call it
|
|
120
|
-
enforced.
|
|
109
|
+
- **Claude Code and Codex:** the lead explicitly dispatches the next ready task when a slot
|
|
110
|
+
completes. No automatic `TeammateIdle` or `TaskCompleted` Brain handler enforces recycling.
|
|
111
|
+
`plugin/scripts/swarm-slot-recycler.mjs` remains available for explicit invocation with an
|
|
112
|
+
appropriate task ledger; do not describe its mere presence as active host enforcement.
|
|
121
113
|
If Ruflo / RuVector MCP tools aren't available in this environment, DON'T block or stall — degrade
|
|
122
114
|
gracefully to the native host's agents and local .rvf, and briefly note the tool that would make
|
|
123
115
|
it better + how to add it. Never demand a tool the user doesn't have.
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { getVersion, getVersionTag, stripTag } from './version.mjs';
|
|
|
22
22
|
import { auditRvfIndexes } from './rvf-index-audit.mjs';
|
|
23
23
|
import { readRvfGenerations, validateSelectedRvfGenerations } from './rvf-generation.mjs';
|
|
24
24
|
import { validatePublicInventory } from './public-inventory.mjs';
|
|
25
|
+
import { bindAssembledReleaseProjection } from './release-projection.mjs';
|
|
25
26
|
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
26
27
|
// sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
|
|
27
28
|
import { orgRepoCount } from './org-repo-count.mjs';
|
|
@@ -590,6 +591,13 @@ if (missing.length) {
|
|
|
590
591
|
process.exit(1);
|
|
591
592
|
}
|
|
592
593
|
|
|
594
|
+
if (PROJECTION) {
|
|
595
|
+
const source = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
|
|
596
|
+
if (source.status !== 0) throw new Error('cannot identify the projected bundle source');
|
|
597
|
+
bindAssembledReleaseProjection({ assetsDir: OUT, version: stripTag(BRAIN_VERSION),
|
|
598
|
+
sourceSnapshot: source.stdout.trim() });
|
|
599
|
+
}
|
|
600
|
+
|
|
593
601
|
// ---- exact release artifact -------------------------------------------------------------------
|
|
594
602
|
// The release signer signs dist/ruvnet-brain.zip, not this assembly directory. Rebuilding only the
|
|
595
603
|
// directory can therefore sign stale bytes left by an older run—the source tree is green while
|
package/scripts/build-primer.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { rerankKb } from '../kb/forge-rerank.mjs';
|
|
16
|
+
import { writeGroundedPrimer } from './primer-grounding.mjs';
|
|
16
17
|
|
|
17
18
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
19
|
const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
@@ -57,10 +58,8 @@ for (const a of ARCHES) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const primer = `# ${NAME} — Primer\n\n<!-- Generated primer · grounded in real source via rerankKb (${VARIANT}) · archetypes: ${ARCHES.map(a => a.key).join(', ')} -->\n\n${sections.join('\n')}`;
|
|
60
|
-
const countRefs = (txt) => [...new Set([...allPaths].filter((p) => txt.includes(p) || txt.includes(p.split('/').pop())))];
|
|
61
|
-
const refs = countRefs(primer);
|
|
62
61
|
const outFile = path.join(KB, `${NAME}-primer.md`);
|
|
63
|
-
|
|
62
|
+
const refs = writeGroundedPrimer({ primer, sourcePaths: allPaths, output: outFile });
|
|
64
63
|
|
|
65
64
|
// 3-vendor "is this a complete, correct primer?" score (informational; the hard gate is citation count)
|
|
66
65
|
const jsys = 'Grade 1-100 whether this repo primer is COMPLETE, CORRECT and CONFIDENT for an engineer new to the repo (98=excellent/actionable; vague-or-hedgy=POISON<50). Return ONLY {"score":N,"reason":"<=15 words"}.';
|
|
@@ -1,56 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import crypto from 'node:crypto';
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { stagedHostVerifier, readCandidateRetrieval, verifyCandidateRetrievalAssets } from './staged-host-verifier.mjs';
|
|
6
6
|
import { payloadIdFor } from './release-payload.mjs';
|
|
7
|
+
import { validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
|
|
7
8
|
|
|
8
9
|
const arg = (name) => {
|
|
9
10
|
const index = process.argv.indexOf(name);
|
|
10
11
|
return index >= 0 ? process.argv[index + 1] : null;
|
|
11
12
|
};
|
|
12
|
-
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const result = await
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
throw new Error(`${name} did not produce a clean installed-search grounding receipt`);
|
|
14
|
+
export async function buildCandidateHostEvidence({ manifestFile, packagePath, bundlePath, planFile, coverageFile, failureFile },
|
|
15
|
+
{ createVerifier = stagedHostVerifier } = {}) {
|
|
16
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
17
|
+
const payloadId = payloadIdFor(manifest);
|
|
18
|
+
const identity = { version: manifest.version, candidateSha: manifest.candidateSha, payloadId };
|
|
19
|
+
const retrieval = readCandidateRetrieval({ manifest, planFile, coverageFile });
|
|
20
|
+
verifyCandidateRetrievalAssets({ retrieval, assets: { packagePath, bundlePath } });
|
|
21
|
+
const result = await createVerifier({ assets: { packagePath, bundlePath }, identity, retrieval })
|
|
22
|
+
.verify({ source: 'candidate', assets: { packagePath, bundlePath } });
|
|
23
|
+
verifyCandidateRetrievalAssets({ retrieval, assets: { packagePath, bundlePath } });
|
|
24
|
+
if (result.verdict !== 'PASS') {
|
|
25
|
+
if (failureFile) fs.writeFileSync(path.resolve(failureFile), JSON.stringify({
|
|
26
|
+
schemaVersion: 1, kind: 'ruvnet-brain-candidate-host-failure', verdict: 'FAIL',
|
|
27
|
+
sha: manifest.candidateSha, payloadId, artifactSha256: retrieval.artifactSha256,
|
|
28
|
+
candidateArchiveSha256: retrieval.candidateArchiveSha256, planSha256: retrieval.plan.planSha256,
|
|
29
|
+
result,
|
|
30
|
+
}, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
31
|
+
throw new Error(`candidate host matrix failed: ${result.error || 'unknown'}`);
|
|
33
32
|
}
|
|
33
|
+
|
|
34
|
+
const modeNames = { claude: 'claude-only', codex: 'codex-only', dual: 'dual-host' };
|
|
35
|
+
const leaves = Object.entries(modeNames).map(([mode, name]) => {
|
|
36
|
+
const fixture = result.fixtures?.[mode];
|
|
37
|
+
const grounding = fixture?.grounding;
|
|
38
|
+
const grounded = grounding && ['repo', 'path', 'file', 'storedPath']
|
|
39
|
+
.every((field) => typeof grounding[field] === 'string' && grounding[field].trim());
|
|
40
|
+
if (fixture?.status !== 'PASS' || fixture?.process?.status !== 0 || !grounded) {
|
|
41
|
+
throw new Error(`${name} did not produce a clean installed-search grounding receipt`);
|
|
42
|
+
}
|
|
43
|
+
validateRetrievalCanaryReceipt(fixture.retrieval, { plan: retrieval.plan });
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
sha: manifest.candidateSha,
|
|
47
|
+
payloadId,
|
|
48
|
+
status: fixture.status === 'PASS' ? 'completed' : 'failed',
|
|
49
|
+
conclusion: 'success',
|
|
50
|
+
verdict: 'PASS',
|
|
51
|
+
source: 'candidate-host-evidence',
|
|
52
|
+
mode,
|
|
53
|
+
functionalSearch: true,
|
|
54
|
+
searchExit: fixture.process.status,
|
|
55
|
+
grounding,
|
|
56
|
+
retrieval: fixture.retrieval,
|
|
57
|
+
artifactSha256: retrieval.artifactSha256,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
34
60
|
return {
|
|
35
|
-
|
|
61
|
+
schemaVersion: 1,
|
|
36
62
|
sha: manifest.candidateSha,
|
|
37
63
|
payloadId,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
verdict: 'PASS',
|
|
41
|
-
source: 'candidate-host-evidence',
|
|
42
|
-
mode,
|
|
43
|
-
functionalSearch: true,
|
|
44
|
-
searchExit: fixture.process.status,
|
|
45
|
-
grounding,
|
|
46
|
-
artifactSha256: sha256(packagePath),
|
|
64
|
+
artifactSha256: retrieval.artifactSha256,
|
|
65
|
+
leaves,
|
|
47
66
|
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
leaves
|
|
55
|
-
}
|
|
56
|
-
console.log(JSON.stringify({ verdict: 'PASS', payloadId, leaves: leaves.map(({ name }) => name) }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
70
|
+
const evidence = await buildCandidateHostEvidence({ manifestFile: arg('--manifest'),
|
|
71
|
+
packagePath: arg('--package'), bundlePath: arg('--bundle'), planFile: arg('--plan'), coverageFile: arg('--coverage'), failureFile: arg('--out') ? `${arg('--out')}.failure.json` : null });
|
|
72
|
+
fs.writeFileSync(path.resolve(arg('--out')), `${JSON.stringify(evidence, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
73
|
+
console.log(JSON.stringify({ verdict: 'PASS', payloadId: evidence.payloadId, leaves: evidence.leaves.map(({ name }) => name) }));
|
|
74
|
+
}
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
//
|
|
24
24
|
// node scripts/ci/build-fixture-kb.mjs --out <dir> [--drop-mcp] [--no-rvf]
|
|
25
25
|
import fs from 'node:fs';
|
|
26
|
+
import crypto from 'node:crypto';
|
|
27
|
+
import { coverageGenerationFor, releaseCoverageGenerationFor, validateCoverageDirectory } from '../../plugin/scripts/coverage-integrity.mjs';
|
|
28
|
+
import { validatePublicInventory } from '../public-inventory.mjs';
|
|
26
29
|
import path from 'node:path';
|
|
27
30
|
import { fileURLToPath } from 'node:url';
|
|
28
31
|
|
|
@@ -60,8 +63,62 @@ fs.writeFileSync(path.join(OUT, 'package.json'), JSON.stringify({
|
|
|
60
63
|
},
|
|
61
64
|
}, null, 2));
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
// Synthetic coverage uses the production validators; it is not release evidence.
|
|
67
|
+
{
|
|
68
|
+
const dir = OUT;
|
|
69
|
+
const source = { brainVersion: JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'))).version, stores: { fixture: {} } };
|
|
70
|
+
fs.writeFileSync(path.join(dir, 'SOURCE.json'), JSON.stringify(source));
|
|
71
|
+
const storeNames = ['fixture'];
|
|
72
|
+
const sourceSnapshot = 'd'.repeat(40);
|
|
73
|
+
const publicLedger = { schemaVersion: 2, kind: 'ruvnet-brain-public-generation-ledger',
|
|
74
|
+
brainVersion: source.brainVersion, releaseTag: `v${source.brainVersion}`, sourceSnapshot, stores: {} };
|
|
75
|
+
for (const name of storeNames) {
|
|
76
|
+
const bytes = Buffer.alloc(512, 7);
|
|
77
|
+
fs.writeFileSync(path.join(dir, `${name}.big.rvf`), bytes);
|
|
78
|
+
publicLedger.stores[name] = { file: `${name}.big.rvf`, sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
79
|
+
bytes: bytes.length, sourceCommit: source.stores[name].sourceCommit || null,
|
|
80
|
+
model: 'fixture-model', dimensions: 384, builtUtc: '2026-08-21T12:00:00.000Z' };
|
|
81
|
+
}
|
|
82
|
+
const publicLedgerBytes = Buffer.from(`${JSON.stringify(publicLedger)}\n`);
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'RVF-GENERATIONS.json'), `${JSON.stringify({ ...publicLedger,
|
|
84
|
+
kind: 'ruvnet-brain-runtime-generation-ledger' })}\n`);
|
|
85
|
+
fs.writeFileSync(path.join(dir, 'PUBLIC-RVF-GENERATIONS.json'), publicLedgerBytes);
|
|
86
|
+
fs.writeFileSync(path.join(dir, 'PRIVATE-STORES.json'), JSON.stringify({ privateStores: [] }));
|
|
87
|
+
fs.writeFileSync(path.join(dir, 'public-store-classes.json'), JSON.stringify({ schemaVersion: 1, derived: [] }));
|
|
88
|
+
const rows = storeNames.map((name) => ({ key: `repo:${name}`, kind: 'repository', name,
|
|
89
|
+
url: `https://github.com/ruvnet/${name}`, status: 'CURRENT', disposition: 'eligible', upstream: {},
|
|
90
|
+
artifact: { store: name }, reasons: [] }));
|
|
91
|
+
const enumerationReceipt = { schemaVersion: 1, terminal: true, duplicateKeys: 0,
|
|
92
|
+
repositories: { expected: rows.length, pages: [] }, gists: { expected: 0, pages: [] } };
|
|
93
|
+
const generatorSourceSha = 'a'.repeat(64);
|
|
94
|
+
const snapshotRoot = 'b'.repeat(64);
|
|
95
|
+
const sourceObservationSha256 = 'c'.repeat(64);
|
|
96
|
+
const policy = { policyDispositionDigests: [], exemptionDigests: [] };
|
|
97
|
+
const corpus = { schemaVersion: 1, kind: 'ruvnet-brain-corpus-coverage', generatorSourceSha,
|
|
98
|
+
snapshotRoot, sourceObservationSha256, rows, enumerationReceipt, policy,
|
|
99
|
+
totals: { rows: rows.length, repositories: rows.length, gists: 0, byStatus: { CURRENT: rows.length } } };
|
|
100
|
+
corpus.coverageGeneration = coverageGenerationFor({ generatorSourceSha, snapshotRoot,
|
|
101
|
+
sourceObservationSha256, rows, enumerationReceipt, policyDispositionDigests: [], exemptionDigests: [] });
|
|
102
|
+
const corpusBytes = `${JSON.stringify(corpus, null, 2)}\n`;
|
|
103
|
+
fs.writeFileSync(path.join(dir, 'CORPUS-COVERAGE.json'), corpusBytes);
|
|
104
|
+
const publicInventory = validatePublicInventory({ assetsDir: dir, coverage: corpus, ledger: publicLedger });
|
|
105
|
+
const release = { ...structuredClone(corpus), kind: 'ruvnet-brain-release-coverage',
|
|
106
|
+
releaseIdentity: { version: source.brainVersion, tag: `v${source.brainVersion}`, sourceSnapshot },
|
|
107
|
+
corpusSeed: { tag: `corpus-sha256-${'e'.repeat(64)}`, archiveSha256: 'e'.repeat(64), archiveBytes: 1,
|
|
108
|
+
receiptSha256: 'f'.repeat(64) },
|
|
109
|
+
corpusCoverage: { file: 'CORPUS-COVERAGE.json', sha256: crypto.createHash('sha256').update(corpusBytes).digest('hex'),
|
|
110
|
+
coverageGeneration: corpus.coverageGeneration },
|
|
111
|
+
generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: crypto.createHash('sha256').update(publicLedgerBytes).digest('hex'),
|
|
112
|
+
bytes: publicLedgerBytes.length, storeCount: storeNames.length },
|
|
113
|
+
publicInventoryPartitionSha256: publicInventory.partitionSha256,
|
|
114
|
+
installedProjectionSchema: 2 };
|
|
115
|
+
delete release.coverageGeneration;
|
|
116
|
+
release.releaseCoverageGeneration = releaseCoverageGenerationFor(release);
|
|
117
|
+
fs.writeFileSync(path.join(dir, 'COVERAGE.json'), JSON.stringify(release));
|
|
118
|
+
const validation = validateCoverageDirectory(dir, { expectedVersion: source.brainVersion });
|
|
119
|
+
if (!validation.valid) throw new Error(validation.failures.join('; '));
|
|
120
|
+
// Deliberately corrupt the validated fixture only for the early integrity-failure scenario.
|
|
121
|
+
if (NO_RVF) fs.rmSync(path.join(dir, 'fixture.big.rvf'));
|
|
65
122
|
}
|
|
66
123
|
|
|
67
124
|
console.log(`[build-fixture-kb] staged ${OUT} (mcp: ${!DROP_MCP}, rvf: ${!NO_RVF})`);
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// `--doctor --hooks` again against the SAME installed surface and assert the battery goes RED.
|
|
5
|
-
// Proves the hook-FIRE assertion is real (a genuine watchdog catch), not manifest-present —
|
|
6
|
-
// tests/integration/install-smoke.mjs's own `--doctor --hooks goes RED when a registered hook
|
|
7
|
-
// sleeps past its declared timeout` test is the fast local rehearsal of this exact mechanism
|
|
8
|
-
// against a synthetic surface; this runs it against the REAL packed-and-installed plugin tree a
|
|
9
|
-
// stranger's machine actually has, right after the `healthy` scenario's install.
|
|
10
|
-
//
|
|
11
|
-
// node scripts/ci/mutate-hook-timeout.mjs --installed <dir> --home <dir>
|
|
2
|
+
// Legacy filename: verify forbidden automatic registration is refused without executing its body.
|
|
3
|
+
// Uses the actual installed package manifest and doctor, not a synthetic status callback.
|
|
12
4
|
import fs from 'node:fs';
|
|
13
5
|
import path from 'node:path';
|
|
14
6
|
import { spawnSync } from 'node:child_process';
|
|
15
|
-
import { fileURLToPath } from 'node:url';
|
|
16
7
|
|
|
17
|
-
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
18
8
|
const argv = process.argv.slice(2);
|
|
19
9
|
const arg = (flag) => { const i = argv.indexOf(flag); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; };
|
|
20
10
|
const INSTALLED = arg('--installed');
|
|
@@ -25,46 +15,33 @@ if (!INSTALLED || !HOME_DIR) {
|
|
|
25
15
|
process.exit(2);
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
const pluginRoot = path.join(
|
|
18
|
+
const pluginRoot = path.join(INSTALLED, 'plugin');
|
|
29
19
|
const hooksFile = path.join(pluginRoot, 'hooks', 'hooks.json');
|
|
30
20
|
if (!fs.existsSync(hooksFile)) {
|
|
31
21
|
console.error(`[mutate-hook-timeout] FAIL: no installed hooks.json at ${hooksFile} — run the healthy scenario first`);
|
|
32
22
|
process.exit(1);
|
|
33
23
|
}
|
|
34
24
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const doc = JSON.parse(fs.readFileSync(hooksFile, 'utf8'));
|
|
25
|
+
// Legacy filename retained. The current invariant rejects registration without running its body.
|
|
26
|
+
const original = fs.readFileSync(hooksFile);
|
|
27
|
+
const marker = path.join(HOME_DIR, 'forbidden-hook-executed');
|
|
28
|
+
const body = path.join(pluginRoot, 'scripts', 'ci-forbidden-hook.mjs');
|
|
29
|
+
fs.writeFileSync(body, `import fs from 'node:fs'; fs.writeFileSync(${JSON.stringify(marker)}, 'executed');`);
|
|
30
|
+
const doc = JSON.parse(original);
|
|
44
31
|
doc.hooks ??= {};
|
|
45
|
-
doc.hooks.UserPromptSubmit
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (r.stderr) console.error(r.stderr);
|
|
61
|
-
|
|
62
|
-
if (r.status === 0) {
|
|
63
|
-
console.error('[mutate-hook-timeout] FAIL: expected --doctor --hooks to go RED with a sleeping hook registered, got exit 0');
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
if (!/\bhang\b/.test(r.stdout || '')) {
|
|
67
|
-
console.error('[mutate-hook-timeout] FAIL: exited non-zero, but no "hang" violation was named — some OTHER check may have failed instead');
|
|
68
|
-
process.exit(1);
|
|
32
|
+
doc.hooks.UserPromptSubmit = [{ hooks: [{ type: 'command', command: 'node "${CLAUDE_PLUGIN_ROOT}/scripts/ci-forbidden-hook.mjs"', timeout: 1 }] }];
|
|
33
|
+
fs.writeFileSync(hooksFile, JSON.stringify(doc));
|
|
34
|
+
try {
|
|
35
|
+
const r = spawnSync(process.execPath, [path.join(INSTALLED, 'bin', 'install.mjs'), '--doctor', '--hooks'], {
|
|
36
|
+
env: { ...process.env, HOME: HOME_DIR, USERPROFILE: HOME_DIR, RUVNET_BRAIN_TEST: '1' },
|
|
37
|
+
input: '', encoding: 'utf8', timeout: 120000,
|
|
38
|
+
});
|
|
39
|
+
console.log(r.stdout);
|
|
40
|
+
if (r.stderr) console.error(r.stderr);
|
|
41
|
+
if (r.status !== 1 || !/retired registry must not declare event UserPromptSubmit/.test(r.stdout || '')
|
|
42
|
+
|| fs.existsSync(marker)) throw new Error('doctor did not reject the forbidden installed registration without executing it');
|
|
43
|
+
console.log('[mutate-hook-timeout] PASS — forbidden registration refused; its body did not execute');
|
|
44
|
+
} finally {
|
|
45
|
+
fs.writeFileSync(hooksFile, original);
|
|
46
|
+
fs.rmSync(body, { force: true });
|
|
69
47
|
}
|
|
70
|
-
console.log(`[mutate-hook-timeout] PASS — the battery went RED (exit ${r.status}) with a real "hang" violation`);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SCENARIOS:
|
|
15
15
|
// healthy — a complete fixture bundle. Asserts exit 0, then runs --doctor --hooks and
|
|
16
|
-
// asserts
|
|
16
|
+
// asserts automatic hooks are absent from the INSTALLED registries, and
|
|
17
17
|
// that no author-local ~/.claude/settings.json exists in this virgin image.
|
|
18
18
|
// seeded-broken — forge-mcp-all.mjs deleted from the fixture (M-D8a). Asserts exit NON-ZERO.
|
|
19
19
|
// strict-ungrounded — a healthy-shaped fixture (already never ships forge-ask-all.mjs — see
|
|
@@ -29,7 +29,7 @@ import fs from 'node:fs';
|
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import os from 'node:os';
|
|
31
31
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
32
|
-
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
33
|
import { selfCheckOuterTimeoutMs } from './stranger-timeout.mjs';
|
|
34
34
|
import { stageLocalBundle } from './stranger-fixture-stage.mjs';
|
|
35
35
|
|
|
@@ -83,7 +83,7 @@ function buildKbFixture({ dropMcp, noRvf }) {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Marketplace-clone-shaped plugin surface, the REAL plugin/ tree at the candidate SHA — never a
|
|
86
|
-
* synthetic fixture — so
|
|
86
|
+
* synthetic fixture — so retirement is checked against the real installed registry.
|
|
87
87
|
*/
|
|
88
88
|
function seedPluginSurface() {
|
|
89
89
|
const dest = path.join(HOME_DIR, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin');
|
|
@@ -181,33 +181,18 @@ if (SCENARIO === 'healthy') {
|
|
|
181
181
|
console.log(doctor.stdout);
|
|
182
182
|
if (doctor.stderr) console.error(doctor.stderr);
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// the install-time smoke stays non-fatal (an air-gapped machine is not a broken install), but
|
|
190
|
-
// the verdict must stop EVAPORATING — it persists to install-state.json and --doctor gates on
|
|
191
|
-
// it until a real cited answer clears it.
|
|
192
|
-
//
|
|
193
|
-
// Every CI image is offline with no model weights, so grounding is ALWAYS unproven here and
|
|
194
|
-
// `--doctor` will ALWAYS exit 1. Asserting exit 0 asserted that D8's own design does not work.
|
|
195
|
-
//
|
|
196
|
-
// So the assertion binds to the SUBSTANCE: the battery must report zero contract violations, and
|
|
197
|
-
// any non-zero exit must be attributable ONLY to the grounding verdict. A doctor that fails for a
|
|
198
|
-
// hook violation still fails this cell — which is the property that was actually wanted.
|
|
184
|
+
const installedApi = await import(pathToFileURL(path.join(INSTALLED, 'bin', 'install.mjs')).href);
|
|
185
|
+
const retirement = installedApi.automaticHookRetirementStatus(INSTALLED, { scope: 'installed' });
|
|
186
|
+
const installedClaude = installedApi.claudeInstalledHookRetirementStatus({ home: HOME_DIR,
|
|
187
|
+
plugin: { managed: true, installed: true, installPath: path.join(HOME_DIR, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin') } });
|
|
188
|
+
if (!retirement.ok || !installedClaude.ok) fail(`automatic hook retirement failed: ${JSON.stringify({ retirement, installedClaude })}`);
|
|
199
189
|
const out = `${doctor.stdout || ''}${doctor.stderr || ''}`;
|
|
200
|
-
|
|
201
|
-
const onlyGrounding = /Grounding UNPROVEN/.test(out) && !/
|
|
202
|
-
if (doctor.status !== 0 && !(
|
|
203
|
-
fail(
|
|
190
|
+
if (!/automatic Brain hook retirement: 0 registration\(s\), 0 manifest error\(s\)/.test(out)) fail('doctor did not verify automatic hook retirement');
|
|
191
|
+
const onlyGrounding = /Grounding UNPROVEN/.test(out) && !/automatic hook retirement failed/i.test(out);
|
|
192
|
+
if (doctor.status !== 0 && !(doctor.status === 1 && onlyGrounding)) {
|
|
193
|
+
fail(`doctor failed beyond unproven grounding: ${describeExit(doctor)}`);
|
|
204
194
|
}
|
|
205
|
-
|
|
206
|
-
const firingsMatch = /registrations from marketplace-clone,\s*\d+\s*stdin regimes each\s*\((\d+)\s*firings\)/.exec(doctor.stdout || '');
|
|
207
|
-
if (!firingsMatch) fail('--doctor --hooks output did not name the marketplace-clone registration/firing count at all');
|
|
208
|
-
const firings = Number(firingsMatch[1]);
|
|
209
|
-
if (!(firings > 0)) fail(`expected at least one hook FIRING through the installed registration, got ${firings}`);
|
|
210
|
-
log(`OK — ${firings} real hook firing(s) through the installed marketplace-clone registration`);
|
|
195
|
+
log(`OK — installed automatic hooks retired across ${retirement.files.length} package surfaces and the actual Claude registry`);
|
|
211
196
|
|
|
212
197
|
if (fs.existsSync(authorSettings)) fail('installer must never create an author-local settings.json in a virgin image');
|
|
213
198
|
log('OK — no author-local ~/.claude/settings.json in this virgin image');
|