ruvnet-brain 4.0.1 → 4.0.2
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/.claude-plugin/marketplace.json +1 -0
- package/README.md +4 -4
- package/bin/install.mjs +100 -5
- package/console/CONTRACT.md +172 -0
- package/console/activity.js +753 -0
- package/console/app.js +4189 -0
- package/console/architecture.html +1221 -0
- package/console/assets/depth-1.webp +0 -0
- package/console/assets/depth-2.webp +0 -0
- package/console/assets/depth-3.webp +0 -0
- package/console/assets/harness-vs-plain.svg +259 -0
- package/console/assets/hero.webp +0 -0
- package/console/assets/memory.webp +0 -0
- package/console/assets/metaharness.svg +247 -0
- package/console/index.html +777 -0
- package/console/install-architecture.html +162 -0
- package/console/install-mockup.html +543 -0
- package/console/style.css +2144 -0
- package/console/tips.css +926 -0
- package/console/tips.html +858 -0
- package/console/tips.js +128 -0
- package/docs/RELEASE-NOTES-4.0.md +88 -0
- package/kb/model-requirements.mjs +37 -6
- package/keys/ruvnet-brain-signing.pub.pem +3 -0
- package/package.json +8 -22
- package/plugin/.claude-plugin/marketplace.json +1 -0
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +2 -2
- package/plugin/commands/configure.md +3 -2
- package/plugin/commands/rvbc.md +4 -3
- package/plugin/commands/rvcb.md +2 -2
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +21 -0
- package/plugin/scripts/detach.mjs +14 -0
- package/plugin/scripts/first-session-worker.mjs +38 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -6
- package/plugin/scripts/hook-shim.mjs +7 -7
- package/plugin/scripts/learn-capture.sh +22 -3
- package/plugin/scripts/learn-flush.mjs +21 -4
- package/plugin/scripts/runtime-preferences.mjs +269 -0
- package/plugin/scripts/session-start-core.mjs +477 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +81 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/scripts/adr-backfill.mjs +107 -0
- package/scripts/advocacy-outcomes.mjs +808 -0
- package/scripts/agentdb-context.mjs +216 -0
- package/scripts/agentdb-fleet-doctor.mjs +101 -0
- package/scripts/ascii-drift.mjs +236 -0
- package/scripts/behavioral-l1-l4.mjs +210 -0
- package/scripts/brain-capability-check.mjs +72 -0
- package/scripts/brain-grade-groundtruth.mjs +100 -0
- package/scripts/brain-latency-50.mjs +227 -0
- package/scripts/brain-novice-50.mjs +189 -0
- package/scripts/brain-stamp.mjs +94 -0
- package/scripts/brain-state.mjs +212 -0
- package/scripts/build-bundle.mjs +522 -0
- package/scripts/build-concepts.mjs +132 -0
- package/scripts/build-l2.mjs +71 -0
- package/scripts/build-primer.mjs +73 -0
- package/scripts/build-symbols.mjs +68 -0
- package/scripts/calibrate-router.mjs +97 -0
- package/scripts/capability-audit.mjs +321 -0
- package/scripts/capability-registry.mjs +876 -0
- package/scripts/check-indexation.mjs +108 -0
- package/scripts/check-legibility.mjs +189 -0
- package/scripts/ci/build-fixture-kb.mjs +67 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
- package/scripts/ci/learning-replay-recorder.mjs +59 -0
- package/scripts/ci/mutate-hook-timeout.mjs +70 -0
- package/scripts/ci/stranger-fixture-stage.mjs +17 -0
- package/scripts/ci/stranger-scenario.mjs +228 -0
- package/scripts/ci/stranger-timeout.mjs +25 -0
- package/scripts/ci-verdict.mjs +29 -0
- package/scripts/claims-verify.mjs +710 -0
- package/scripts/clear-claude-tmp.sh +31 -0
- package/scripts/console-engine.mjs +434 -0
- package/scripts/console-engine.test.mjs +125 -0
- package/scripts/corpus-qa.mjs +250 -0
- package/scripts/correction-detect-embed.mjs +346 -0
- package/scripts/correction-detect-measure.mjs +270 -0
- package/scripts/correction-detect.mjs +686 -0
- package/scripts/count-chunks.mjs +54 -0
- package/scripts/described-questions.json +30 -0
- package/scripts/design-grade.mjs +58 -0
- package/scripts/dev-plugin-link.sh +105 -0
- package/scripts/distill-project.mjs +200 -0
- package/scripts/doc-currency.mjs +801 -0
- package/scripts/eval-brain.mjs +244 -0
- package/scripts/fix-metaharness-memretrieve.mjs +121 -0
- package/scripts/full-hints.mjs +87 -0
- package/scripts/gate.sh +39 -0
- package/scripts/gates.mjs +146 -0
- package/scripts/gen-console-images.mjs +54 -0
- package/scripts/gen-images.mjs +47 -0
- package/scripts/git-clone-refresh.mjs +52 -0
- package/scripts/git-hooks/pre-push +126 -0
- package/scripts/goal-match.mjs +398 -0
- package/scripts/goldie-research.mjs +223 -0
- package/scripts/goldie-weekly.sh +67 -0
- package/scripts/health-repair.mjs +250 -0
- package/scripts/helix-scenario-questions.json +10 -0
- package/scripts/ingest-gists.mjs +230 -0
- package/scripts/ingest-meeting.mjs +115 -0
- package/scripts/ingest-repo.mjs +79 -0
- package/scripts/install-npx-witness.sh +49 -0
- package/scripts/issue-fix.mjs +639 -0
- package/scripts/issue-watch.mjs +276 -0
- package/scripts/issue4-close-note.md +31 -0
- package/scripts/key-canary.mjs +91 -0
- package/scripts/latency-to-surface.mjs +233 -0
- package/scripts/learning-enable.mjs +380 -0
- package/scripts/learning-replay.mjs +1570 -0
- package/scripts/learnings.mjs +62 -0
- package/scripts/lesson-gate.mjs +680 -0
- package/scripts/lesson-lifecycle.mjs +449 -0
- package/scripts/lesson-promote.mjs +262 -0
- package/scripts/lesson-ratify.mjs +98 -0
- package/scripts/lesson-seed.mjs +252 -0
- package/scripts/lesson-store.mjs +447 -0
- package/scripts/loop-checkpoint.mjs +86 -0
- package/scripts/memdb-health.sh +14 -0
- package/scripts/memory-doctor.mjs +271 -0
- package/scripts/model-catalog.mjs +79 -0
- package/scripts/nightly-controller.mjs +66 -0
- package/scripts/nightly-gists.sh +72 -0
- package/scripts/nightly-wrapper.sh +180 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2749 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/prove.mjs +95 -0
- package/scripts/proxy/claude-proxied.sh +57 -0
- package/scripts/proxy/proxy-revert.sh +59 -0
- package/scripts/proxy/proxy-up.sh +60 -0
- package/scripts/proxy/proxy-verify.mjs +142 -0
- package/scripts/published-surface-probe.mjs +241 -0
- package/scripts/qe/card-lane-gate.mjs +162 -0
- package/scripts/qe/session-start-gate.mjs +229 -0
- package/scripts/qe/ux-suite.mjs +323 -0
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/record-lesson.mjs +113 -0
- package/scripts/refresh-model-catalog.mjs +99 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +395 -0
- package/scripts/remedy-registry.mjs +247 -0
- package/scripts/rerank-cap-eval.mjs +265 -0
- package/scripts/rerank-cap-warm-ab.mjs +129 -0
- package/scripts/route-cheap.mjs +20 -15
- package/scripts/router-utilization.mjs +182 -0
- package/scripts/routing-flywheel.mjs +596 -0
- package/scripts/rvf-generation.mjs +104 -0
- package/scripts/rvf-index-audit.mjs +138 -0
- package/scripts/self-update.mjs +508 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stack-sync.mjs +469 -0
- package/scripts/stamp-existing-rvf-generations.mjs +53 -0
- package/scripts/stamp-sweep.mjs +144 -0
- package/scripts/status-honesty.mjs +102 -0
- package/scripts/sync-version.mjs +217 -0
- package/scripts/token-report.mjs +102 -0
- package/scripts/top100-benchmark.mjs +479 -0
- package/scripts/top100-corpus.mjs +112 -0
- package/scripts/top100-semantic-assertions.mjs +449 -0
- package/scripts/update-apply.mjs +9 -0
- package/scripts/upgrade-notice.mjs +14 -0
- package/scripts/verify-bundle.mjs +51 -0
- package/scripts/verify-channels.mjs +184 -0
- package/scripts/verify-model-catalog.mjs +104 -0
- package/scripts/verify-nightly-close-issue4.sh +31 -0
- package/scripts/version.mjs +40 -0
- package/scripts/wired-check.mjs +864 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
|
@@ -10,8 +10,10 @@ Treat `/rvbc`, `/rvcb`, `/brain-console`, and `/ruvnet-brain:configure` as equal
|
|
|
10
10
|
Never correct the user's spelling.
|
|
11
11
|
|
|
12
12
|
1. Say one short sentence: "Opening it now; it scans live while you watch."
|
|
13
|
-
2.
|
|
14
|
-
|
|
13
|
+
2. Resolve the installed runtime at
|
|
14
|
+
`${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
15
|
+
A current-repository `scripts/onboarding-console.mjs` is allowed only for an explicit developer
|
|
16
|
+
checkout. Never fall back to a guessed `~/Code` path.
|
|
15
17
|
3. Run `node <resolved-script> --serve --open` in the background.
|
|
16
18
|
4. Give the URL immediately. Do not promise a duration; the page reports its own scan progress.
|
|
17
19
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release-proof
|
|
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 open issues, exact-SHA GitHub success, nonzero no-skip QE, packed-artifact host tests, installed Brain/RVF proof, independent graders, and post-publication byte verification.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release Proof
|
|
7
|
+
|
|
8
|
+
Treat release as a two-seal transaction. Never publish from a source checkout merely because its
|
|
9
|
+
tests pass. Never turn `UNKNOWN`, `SKIP`, `todo`, `0 tests`, dirty state, or an agent report into
|
|
10
|
+
green.
|
|
11
|
+
|
|
12
|
+
## Non-bypassable rules
|
|
13
|
+
|
|
14
|
+
1. Use the exact source SHA and one packed-artifact SHA-256 everywhere.
|
|
15
|
+
2. Require zero open GitHub issues for RuvNet Brain. A local fix is not a closed issue.
|
|
16
|
+
3. Require every named GitHub workflow to complete successfully on the exact candidate SHA.
|
|
17
|
+
4. Reject any test/QE result with zero tests, skips, todos, unknowns, pending jobs, or failures.
|
|
18
|
+
5. Require two distinct independent graders scoring at least 95, each bound to the SHA and digest.
|
|
19
|
+
6. Install the sealed artifact into virgin Claude Code and Codex homes; test their real entrypoints.
|
|
20
|
+
7. Require the active Brain registry to contain the `ruvnet-brain` RVF store and require narrow,
|
|
21
|
+
broad, and concurrent cited searches to complete within 80% of their deadline.
|
|
22
|
+
8. Publish only through the protected release workflow. Never run `npm publish` or `gh release
|
|
23
|
+
create` locally.
|
|
24
|
+
9. After publication, download npm and GitHub artifacts, compare their bytes with the seal, install
|
|
25
|
+
both hosts again, query the active MCP again, and require `published-surface-probe` green.
|
|
26
|
+
10. Close an issue only after posting its acceptance evidence. Never close from source inspection.
|
|
27
|
+
|
|
28
|
+
## Candidate seal
|
|
29
|
+
|
|
30
|
+
Generate the receipt from commands in the protected candidate workflow. Do not hand-author it.
|
|
31
|
+
Validate it from the repository with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node scripts/release-proof.mjs --candidate release-evidence/candidate-receipt.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
From an installed Claude plugin, run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node "$CLAUDE_PLUGIN_ROOT/skills/release-proof/scripts/release-proof.mjs" \
|
|
41
|
+
--candidate release-evidence/candidate-receipt.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Exit 0 is the only candidate seal. Read every failure code; repair the system, regenerate evidence,
|
|
45
|
+
and rerun. Do not edit the receipt to remove a failure.
|
|
46
|
+
|
|
47
|
+
## Publication seal
|
|
48
|
+
|
|
49
|
+
After the protected publisher completes, validate both receipts:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node scripts/release-proof.mjs \
|
|
53
|
+
--candidate release-evidence/candidate-receipt.json \
|
|
54
|
+
--publication release-evidence/publication-receipt.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Only exit 0 permits “shipped,” “deployed,” “green,” or “ready.” If publication occurred but this
|
|
58
|
+
seal fails, say `PUBLICATION DEGRADED`, preserve the previous known-good release, and repair or
|
|
59
|
+
roll back through the release workflow.
|
|
60
|
+
|
|
61
|
+
## Evidence and issue handling
|
|
62
|
+
|
|
63
|
+
For each issue:
|
|
64
|
+
|
|
65
|
+
1. Reproduce the original symptom against the old/public artifact.
|
|
66
|
+
2. Run its acceptance criteria against the sealed candidate.
|
|
67
|
+
3. Disable or mutate the fix; the regression must fail.
|
|
68
|
+
4. Post SHA, artifact digest, commands, results, and untested limits to the issue.
|
|
69
|
+
5. Close only after the GitHub evidence is visible and exact-SHA required checks are green.
|
|
70
|
+
|
|
71
|
+
Read [references/receipt-contract.md](references/receipt-contract.md) for receipt fields and failure
|
|
72
|
+
semantics. Store the protocol and final release receipt in Ruflo/AgentDB only after the publication
|
|
73
|
+
seal passes.
|
|
74
|
+
|
|
75
|
+
## Status language
|
|
76
|
+
|
|
77
|
+
- Candidate seal absent or failed: `NOT READY`.
|
|
78
|
+
- Candidate sealed, not published: `SEALED, NOT SHIPPED`.
|
|
79
|
+
- Published, publication seal pending: `PUBLISHED, NOT VERIFIED`.
|
|
80
|
+
- Publication seal failed: `PUBLICATION DEGRADED`.
|
|
81
|
+
- Both seals exit 0: `SHIPPED AND VERIFIED`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Receipt contract
|
|
2
|
+
|
|
3
|
+
The authority accepts schema version 1 JSON. Receipts are append-only evidence artifacts generated
|
|
4
|
+
by protected workflows, never editable status documents.
|
|
5
|
+
|
|
6
|
+
## Candidate receipt
|
|
7
|
+
|
|
8
|
+
Required bindings:
|
|
9
|
+
|
|
10
|
+
- `sha`, `tree`, `dirty:false`
|
|
11
|
+
- `artifact.path`, `artifact.sha256`, `artifact.sourceSha`
|
|
12
|
+
- exact-SHA release-vector verdict with zero unknown/skipped
|
|
13
|
+
- aggregate tests with nonzero total, all passed, zero failed/skipped/todo
|
|
14
|
+
- fresh coverage floor and zero critical/high security findings
|
|
15
|
+
- zero open GitHub issues
|
|
16
|
+
- required GitHub workflow results on the same SHA
|
|
17
|
+
- virgin-home Claude and Codex results on the same artifact digest
|
|
18
|
+
- installed Brain self-RVF plus narrow, broad, and concurrent cited search timings
|
|
19
|
+
- nonzero Agentic QE totals with zero failed/skipped
|
|
20
|
+
- two distinct independent grader receipts at 95 or higher, bound to SHA and digest
|
|
21
|
+
|
|
22
|
+
## Publication receipt
|
|
23
|
+
|
|
24
|
+
Required bindings:
|
|
25
|
+
|
|
26
|
+
- candidate SHA and artifact digest
|
|
27
|
+
- npm and GitHub release bytes matching the candidate digest
|
|
28
|
+
- clean installed Claude and Codex results from the public package
|
|
29
|
+
- installed Brain self-RVF and broad search within 80 percent of deadline
|
|
30
|
+
- successful exact-SHA `published-surface-probe`
|
|
31
|
+
|
|
32
|
+
## Failure semantics
|
|
33
|
+
|
|
34
|
+
Any missing field, malformed digest, mismatched SHA, dirty tree, open issue, absent/pending/red
|
|
35
|
+
workflow, skipped/todo/zero-test result, missing RVF store, uncited search, deadline-margin breach,
|
|
36
|
+
low/missing grader, or public byte mismatch is `FAIL`. There is no warning state and no score
|
|
37
|
+
average. The authority never publishes; publication belongs to the protected workflow after the
|
|
38
|
+
candidate seal.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fail-closed release authority. It validates evidence bound to one clean source SHA and one
|
|
3
|
+
// packed-artifact digest. It never publishes and never converts UNKNOWN/SKIP/zero work into PASS.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_CHECKS = ['ci', 'integration-linux', 'stranger-matrix', 'ux-qe'];
|
|
10
|
+
|
|
11
|
+
const fail = (code, detail) => ({ code, detail });
|
|
12
|
+
const cleanHex = (value, length) => new RegExp(`^[a-f0-9]{${length}}$`, 'i').test(String(value || ''));
|
|
13
|
+
const digestOf = (receipt) => String(receipt?.artifact?.sha256 || '').replace(/^sha256:/, '');
|
|
14
|
+
|
|
15
|
+
export function evaluateCandidateReceipt(receipt) {
|
|
16
|
+
const failures = [];
|
|
17
|
+
const sha = String(receipt?.sha || '');
|
|
18
|
+
const digest = digestOf(receipt);
|
|
19
|
+
|
|
20
|
+
if (receipt?.schemaVersion !== 1 || receipt?.phase !== 'candidate') failures.push(fail('INVALID_RECEIPT', 'schemaVersion=1 and phase=candidate are required'));
|
|
21
|
+
if (!cleanHex(sha, 40) || !cleanHex(receipt?.tree, 40)) failures.push(fail('INVALID_LINEAGE', 'candidate SHA and tree must be full git object ids'));
|
|
22
|
+
if (receipt?.dirty !== false) failures.push(fail('DIRTY_WORKTREE', 'release candidates must come from a clean worktree'));
|
|
23
|
+
if (!cleanHex(digest, 64)) failures.push(fail('INVALID_ARTIFACT_DIGEST', 'artifact SHA-256 is missing or malformed'));
|
|
24
|
+
if (receipt?.artifact?.sourceSha !== sha) failures.push(fail('ARTIFACT_SOURCE_MISMATCH', 'packed artifact is not bound to the candidate SHA'));
|
|
25
|
+
|
|
26
|
+
const vector = receipt?.releaseVector || {};
|
|
27
|
+
if (vector.verdict !== 'PASS' || vector.sha !== sha || vector.unknown !== 0 || vector.skipped !== 0) {
|
|
28
|
+
failures.push(fail('RELEASE_VECTOR_NOT_PASS', 'release vector must PASS on the exact SHA with zero UNKNOWN/SKIP'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tests = receipt?.tests || {};
|
|
32
|
+
if (!(tests.total > 0)) failures.push(fail('ZERO_TESTS', 'a zero-test run is never evidence'));
|
|
33
|
+
if (tests.failed !== 0 || tests.skipped !== 0 || tests.todo !== 0 || tests.passed !== tests.total) {
|
|
34
|
+
failures.push(fail('TESTS_NOT_ALL_EXECUTED', 'all discovered tests must execute and pass; skipped/todo are release failures'));
|
|
35
|
+
}
|
|
36
|
+
const coverage = receipt?.coverage || {};
|
|
37
|
+
if (coverage.status !== 'PASS' || !(coverage.lines >= coverage.requiredLines)) failures.push(fail('COVERAGE_NOT_PASS', 'fresh exact-SHA coverage must meet the floor'));
|
|
38
|
+
const security = receipt?.security || {};
|
|
39
|
+
if (security.status !== 'PASS' || security.critical !== 0 || security.high !== 0) failures.push(fail('SECURITY_NOT_PASS', 'security must pass with zero critical/high findings'));
|
|
40
|
+
|
|
41
|
+
const openIssues = Array.isArray(receipt?.issues?.open) ? receipt.issues.open : null;
|
|
42
|
+
if (openIssues === null || openIssues.length > 0) failures.push(fail('OPEN_ISSUES', openIssues === null ? 'open-issue evidence missing' : `${openIssues.length} issue(s) remain open`));
|
|
43
|
+
|
|
44
|
+
if (receipt?.github?.sha !== sha) failures.push(fail('GITHUB_SHA_MISMATCH', 'GitHub evidence is not bound to candidate SHA'));
|
|
45
|
+
const checks = Array.isArray(receipt?.github?.checks) ? receipt.github.checks : [];
|
|
46
|
+
for (const name of REQUIRED_CHECKS) {
|
|
47
|
+
const matches = checks.filter((check) => check?.name === name);
|
|
48
|
+
if (matches.length === 0) failures.push(fail('REQUIRED_CHECK_MISSING', `${name} has no exact-SHA result`));
|
|
49
|
+
else if (!matches.some((check) => check.status === 'completed' && check.conclusion === 'success')) failures.push(fail('REQUIRED_CHECK_NOT_GREEN', `${name} is not completed/success`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const hostName of ['claude', 'codex']) {
|
|
53
|
+
const host = receipt?.hosts?.[hostName];
|
|
54
|
+
if (host?.status !== 'PASS') failures.push(fail('HOST_NOT_PASS', `${hostName} clean-install acceptance did not pass`));
|
|
55
|
+
if (host?.artifactSha256 !== digest) failures.push(fail('HOST_ARTIFACT_MISMATCH', `${hostName} did not install the sealed artifact`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const brain = receipt?.brain || {};
|
|
59
|
+
if (brain.status !== 'PASS') failures.push(fail('BRAIN_NOT_PASS', 'installed MCP search acceptance did not pass'));
|
|
60
|
+
if (brain.selfStore !== true) failures.push(fail('BRAIN_SELF_STORE_MISSING', 'active RVF registry does not contain ruvnet-brain'));
|
|
61
|
+
if (brain.citedSelfSource !== true) failures.push(fail('BRAIN_SELF_CITATION_MISSING', 'installed Brain did not cite its own source'));
|
|
62
|
+
const deadline = Number(brain.deadlineMs);
|
|
63
|
+
const observed = Math.max(Number(brain.narrowMs), Number(brain.broadMs), Number(brain.concurrentMs));
|
|
64
|
+
if (!(deadline > 0) || !Number.isFinite(observed) || observed > deadline * 0.8) failures.push(fail('BRAIN_DEADLINE_MARGIN', 'searches must finish within 80% of deadline'));
|
|
65
|
+
|
|
66
|
+
const qe = receipt?.qe || {};
|
|
67
|
+
if (!(qe.total > 0)) failures.push(fail('QE_ZERO_TESTS', 'Agentic QE executed zero tests'));
|
|
68
|
+
if (qe.status !== 'PASS' || qe.failed !== 0 || qe.skipped !== 0 || qe.passed !== qe.total) failures.push(fail('QE_NOT_PASS', 'Agentic QE must run a nonzero fleet with zero failed/skipped tests'));
|
|
69
|
+
|
|
70
|
+
const graders = Array.isArray(receipt?.graders) ? receipt.graders : [];
|
|
71
|
+
const independent = graders.filter((grader) => grader?.independent === true);
|
|
72
|
+
if (independent.length < 2 || new Set(independent.map((grader) => grader.id)).size < 2) failures.push(fail('INDEPENDENT_GRADERS', 'two distinct independent graders are required'));
|
|
73
|
+
for (const grader of independent) {
|
|
74
|
+
if (!(grader.score >= 95)) failures.push(fail('GRADER_BELOW_95', `${grader.id || 'grader'} scored ${grader.score ?? 'UNKNOWN'}`));
|
|
75
|
+
if (grader.sha !== sha || grader.artifactSha256 !== digest) failures.push(fail('GRADER_BINDING_MISMATCH', `${grader.id || 'grader'} is not bound to the seal`));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { verdict: failures.length === 0 ? 'PASS' : 'FAIL', sha, artifactSha256: digest, failures };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function evaluatePublicationReceipt(candidate, publication) {
|
|
82
|
+
const candidateResult = evaluateCandidateReceipt(candidate);
|
|
83
|
+
const failures = [...candidateResult.failures];
|
|
84
|
+
const sha = candidateResult.sha;
|
|
85
|
+
const digest = candidateResult.artifactSha256;
|
|
86
|
+
if (publication?.schemaVersion !== 1 || publication?.phase !== 'publication') failures.push(fail('INVALID_PUBLICATION_RECEIPT', 'schemaVersion=1 and phase=publication are required'));
|
|
87
|
+
if (publication?.sha !== sha || publication?.artifactSha256 !== digest) failures.push(fail('PUBLICATION_SEAL_MISMATCH', 'publication does not reference candidate seal'));
|
|
88
|
+
for (const surface of ['npm', 'githubRelease']) {
|
|
89
|
+
const item = publication?.[surface];
|
|
90
|
+
if (item?.sha !== sha || item?.artifactSha256 !== digest) failures.push(fail('PUBLIC_ARTIFACT_MISMATCH', `${surface} differs from candidate seal`));
|
|
91
|
+
}
|
|
92
|
+
for (const hostName of ['claude', 'codex']) {
|
|
93
|
+
const host = publication?.installed?.[hostName];
|
|
94
|
+
if (host?.status !== 'PASS' || host?.artifactSha256 !== digest) failures.push(fail('PUBLIC_HOST_NOT_PASS', `${hostName} is not running sealed public artifact`));
|
|
95
|
+
}
|
|
96
|
+
const brain = publication?.brain || {};
|
|
97
|
+
if (brain.status !== 'PASS' || brain.selfStore !== true || !(brain.broadMs <= Number(brain.deadlineMs) * 0.8)) failures.push(fail('PUBLIC_BRAIN_NOT_PASS', 'public installed Brain acceptance failed'));
|
|
98
|
+
const probes = Array.isArray(publication?.postPublicationChecks) ? publication.postPublicationChecks : [];
|
|
99
|
+
const probe = probes.find((check) => check?.name === 'published-surface-probe' && check?.sha === sha);
|
|
100
|
+
if (probe?.status !== 'completed' || probe?.conclusion !== 'success') failures.push(fail('POST_PUBLICATION_CHECK_NOT_GREEN', 'published-surface-probe is not green'));
|
|
101
|
+
return { verdict: failures.length === 0 ? 'PASS' : 'FAIL', sha, artifactSha256: digest, failures };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function evaluateLivePreflight(observed) {
|
|
105
|
+
const failures = [];
|
|
106
|
+
if (observed?.dirty !== false) failures.push(fail('DIRTY_WORKTREE', 'local worktree is dirty'));
|
|
107
|
+
if (observed?.localSha !== observed?.remoteSha) failures.push(fail('REMOTE_SHA_MISMATCH', 'local HEAD is not origin/main'));
|
|
108
|
+
if (!Array.isArray(observed?.openIssues) || observed.openIssues.length > 0) failures.push(fail('OPEN_ISSUES', `${observed?.openIssues?.length ?? 'unknown'} issue(s) open`));
|
|
109
|
+
if (!Array.isArray(observed?.failedRuns) || observed.failedRuns.length > 0) failures.push(fail('GITHUB_FAILURES', `${observed?.failedRuns?.length ?? 'unknown'} recent failed run(s)`));
|
|
110
|
+
if (observed?.activeSelfStore !== true) failures.push(fail('BRAIN_SELF_STORE_MISSING', 'installed active registry lacks ruvnet-brain'));
|
|
111
|
+
if (observed?.branchEnforceAdmins !== true) failures.push(fail('ADMIN_BYPASS_ENABLED', 'main protection does not enforce required checks for admins'));
|
|
112
|
+
if (observed?.productionProtected !== true) failures.push(fail('PRODUCTION_ENV_UNPROTECTED', 'production environment has no required reviewer protection'));
|
|
113
|
+
if (observed?.releaseVector !== 'PASS') failures.push(fail('RELEASE_VECTOR_NOT_PASS', `release vector is ${observed?.releaseVector ?? 'UNKNOWN'}`));
|
|
114
|
+
return { verdict: failures.length === 0 ? 'PASS' : 'FAIL', observed, failures };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function latestRunsByWorkflow(runs) {
|
|
118
|
+
const latest = new Map();
|
|
119
|
+
for (const run of Array.isArray(runs) ? runs : []) {
|
|
120
|
+
const prior = latest.get(run.workflowName);
|
|
121
|
+
if (!prior || Number(run.databaseId || 0) > Number(prior.databaseId || 0)) latest.set(run.workflowName, run);
|
|
122
|
+
}
|
|
123
|
+
return [...latest.values()];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function command(cmd, args, options = {}) {
|
|
127
|
+
const result = spawnSync(cmd, args, { encoding: 'utf8', ...options });
|
|
128
|
+
return {
|
|
129
|
+
status: result.status,
|
|
130
|
+
stdout: result.stdout || '',
|
|
131
|
+
stderr: result.stderr || '',
|
|
132
|
+
error: result.error,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function jsonCommand(cmd, args, options = {}) {
|
|
137
|
+
const result = command(cmd, args, options);
|
|
138
|
+
if (result.status !== 0) return null;
|
|
139
|
+
try { return JSON.parse(result.stdout); } catch { return null; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function collectLivePreflight({ root = process.cwd(), repo = 'stuinfla/ruvnet-brain', includeVector = true } = {}) {
|
|
143
|
+
const git = (args) => command('git', args, { cwd: root });
|
|
144
|
+
const localSha = git(['rev-parse', 'HEAD']).stdout.trim();
|
|
145
|
+
const dirty = Boolean(git(['status', '--porcelain']).stdout.trim());
|
|
146
|
+
git(['fetch', '--quiet', 'origin', 'main']);
|
|
147
|
+
const remoteSha = git(['rev-parse', 'origin/main']).stdout.trim();
|
|
148
|
+
const openIssues = jsonCommand('gh', ['issue', 'list', '--repo', repo, '--state', 'open', '--limit', '100', '--json', 'number,title,url']) ?? null;
|
|
149
|
+
const runs = jsonCommand('gh', ['run', 'list', '--repo', repo, '--limit', '50', '--json', 'databaseId,workflowName,headSha,status,conclusion,url']) ?? null;
|
|
150
|
+
const failedRuns = Array.isArray(runs)
|
|
151
|
+
? latestRunsByWorkflow(runs.filter((run) => run.headSha === remoteSha))
|
|
152
|
+
.filter((run) => run.status !== 'completed' || run.conclusion !== 'success')
|
|
153
|
+
: null;
|
|
154
|
+
const protection = jsonCommand('gh', ['api', `repos/${repo}/branches/main/protection`]);
|
|
155
|
+
const environments = jsonCommand('gh', ['api', `repos/${repo}/environments`]);
|
|
156
|
+
const production = environments?.environments?.find((environment) => environment.name === 'Production – ruvnet-brain');
|
|
157
|
+
let activeSelfStore = false;
|
|
158
|
+
try {
|
|
159
|
+
const source = JSON.parse(fs.readFileSync(path.join(process.env.HOME || '', '.cache/ruvnet-brain/kb/SOURCE.json'), 'utf8'));
|
|
160
|
+
const stores = source.sources || source.repos || {};
|
|
161
|
+
activeSelfStore = Boolean(stores['ruvnet-brain']);
|
|
162
|
+
} catch {}
|
|
163
|
+
let releaseVector = 'UNKNOWN';
|
|
164
|
+
if (includeVector) {
|
|
165
|
+
const vector = jsonCommand(process.execPath, [path.join(root, 'scripts/release-vector.mjs'), '--json'], { cwd: root });
|
|
166
|
+
releaseVector = vector?.verdict || 'UNKNOWN';
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
localSha,
|
|
170
|
+
remoteSha,
|
|
171
|
+
dirty,
|
|
172
|
+
openIssues,
|
|
173
|
+
failedRuns,
|
|
174
|
+
activeSelfStore,
|
|
175
|
+
branchEnforceAdmins: protection?.enforce_admins?.enabled === true,
|
|
176
|
+
productionProtected: Array.isArray(production?.protection_rules) && production.protection_rules.length > 0 && production.can_admins_bypass === false,
|
|
177
|
+
releaseVector,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
|
|
182
|
+
const argument = (args, flag) => {
|
|
183
|
+
const index = args.indexOf(flag);
|
|
184
|
+
return index >= 0 ? args[index + 1] : null;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export function main(args = process.argv.slice(2)) {
|
|
188
|
+
if (args.includes('--status')) {
|
|
189
|
+
const result = evaluateLivePreflight(collectLivePreflight({ includeVector: !args.includes('--quick') }));
|
|
190
|
+
console.log(JSON.stringify(result, null, 2));
|
|
191
|
+
return result.verdict === 'PASS' ? 0 : 1;
|
|
192
|
+
}
|
|
193
|
+
const candidatePath = argument(args, '--candidate');
|
|
194
|
+
const publicationPath = argument(args, '--publication');
|
|
195
|
+
if (!candidatePath) {
|
|
196
|
+
console.error('Usage: release-proof.mjs --candidate <candidate-receipt.json> [--publication <publication-receipt.json>]');
|
|
197
|
+
return 2;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const candidate = readJson(candidatePath);
|
|
201
|
+
const result = publicationPath ? evaluatePublicationReceipt(candidate, readJson(publicationPath)) : evaluateCandidateReceipt(candidate);
|
|
202
|
+
console.log(JSON.stringify(result, null, 2));
|
|
203
|
+
return result.verdict === 'PASS' ? 0 : 1;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(`release-proof: ${error.message}`);
|
|
206
|
+
return 2;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# THE PLAYBOOK — the standing build playbook, in full
|
|
2
2
|
|
|
3
|
-
Updated: 2026-07-
|
|
3
|
+
Updated: 2026-07-30 | Version 1.0.1
|
|
4
4
|
Created: 2026-07-27
|
|
5
5
|
|
|
6
6
|
**Read this before your first build response in a session.** `plugin/scripts/session-start.sh`
|
|
@@ -41,6 +41,10 @@ the exact lie that makes people distrust rUv's code.
|
|
|
41
41
|
token exchange", not "does RuvNet apply") — the useful hit can be in ANY of the 32 repos, never
|
|
42
42
|
trust memory about what the corpus does or doesn't have.
|
|
43
43
|
- Check project memory (ruflo memory search / AgentDB) for prior decisions on this area.
|
|
44
|
+
- Diagnose memory only through one canonical absolute path: store a unique key with
|
|
45
|
+
`ruflo memory store --path <project>/.swarm/memory.db`, retrieve that exact key with the same
|
|
46
|
+
`--path`, then confirm the exact row through SQLite. A semantic-search miss, DB/WAL mtime,
|
|
47
|
+
daemon startup, or `[OK] Data stored successfully` alone proves neither failure nor success.
|
|
44
48
|
- Invoke Ruflo MCP tools first for capabilities they already expose. For a CLI-only interface,
|
|
45
49
|
use the brain's `ruvnet_cli_help` then `ruvnet_cli_run` tools with literal argv; never guess flags
|
|
46
50
|
by reconstructing a raw shell command.
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rvbc
|
|
3
|
-
description: Open the RuvNet Brain Console in Codex when the user mentions "rvbc", asks for the Brain Console, or wants to configure or inspect RuvNet Brain.
|
|
4
|
-
updated: 2026-07-
|
|
3
|
+
description: Open the RuvNet Brain Console in Claude Code and Codex when the user says "Configure RuvNet Brain", mentions "rvbc", asks for the Brain Console, or wants to configure or inspect RuvNet Brain. Claude Code supports /rvbc; Codex invokes the native $ruvnet-brain:rvbc skill.
|
|
4
|
+
updated: 2026-07-30
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# RuvNet Brain Console
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Codex
|
|
9
|
+
“Configure RuvNet Brain” opens this Console in Claude Code and Codex. Claude Code also supports
|
|
10
|
+
`/rvbc`; Codex invokes this skill as `$ruvnet-brain:rvbc` because custom plugin slash commands are
|
|
11
|
+
not part of the Codex CLI slash-command surface.
|
|
11
12
|
|
|
12
13
|
1. Say one short sentence: "Opening it now; it scans live while you watch."
|
|
13
|
-
2.
|
|
14
|
-
|
|
14
|
+
2. Resolve the installed runtime at
|
|
15
|
+
`${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
16
|
+
A current-repository `scripts/onboarding-console.mjs` is allowed only for an explicit developer
|
|
17
|
+
checkout. Never fall back to a guessed `~/Code` path.
|
|
15
18
|
3. Run `node <resolved-script> --serve --open` in the background.
|
|
16
19
|
4. Give the URL immediately. Do not promise a duration; the page reports its own scan progress.
|
|
17
20
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* adr-backfill.mjs — lift status and dates into frontmatter where tooling can read them.
|
|
4
|
+
*
|
|
5
|
+
* THE GAP, measured 2026-07-22: 12 of 32 ADRs carried NO frontmatter status and NO dates, and 9
|
|
6
|
+
* more had a creation date but no `updated`. The owner's requirement is exact: *"One thing I ALWAYS
|
|
7
|
+
* want you to do is indicate in an ADR what the status is... they should have a time and date
|
|
8
|
+
* stamped on when they were initially written and a time and date stamped on the last time they
|
|
9
|
+
* were updated and why. That way I know I'm never looking at out-of-date documents."*
|
|
10
|
+
*
|
|
11
|
+
* NOTHING HERE IS INVENTED, and that constraint is the whole design:
|
|
12
|
+
*
|
|
13
|
+
* status LIFTED from the document's own body. All 12 already say `**Status**: Accepted (date)`
|
|
14
|
+
* — the information was never missing, it just sat where no tool could read it. A status
|
|
15
|
+
* is a judgement; deriving one from git would be fabrication, so a document that does
|
|
16
|
+
* not state its own status is REPORTED and skipped, never guessed at.
|
|
17
|
+
* date LIFTED from the body's `**Date**:`, falling back to git's first-commit date.
|
|
18
|
+
* updated DERIVED from `git log -1` — the last commit that actually touched the file. A typed
|
|
19
|
+
* date can be wrong; a git object cannot.
|
|
20
|
+
*
|
|
21
|
+
* Every written value is therefore traceable to either the document itself or the repository's
|
|
22
|
+
* history. This matters because a fabricated timestamp is worse than a missing one: a missing
|
|
23
|
+
* stamp is visibly unknown, while a wrong stamp is confidently misleading — the exact failure mode
|
|
24
|
+
* this project has paid for repeatedly.
|
|
25
|
+
*
|
|
26
|
+
* node scripts/adr-backfill.mjs report what WOULD change (default; writes nothing)
|
|
27
|
+
* node scripts/adr-backfill.mjs --apply write it
|
|
28
|
+
*/
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { execFileSync } from 'node:child_process';
|
|
32
|
+
|
|
33
|
+
const REPO = path.resolve(import.meta.dirname, '..');
|
|
34
|
+
const ADR_DIR = path.join(REPO, 'docs', 'adr');
|
|
35
|
+
const apply = process.argv.includes('--apply');
|
|
36
|
+
|
|
37
|
+
const git = (args) => {
|
|
38
|
+
try { return execFileSync('git', args, { cwd: REPO, encoding: 'utf8' }).trim(); }
|
|
39
|
+
catch { return ''; }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** First-commit date — the honest "created", when the body does not state one. */
|
|
43
|
+
const gitCreated = (rel) =>
|
|
44
|
+
(git(['log', '--follow', '--diff-filter=A', '--format=%ad', '--date=short', '--', rel]).split('\n').filter(Boolean).pop() || '');
|
|
45
|
+
|
|
46
|
+
/** Last commit that touched the file — the honest "updated". */
|
|
47
|
+
const gitUpdated = (rel) => git(['log', '-1', '--format=%ad', '--date=short', '--', rel]);
|
|
48
|
+
|
|
49
|
+
const VALID = ['Proposed', 'Accepted', 'Implemented', 'Superseded', 'Deprecated'];
|
|
50
|
+
|
|
51
|
+
function parseBody(src) {
|
|
52
|
+
// `**Status**: Accepted (2026-06-27)` → Accepted, 2026-06-27
|
|
53
|
+
const sm = src.match(/^\*\*Status\*\*:\s*([A-Za-z]+)(?:\s*\((\d{4}-\d{2}-\d{2})\))?/m);
|
|
54
|
+
const dm = src.match(/^\*\*Date\*\*:\s*(\d{4}-\d{2}-\d{2})/m);
|
|
55
|
+
const status = sm && VALID.find((v) => v.toLowerCase() === sm[1].toLowerCase());
|
|
56
|
+
return { status: status || null, statusDate: sm?.[2] || null, date: dm?.[1] || null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function frontmatter(src) {
|
|
60
|
+
const lines = src.split('\n');
|
|
61
|
+
if (lines[0]?.trim() !== '---') return null;
|
|
62
|
+
const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
|
|
63
|
+
if (end < 0) return null;
|
|
64
|
+
const keys = {};
|
|
65
|
+
for (let i = 1; i < end; i++) {
|
|
66
|
+
const m = lines[i].match(/^(\w+):\s*(.*)$/);
|
|
67
|
+
if (m) keys[m[1]] = m[2];
|
|
68
|
+
}
|
|
69
|
+
return { keys, end, lines };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const f of fs.readdirSync(ADR_DIR).filter((n) => /^\d{4}-.*\.md$/.test(n)).sort()) {
|
|
74
|
+
const rel = path.join('docs', 'adr', f);
|
|
75
|
+
const abs = path.join(ADR_DIR, f);
|
|
76
|
+
const src = fs.readFileSync(abs, 'utf8');
|
|
77
|
+
const fm = frontmatter(src);
|
|
78
|
+
if (!fm) { results.push({ f, skip: 'no frontmatter block to extend' }); continue; }
|
|
79
|
+
|
|
80
|
+
const body = parseBody(src);
|
|
81
|
+
const need = {};
|
|
82
|
+
if (!fm.keys.status && body.status) need.status = body.status;
|
|
83
|
+
if (!fm.keys.date) { const d = body.date || body.statusDate || gitCreated(rel); if (d) need.date = d; }
|
|
84
|
+
if (!fm.keys.updated) { const u = gitUpdated(rel); if (u) need.updated = u; }
|
|
85
|
+
|
|
86
|
+
if (!fm.keys.status && !body.status) {
|
|
87
|
+
// A status is a judgement. Refuse to guess; report it so a human can decide.
|
|
88
|
+
results.push({ f, skip: 'no status stated anywhere — a human must set this, it cannot be derived' });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!Object.keys(need).length) { results.push({ f, ok: true }); continue; }
|
|
92
|
+
|
|
93
|
+
if (apply) {
|
|
94
|
+
const insert = Object.entries(need).map(([k, v]) => `${k}: ${v}`);
|
|
95
|
+
const out = [...fm.lines.slice(0, fm.end), ...insert, ...fm.lines.slice(fm.end)];
|
|
96
|
+
fs.writeFileSync(abs, out.join('\n'));
|
|
97
|
+
}
|
|
98
|
+
results.push({ f, need });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const changed = results.filter((r) => r.need);
|
|
102
|
+
const skipped = results.filter((r) => r.skip);
|
|
103
|
+
console.log(`\n ${results.length} ADR(s) · ${changed.length} ${apply ? 'updated' : 'would change'} · ${skipped.length} skipped · ${results.filter((r) => r.ok).length} already complete\n`);
|
|
104
|
+
for (const r of changed) console.log(` ${apply ? '✓' : '·'} ${r.f} +${Object.entries(r.need).map(([k, v]) => `${k}=${v}`).join(' +')}`);
|
|
105
|
+
for (const r of skipped) console.log(` ⚠ ${r.f} — ${r.skip}`);
|
|
106
|
+
if (!apply && changed.length) console.log(`\n Nothing written. Re-run with --apply.\n`);
|
|
107
|
+
else console.log('');
|