ruvnet-brain 4.0.1 → 4.0.4
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 +303 -24
- 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/commands/whats-new.md +6 -6
- package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +90 -32
- 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 +34 -29
- 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 +503 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/scripts/whats-new.mjs +42 -0
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +98 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/plugin/skills/whats-new/SKILL.md +4 -4
- 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 +531 -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/fix-workstream.mjs +291 -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 +237 -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 +558 -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 +326 -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 +172 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2922 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/protected-release-invocation.mjs +76 -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/publication-receipt.mjs +307 -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-authority.mjs +93 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +439 -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 +296 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stabilization-receipt.mjs +108 -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 +867 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wired-check.mjs — refuses to let a module ship with zero callers.
|
|
4
|
+
*
|
|
5
|
+
* THE FAILURE THIS EXISTS TO END. On 2026-07-22 this project shipped built-tested-unwired code
|
|
6
|
+
* SEVEN times in a single session: capability-registry, capability-audit, lesson-gate, anticipate,
|
|
7
|
+
* advocacy-outcomes, lesson-promote, continuation-gate. Each was found by a human running grep.
|
|
8
|
+
* Every one passed its own tests, because a unit test imports the module directly — the one caller
|
|
9
|
+
* whose existence is guaranteed by its own file.
|
|
10
|
+
*
|
|
11
|
+
* (Those names are deliberately NOT written here as `<name>.mjs`. The first version of this header
|
|
12
|
+
* listed them in invocation form, and the gate's own memorial then matched as a caller for every
|
|
13
|
+
* module it eulogised. A gate must not be able to wire its own dead.)
|
|
14
|
+
*
|
|
15
|
+
* The owner's principle, P7: "Built is not shipped; shipped is not wired. A feature exists only
|
|
16
|
+
* when a real caller invokes it on a real user path."
|
|
17
|
+
*
|
|
18
|
+
* node scripts/wired-check.mjs report
|
|
19
|
+
* node scripts/wired-check.mjs --check exit 1 if any shippable module has no caller
|
|
20
|
+
*
|
|
21
|
+
* ── WHAT CHANGED, 2026-07-22 (ADR-037 / DDD-0010) ────────────────────────────────────────────
|
|
22
|
+
* v1 of this gate reported 62/62 wired, exit 0, and had never failed. That was not health, it was
|
|
23
|
+
* silence. Adversarial review (GPT-5.6-Sol, Fable 5) found three defects, all measured:
|
|
24
|
+
*
|
|
25
|
+
* 1. THE PREDICATE. v1 matched any MENTION of the basename anywhere in a file. A comment counted.
|
|
26
|
+
* A substring counted — `prove` matched "proven"; `version` matched a `"version"` JSON key.
|
|
27
|
+
* DDD-0010's own glossary said a Caller is "explicitly NOT any mention of the module's name",
|
|
28
|
+
* and the implementation was exactly that. The model disclaimed the code and nobody diffed it.
|
|
29
|
+
* Now: a caller must reference the module in INVOCATION SHAPE — inside a quoted string (import,
|
|
30
|
+
* require, npm script, workflow `run:`) or after node/bash/sh. Prose no longer wires anything.
|
|
31
|
+
*
|
|
32
|
+
* 2. THE INVENTORY. v1 read `scripts/*.mjs` only — 40 of 129 first-party executables were
|
|
33
|
+
* invisible, never audited, never printed, never counted. Among them
|
|
34
|
+
* `plugin/scripts/anticipate.sh`: one of the seven failures above, which v1 could never have
|
|
35
|
+
* caught. An invisible module is worse than an unwired one, because nothing reports it.
|
|
36
|
+
*
|
|
37
|
+
* 3. THE SEARCH SET. `kb/` was scanned for callers — ~3MB of JSON corpora INDEXING 68 OTHER
|
|
38
|
+
* REPOSITORIES, so a foreign repo's filenames counted as callers here. Removed. Also added:
|
|
39
|
+
* `.github/` + `--include=*.yml` (all 5 workflows are YAML; adding the directory without the
|
|
40
|
+
* glob, as ADR-037 draft 1 proposed, would have matched exactly nothing) and root package.json,
|
|
41
|
+
* where npm-script callers actually live.
|
|
42
|
+
*
|
|
43
|
+
* Also fixed: the test exclusion filtered `/tests/` PATHS, so `scripts/console-engine.test.mjs`
|
|
44
|
+
* counted as a caller — violating this file's own rule in-tree. Now excluded by NAME, anywhere.
|
|
45
|
+
*
|
|
46
|
+
* WHAT COUNTS AS A CALLER: an invocation-shaped reference from non-test, non-self source. A test is
|
|
47
|
+
* explicitly NOT a caller — that exclusion is the entire point, because every one of the seven had
|
|
48
|
+
* passing tests.
|
|
49
|
+
*/
|
|
50
|
+
import fs from 'node:fs';
|
|
51
|
+
import path from 'node:path';
|
|
52
|
+
import os from 'node:os';
|
|
53
|
+
import { loadLessons, TRIGGERS, STATUS } from './lesson-store.mjs';
|
|
54
|
+
|
|
55
|
+
const REPO = path.resolve(import.meta.dirname, '..');
|
|
56
|
+
const argv = process.argv.slice(2);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Modules that are legitimately standalone: a human or an out-of-repo scheduler runs them, so a call
|
|
60
|
+
* site would be wrong to demand.
|
|
61
|
+
*
|
|
62
|
+
* An array, not an object literal, so a duplicate name is DETECTABLE. v1 used an object and had
|
|
63
|
+
* `memory-doctor` twice (lines 50 and 64) — last-wins discarded one silently.
|
|
64
|
+
*
|
|
65
|
+
* HONEST LIMIT (ADR-037 §3): a reason here is PROSE, and prose is not verification. v1's four
|
|
66
|
+
* "invoked from the workflow" reasons were all written in the same commit as the gate, by an author
|
|
67
|
+
* who never ran the check — three of them were simply false. No schema detects that. The two real
|
|
68
|
+
* mitigations are (a) needing far fewer entries now the predicate is correct, and (b) PRINTING
|
|
69
|
+
* every entry on every run, below, so they cannot rot unseen.
|
|
70
|
+
*/
|
|
71
|
+
const STANDALONE = [
|
|
72
|
+
['lesson-seed', 'one-shot seeding, run deliberately by a human'],
|
|
73
|
+
['lesson-ratify', 'the human control surface — a CLI is its entire purpose'],
|
|
74
|
+
['stamp-sweep', 'ADR-056 §2 — the ONE-TIME backfill half of the stamp rule. A human runs it once '
|
|
75
|
+
+ '(--apply) to reach the files nobody is editing; the ongoing half is the md-stamp PostToolUse '
|
|
76
|
+
+ 'hook, which IS wired. Deliberately not in a gate: it WRITES to documents, and a writer that '
|
|
77
|
+
+ 'fires unattended on every push is how a repo gets a churn diff it did not ask for. It shares '
|
|
78
|
+
+ 'its entire placement implementation with the hook (ensureStamp/stampInsertionPoint/hasStamp), '
|
|
79
|
+
+ 'so there is no second copy to drift. Converged 2026-07-27: 132 stamped, 0 stampable.'],
|
|
80
|
+
['memory-doctor', 'diagnostic CLI'],
|
|
81
|
+
['token-report', 'diagnostic CLI'],
|
|
82
|
+
['agentdb-fleet-doctor', 'diagnostic CLI run by hand when a fleet looks wrong'],
|
|
83
|
+
['agentdb-context', 'targeted full-fidelity AgentDB recall CLI run by a human (`--grep`, `--key`, '
|
|
84
|
+
+ '`--window`). Its former automatic 36-hour dump was deliberately retired from the machine-wide '
|
|
85
|
+
+ 'SessionStart hook after 61KB of output hid the current checkpoint; SessionStart now prints the '
|
|
86
|
+
+ 'checkpoint plus a compact lesson index and directs topic recall through `ruflo memory search`.'],
|
|
87
|
+
['onboarding-console', 'human-started local server reached through the shipped `/rvbc`, `/rvcb`, '
|
|
88
|
+
+ '`/brain-console`, and `/ruvnet-brain:configure` command documents. The command host executes '
|
|
89
|
+
+ 'those instructions; there is intentionally no in-process source caller for a long-running CLI.'],
|
|
90
|
+
['ingest-meeting', 'one-shot ingestion, run by hand'],
|
|
91
|
+
['fix-metaharness-memretrieve', 'one-shot historical repair; kept for the record'],
|
|
92
|
+
['gen-console-images', 'build-time asset generation, run by hand'],
|
|
93
|
+
['adr-backfill', 'one-shot backfill by a human; its result is enforced by adr-format.test.mjs'],
|
|
94
|
+
['stamp-existing-rvf-generations', 'one-shot maintainer migration that binds already-built canonical '
|
|
95
|
+
+ 'RVFs to RVF-GENERATIONS.json and optionally prunes legacy sidecars; build-bundle.mjs consumes '
|
|
96
|
+
+ 'and validates the resulting manifest, so scheduling this destructive migration would be wrong'],
|
|
97
|
+
['release', 'the ship path, run by a human'],
|
|
98
|
+
['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
|
|
99
|
+
+ 'isolated writing agent to start and hand off a fix lane. Scheduling it would violate its safety '
|
|
100
|
+
+ 'boundary: it prepares evidence but never merges, pushes, publishes, deletes, or cleans worktrees'],
|
|
101
|
+
// Run by the launchd nightly, which lives OUTSIDE this repo — so no in-repo caller can exist.
|
|
102
|
+
// This is the one category the scanner genuinely cannot reach, and saying so is the honest form.
|
|
103
|
+
['self-update', 'launchd nightly (out-of-repo scheduler)'],
|
|
104
|
+
['count-chunks', 'human-run CLI — recount + restamp chunk surfaces (--check for drift); no scheduler'],
|
|
105
|
+
['brain-stamp', 'invoked by self-update.mjs:249, the nightly launchd driver (com.ruvnet.brain-nightly)'],
|
|
106
|
+
['lesson-promote', 'human-run CLI — promotion is manual (--apply); no scheduler yet (automation is ADR-029 #4, open)'],
|
|
107
|
+
['behavioral-l1-l4', 'behavioural harness invoked by its own test file — not a product path'],
|
|
108
|
+
// A measurement harness, not a product path: it answers "would bounding the cross-encoder pool
|
|
109
|
+
// change the answers?" and its --collect phase costs one full uncapped all-repos query per
|
|
110
|
+
// held-out question (~2-8 min each, ~120 questions). Nothing may schedule that. Run by a human
|
|
111
|
+
// whenever the retrieval pool policy is touched; `npm run cap:report` replays the recorded scores.
|
|
112
|
+
['rerank-cap-eval', 'human-run measurement harness — --collect is hours of cross-encoder time; never scheduled. '
|
|
113
|
+
+ 'Collects an uncapped baseline (--cap 0), a real capped run (--cap N, ADR-059) or a real cascade run '
|
|
114
|
+
+ '(--cascade K, ADR-060); all three are collected FOR REAL because batch composition moves scores'],
|
|
115
|
+
// The other half of the same measurement (ADR-059): --collect/--report replays a cap offline,
|
|
116
|
+
// this one runs the real thing warm and paired, because batch composition moves cross-encoder
|
|
117
|
+
// scores and a replayed cap is therefore an approximation of a real one. ~35 minutes of ONNX
|
|
118
|
+
// inference for 24 questions; human-run, never scheduled.
|
|
119
|
+
['rerank-cap-warm-ab', 'human-run measurement harness — the real warm paired A/B behind ADR-059 (--cap) and '
|
|
120
|
+
+ 'ADR-059 (--cascade); never scheduled. Both policies share it on purpose: a number is only comparable '
|
|
121
|
+
+ 'to another number taken the same way'],
|
|
122
|
+
|
|
123
|
+
// ADDED 2026-07-23 (P7 sweep, ADR-037 honesty bar — each reason verified against reality below,
|
|
124
|
+
// not written blind. See PROGRESS.md / the wiring report for the per-item evidence.)
|
|
125
|
+
//
|
|
126
|
+
// launchd nightly (out-of-repo scheduler) — confirmed LIVE via `launchctl list` + the installed
|
|
127
|
+
// plist's own ProgramArguments on 2026-07-23, not assumed from the header comment alone:
|
|
128
|
+
['clear-claude-tmp', 'launchd, every 3h (out-of-repo scheduler) — confirmed live: '
|
|
129
|
+
+ 'com.stuartkerr.clear-claude-tmp.plist is loaded and its ProgramArguments invoke this exact file'],
|
|
130
|
+
['nightly-gists', 'launchd nightly 21:47 (out-of-repo scheduler) — confirmed live: '
|
|
131
|
+
+ 'com.ruvnet.brain-gists.plist is loaded and its ProgramArguments invoke this exact file'],
|
|
132
|
+
['nightly-wrapper', 'launchd nightly 03:15 (out-of-repo scheduler) — confirmed live: '
|
|
133
|
+
+ 'com.ruvnet.brain-nightly.plist is loaded and its ProgramArguments invoke this exact file'],
|
|
134
|
+
['routing-flywheel', 'launchd nightly 04:45 --dry-run (out-of-repo scheduler) — confirmed live: '
|
|
135
|
+
+ 'com.ruvnet.routing-flywheel.plist is loaded and its ProgramArguments invoke this exact file'],
|
|
136
|
+
['install-npx-witness', 'one-shot idempotent installer for the com.ruvnet.npx-witness launchd job, '
|
|
137
|
+
+ 'run by hand once — confirmed live: the installed plist\'s ProgramArguments match exactly what '
|
|
138
|
+
+ 'this script writes. The recurring job body is scripts/npx-witness.sh, wired separately'],
|
|
139
|
+
|
|
140
|
+
// human-run KB-build/grading harnesses — documented as a unit in CONTRIBUTING.md §5 ("the test/
|
|
141
|
+
// grading scripts") and exercised by hand per PROGRESS.md session logs (`--name X --variant Y`).
|
|
142
|
+
// Each needs an external `../ruvnet-repos/<name>` clone and/or a paid multi-vendor LLM call
|
|
143
|
+
// (OPENROUTER_API_KEY) to grade a KB variant — deliberately outside gate.sh's fast/free/local
|
|
144
|
+
// pipeline (gate.sh only runs prove.mjs, which needs neither):
|
|
145
|
+
['brain-capability-check', 'human-run KB grading harness (CONTRIBUTING.md §5); '
|
|
146
|
+
+ 'grades a built variant against kb/capability.*.json by hand before shipping'],
|
|
147
|
+
['brain-grade-groundtruth', 'human-run KB grading harness (CONTRIBUTING.md §5); needs an external '
|
|
148
|
+
+ '../ruvnet-repos/<name> clone + paid multi-vendor LLM grading, run by hand per repo'],
|
|
149
|
+
['build-l2', 'human-run KB synthesis harness (CONTRIBUTING.md §5); synthesizes + grades L2 prose '
|
|
150
|
+
+ 'via a paid multi-vendor LLM panel, run by hand per repo/variant'],
|
|
151
|
+
['build-primer', 'human-run KB synthesis harness, the same proven shape as build-l2 (its own header): '
|
|
152
|
+
+ 'per-repo top-down primer over the 6 comprehension archetypes, paid-LLM synthesis, run by hand per '
|
|
153
|
+
+ 'repo. Surfaced here 2026-07-23 when the comment-strip stopped counting its usage-example mentions '
|
|
154
|
+
+ 'as callers — it never had a code caller, it is invoked by a person.'],
|
|
155
|
+
['gen-images', 'human-run explainer-image generator (OpenAI gpt-image-1, dall-e-3 fallback), run by '
|
|
156
|
+
+ 'hand when the explainer art needs regenerating — a paid one-shot build tool with no code caller, '
|
|
157
|
+
+ 'same class as the KB synthesis harnesses above. Surfaced by the 2026-07-23 comment-strip.'],
|
|
158
|
+
|
|
159
|
+
// diagnostic CLIs run by hand — same shape as memory-doctor/token-report/agentdb-fleet-doctor above:
|
|
160
|
+
['calibrate-router', 'measurement harness run by hand to calibrate router tiers on real runs; '
|
|
161
|
+
+ 'billing-safety wrapped (strips API keys so it can only bill the subscription)'],
|
|
162
|
+
['proactivity-metrics', 'ADR-041 recall + false-alarm harness. Its own CLI prints the two metrics, and '
|
|
163
|
+
+ 'tests/integration/proactivity-ground-truth + tests/mutation/proactivity-detector-mutation exercise '
|
|
164
|
+
+ 'it — a test is not a caller, so it registers here like calibrate-router. It spawns the REAL '
|
|
165
|
+
+ 'capability-registry.mjs against a ground-truth scratch machine to measure detector-recall and '
|
|
166
|
+
+ 'false-alarm IN-FENCE (the two ADR-028 metrics that do not need the deploy gate).'],
|
|
167
|
+
['correction-detect-measure', 'the measurement harness ADR-033 §2 requires, run by hand (its own '
|
|
168
|
+
+ '`invokedDirectly` CLI). It walks the real transcript corpus to score correction-detect on a '
|
|
169
|
+
+ 'held-out split. It was previously reported "wired" by three mentions that are ALL prose — two '
|
|
170
|
+
+ 'header comments in correction-detect{,-embed}.mjs and this file\'s own HELD string — the exact '
|
|
171
|
+
+ '"a gate must not wire its own dead" hole an independent regrade found; the comment-strip closes '
|
|
172
|
+
+ 'the comment half, and this entry is the honest classification for a hand-run harness.'],
|
|
173
|
+
['correction-detect-embed', 'ADR-033 NEGATIVE-RESULT reference, run by hand. Built 2026-07-23 to '
|
|
174
|
+
+ 'test whether an embedding k-NN (the same local Xenova/all-MiniLM-L6-v2 path the KB uses) clears '
|
|
175
|
+
+ 'the lesson-extraction floor where the regex cannot. It does NOT: measured WORSE precision than '
|
|
176
|
+
+ 'the regex (25% vs 50-100%) at comparable recall on the same held-out split, because MiniLM '
|
|
177
|
+
+ 'separates by topic, not by the pragmatic "is this correcting the agent" property. Kept, wired to '
|
|
178
|
+
+ 'nothing on purpose, so the negative is reproducible and not re-litigated — NOT a path to wire in.'],
|
|
179
|
+
['check-indexation', 'diagnostic CLI run by hand; explicitly "always exit 0, not a gate" per its '
|
|
180
|
+
+ 'own header — live Bing/Google scraping to eyeball real-world SEO status, not CI-appropriate'],
|
|
181
|
+
['check-legibility', 'manual pre-ship check run by hand against a LOCAL dev server the developer '
|
|
182
|
+
+ 'starts themselves (--url http://127.0.0.1:.../page.html, via Playwright) — needs a live '
|
|
183
|
+
+ 'rendered page, not a static diff, so it is a dev-loop tool like gen-console-images.mjs, not a '
|
|
184
|
+
+ 'CI step'],
|
|
185
|
+
['dev-plugin-link', 'developer convenience CLI — its own header: "Users never run it." Hot-links '
|
|
186
|
+
+ 'the cached plugin install to the working tree so hook edits apply without a CC restart'],
|
|
187
|
+
|
|
188
|
+
// ADR-0026 Meta LLM Proxy passthrough trial — both are human-run by design (one launches a proxied
|
|
189
|
+
// session interactively via `exec claude "$@"`, the other is the trial's safety-net uninstaller):
|
|
190
|
+
['claude-proxied', 'ADR-0026 proxy trial: human-run launcher for one proxied Claude Code session '
|
|
191
|
+
+ '(exec claude "$@") — interactive by design, never invoked programmatically'],
|
|
192
|
+
['proxy-revert', 'ADR-0026 proxy trial: human-run safety-net uninstaller, run by hand to fully '
|
|
193
|
+
+ 'revert the trial'],
|
|
194
|
+
|
|
195
|
+
// lesson-capture CLI — same shape as lesson-seed/lesson-ratify above: a human runs this with
|
|
196
|
+
// --task/--tried/--worked/--critique flags. docs/ARCHITECTURE-MAP.md documents the pipeline as
|
|
197
|
+
// "record-lesson.mjs -> lesson-store.mjs -> lesson-ratify.mjs (a human, never the model)":
|
|
198
|
+
['record-lesson', 'human-run structured lesson-capture CLI (--task/--tried/--worked/--critique); '
|
|
199
|
+
+ 'never invoked by the model itself, by design'],
|
|
200
|
+
|
|
201
|
+
// one-shot, and its job is done: issue #4 closed 2026-07-10 (confirmed live via `gh issue view 4`).
|
|
202
|
+
// Per the file's own header its 07:17 launchd trigger removed itself after firing, so no plist
|
|
203
|
+
// remains to find. Kept for the historical record, same precedent as fix-metaharness-memretrieve
|
|
204
|
+
// above — NOT re-wired, because there is nothing left for it to verify:
|
|
205
|
+
['verify-nightly-close-issue4', 'one-shot, job complete: issue #4 closed 2026-07-10 (confirmed '
|
|
206
|
+
+ 'live); its own launchd trigger self-removed after firing. Kept for the record — deletion is '
|
|
207
|
+
+ 'also reasonable and is flagged as a candidate in the wiring report, Stuart\'s call'],
|
|
208
|
+
|
|
209
|
+
// The grounding wall and its successful-search stamp used to be in this inert class. They now
|
|
210
|
+
// ship through the plugin shim on both Claude and Codex; keeping them exempt here made the wiring
|
|
211
|
+
// report contradict its own live hook census.
|
|
212
|
+
['kling-preflight', 'PreToolUse (Bash) gate — ships inert by design, same SECURITY.md class as '
|
|
213
|
+
+ 'ground-before-write. Per ADR-0014 ownership moved to the Kling skill (confirmed live: a copy '
|
|
214
|
+
+ 'ships at ~/.claude/skills/klingai/scripts/kling-preflight.sh); NOT currently wired into any '
|
|
215
|
+
+ 'settings.json there either — honestly dormant until a user opts in, not a silent gap'],
|
|
216
|
+
];
|
|
217
|
+
// REMOVED 2026-07-22, each verified before removal:
|
|
218
|
+
// check-legibility / check-indexation / status-honesty — claimed "invoked from the workflow";
|
|
219
|
+
// nothing in .github/ invokes them. The claim was false, so they are now audited for real.
|
|
220
|
+
// claims-verify — the claim was TRUE (.github/workflows/ci.yml:56 runs `npm run claims:verify`).
|
|
221
|
+
// It needs no entry now that package.json and *.yml are searched. Draft 1 of ADR-037 called this
|
|
222
|
+
// one false too; it had grepped `claims-verify` while the npm script is `claims:verify`.
|
|
223
|
+
// doc-currency, sync-version, build-bundle, health-repair, capability-audit, wired-check —
|
|
224
|
+
// all have real invokers the corrected scanner now finds on its own.
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* DELIBERATELY HELD — built, correct to keep, knowingly NOT wired, each with the bar it must clear.
|
|
228
|
+
* A separate category from STANDALONE on purpose: filing held work under "standalone" would be a
|
|
229
|
+
* small lie that hides a real gap. Held work is VISIBLE work.
|
|
230
|
+
*/
|
|
231
|
+
const HELD = {
|
|
232
|
+
'correction-detect': 'N3 lesson extraction. Re-measured 2026-07-23 on a reproducible held-out split '
|
|
233
|
+
+ 'of 1,328 real transcripts (scripts/correction-detect-measure.mjs): 5 real detector bugs fixed, '
|
|
234
|
+
+ 'corpus detections 2->8, ~50-100% precision at n=4 on the holdout. TWO measured findings now bound '
|
|
235
|
+
+ 'this, and both point the same way. (1) The >=100-DETECTION floor is UNREACHABLE ON THIS CORPUS: '
|
|
236
|
+
+ 'hand-labeling all 271 loose-net candidates across the full 1,328 transcripts against ADR-033\'s '
|
|
237
|
+
+ 'four-signal definition found only ~37-43 genuine corrections TOTAL — a third of the floor. The '
|
|
238
|
+
+ 'floor is a fact about the data, not the regex. (2) The embedding classifier that this note used '
|
|
239
|
+
+ 'to name as the way out was BUILT and MEASURED (scripts/correction-detect-embed.mjs, STANDALONE): '
|
|
240
|
+
+ 'it is WORSE than the regex (25% vs 50-100% precision, comparable recall) and less legible, because '
|
|
241
|
+
+ 'MiniLM separates by topic not by pragmatics. No primitive clears >=100 here. The honest floor '
|
|
242
|
+
+ '(owner decision, ADR-033) is ">=90% precision at whatever N the corpus supports, accumulating as '
|
|
243
|
+
+ 'it grows" — under which this STILL stays HELD, precision at the available N being unproven. '
|
|
244
|
+
+ 'CORPUS WIDENED 2026-07-24 (the lever nobody had pulled): the note above said the floor is a fact '
|
|
245
|
+
+ 'about THE DATA — true, and it was measured on ONE project. Re-run across 8 further transcript '
|
|
246
|
+
+ 'corpora (--corpus-dir): 2,728 files / 2,641 candidates / 11 detections, for a combined 4,083 '
|
|
247
|
+
+ 'files / 4,023 candidates / 19 detections. Detections went 8 -> 19 with ZERO detector changes, so '
|
|
248
|
+
+ 'the binding constraint was corpus size, not the regex. TWO findings. (1) IT GENERALISES: it fires '
|
|
249
|
+
+ 'in a salon website, an insurance-appeal tool and a health platform — non-software domains it was '
|
|
250
|
+
+ 'never tuned on — at a comparable low rate. Not 0 (which would mean overfit to this repo\'s '
|
|
251
|
+
+ 'dialect) and not a flood (false positives). That was the open question about the second-person '
|
|
252
|
+
+ 'rule and it now has an answer. (2) THE FLOOR IS REACHABLE, JUST FAR: at ~19 per 4,083 transcripts, '
|
|
253
|
+
+ '>=100 needs on the order of 21,000 transcripts. That is a schedule, not a wall — the earlier '
|
|
254
|
+
+ '"unreachable" was true of one project and false of the machine. '
|
|
255
|
+
+ 'PRECISION MEASURED AT n=19 (2026-07-24), by THREE independent raters labelling BLIND — they were '
|
|
256
|
+
+ 'given the utterances with the detector verdict stripped, and the author did not label, being '
|
|
257
|
+
+ 'unblinded by construction. Majority vote. HOLDOUT (the only unbiased half): n=9, 7 TRUE / 2 FALSE, '
|
|
258
|
+
+ 'ZERO borderline, so strict and lenient agree exactly: PRECISION 77.8%. Combined n=19: 68.4% strict '
|
|
259
|
+
+ '/ 84.2% lenient. Rater agreement 73.7-84.2% pairwise, 13/19 unanimous. THIS REPLACES the old '
|
|
260
|
+
+ '"~50-100% at n=4" — a range so wide it asserted nothing. The wider corpus did not just grow N, it '
|
|
261
|
+
+ 'REVEALED THE DETECTOR IS WEAKER than the small sample implied, which is precisely why n=4 was never '
|
|
262
|
+
+ 'shippable. 77.8% is BELOW the >=90% floor, so this stays HELD — now for a measured reason instead '
|
|
263
|
+
+ 'of an unproven one. THE RESIDUAL FALSE-POSITIVE CLASS IS NAMED AND SINGULAR: both holdout misses '
|
|
264
|
+
+ '(and every tune borderline) are REPROACH PHRASED AS A QUESTION — "you keep doing X, why?" — real '
|
|
265
|
+
+ 'dissatisfaction about a real pattern, carrying no explicit forward rule. Signal 3 binds a quantifier '
|
|
266
|
+
+ 'to the second person and these satisfy it ("you keep"), yet a human reads them as complaint, not '
|
|
267
|
+
+ 'instruction. That is the next thing to fix, and it is one class, not a long tail.',
|
|
268
|
+
'lesson-lifecycle': 'retirement + generalization for extracted lessons. Depends on '
|
|
269
|
+
+ 'correction-detect; wiring it alone would retire hand-written lessons on evidence that does '
|
|
270
|
+
+ 'not exist yet.',
|
|
271
|
+
'capability-audit': 'THE L3 GAP, and now visible instead of falsely green. The offensive half of '
|
|
272
|
+
+ 'retrieval — it answers "what capability is installed and dormant?" — but nothing calls it: verified '
|
|
273
|
+
+ '2026-07-23 that its only non-comment mention is its own invokedDirectly guard (self). It was '
|
|
274
|
+
+ 'reported "wired" purely by JSDoc mentions in capability-registry.mjs until the comment-strip; '
|
|
275
|
+
+ 'ADR-028 and 4.0-READINESS §4 both name this the single largest 4.0 gap. HELD because the fix is a '
|
|
276
|
+
+ 'real decision — one in-session consumer that speaks unprompted, with a one-action permanent silence '
|
|
277
|
+
+ '— not a line of wiring.',
|
|
278
|
+
'issue-fix': 'the GitHub-issue AUTO-FIXER, built but intentionally NOT wired. Verified 2026-07-23: '
|
|
279
|
+
+ 'issue-watch.yml runs only issue-watch.mjs (which LISTS issues), and issue-watch.mjs spawns only '
|
|
280
|
+
+ 'gh/sleep — never this. Its own yml comment says why it is gated: it "spawns a headless Claude and '
|
|
281
|
+
+ 'costs money + needs credentials". Wiring it is a cost-and-consent decision the owner makes, not a '
|
|
282
|
+
+ 'gap to silently close.',
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/** Where first-party executables live. v1 knew only the first line of this table. */
|
|
286
|
+
const INVENTORY_ROOTS = [
|
|
287
|
+
{ dir: 'scripts', exts: ['.mjs', '.sh'], recurse: true },
|
|
288
|
+
{ dir: 'plugin/scripts', exts: ['.mjs', '.sh'], recurse: true },
|
|
289
|
+
{ dir: 'bin', exts: ['.mjs'], recurse: false },
|
|
290
|
+
{ dir: 'console', exts: ['.js'], recurse: false },
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Where callers live. NOT kb/ — it indexes 68 other repos and their filenames are not our callers.
|
|
295
|
+
*
|
|
296
|
+
* `.claude/` earns its place the hard way: the first run of the corrected gate reported
|
|
297
|
+
* `version-bump-gate.sh` unwired, and it is invoked from `.claude/settings.json`. Per ADR-037 §3, an
|
|
298
|
+
* invoker outside the roots proves the ROOTS are incomplete — the fix is to add the root, never to
|
|
299
|
+
* write an exemption. That rule caught its own author within a minute of the gate first running.
|
|
300
|
+
*/
|
|
301
|
+
const CALLER_ROOTS = ['scripts', 'plugin', 'console', 'bin', '.github', '.claude', 'package.json'];
|
|
302
|
+
const CALLER_EXTS = new Set(['.mjs', '.js', '.sh', '.json', '.html', '.yml', '.yaml']);
|
|
303
|
+
|
|
304
|
+
const isTestFile = (f) => /\.(test|spec)\.(mjs|js)$/.test(path.basename(f))
|
|
305
|
+
|| f.includes(`${path.sep}tests${path.sep}`) || f.startsWith(`tests${path.sep}`);
|
|
306
|
+
|
|
307
|
+
function walk(abs, recurse, out = []) {
|
|
308
|
+
let entries = [];
|
|
309
|
+
try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return out; }
|
|
310
|
+
for (const e of entries) {
|
|
311
|
+
const full = path.join(abs, e.name);
|
|
312
|
+
if (e.isDirectory()) {
|
|
313
|
+
if (recurse && e.name !== 'node_modules' && !e.name.startsWith('.')) walk(full, recurse, out);
|
|
314
|
+
} else out.push(full);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Every first-party module expected to be USED by something. */
|
|
320
|
+
export function shippableModules(repo = REPO) {
|
|
321
|
+
const out = [];
|
|
322
|
+
for (const root of INVENTORY_ROOTS) {
|
|
323
|
+
const abs = path.join(repo, root.dir);
|
|
324
|
+
for (const full of walk(abs, root.recurse)) {
|
|
325
|
+
const ext = path.extname(full);
|
|
326
|
+
if (!root.exts.includes(ext)) continue;
|
|
327
|
+
if (isTestFile(full)) continue;
|
|
328
|
+
const rel = path.relative(repo, full).split(path.sep).join('/'); // POSIX-style always: repo-relative keys must not vary by OS
|
|
329
|
+
out.push({ base: path.basename(full, ext), file: path.basename(full), rel });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Files that could contain a caller. */
|
|
336
|
+
function callerFiles(repo = REPO) {
|
|
337
|
+
const out = [];
|
|
338
|
+
for (const r of CALLER_ROOTS) {
|
|
339
|
+
const abs = path.join(repo, r);
|
|
340
|
+
let st; try { st = fs.statSync(abs); } catch { continue; }
|
|
341
|
+
if (st.isFile()) { out.push(abs); continue; }
|
|
342
|
+
// ADR-056, 2026-07-27: git hooks are EXTENSIONLESS by git's own contract — the file must be named
|
|
343
|
+
// exactly `pre-push` / `pre-commit` or git ignores it. So the extension filter excluded
|
|
344
|
+
// `scripts/git-hooks/pre-push` — THIS REPO'S PRIMARY SHIP GATE — from the caller set entirely, and
|
|
345
|
+
// anything reachable only from there read as unwired. Found the moment the currency check was
|
|
346
|
+
// wired into pre-push and `wired-check` went on reporting it `manual`. This file's own note names
|
|
347
|
+
// the remedy: "an invoker outside the roots proves the ROOTS are incomplete — the fix is to add
|
|
348
|
+
// the root, never to" special-case the module. Same rule, applied to the extension filter.
|
|
349
|
+
for (const f of walk(abs, true)) {
|
|
350
|
+
if (CALLER_EXTS.has(path.extname(f))) { out.push(f); continue; }
|
|
351
|
+
if (path.extname(f) === '' && path.basename(path.dirname(f)) === 'git-hooks') out.push(f);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* INVOCATION-SHAPED match. The whole correction lives here.
|
|
359
|
+
*
|
|
360
|
+
* A caller references the file either inside a quoted string — covering `import ... from '...'`,
|
|
361
|
+
* `require('...')`, npm scripts, and a workflow's `run:` — or directly after node/bash/sh. A bare
|
|
362
|
+
* mention in prose does not match, which is what let a comment wire a module in v1.
|
|
363
|
+
*/
|
|
364
|
+
export function callerPattern(fileName) {
|
|
365
|
+
const q = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
366
|
+
// eslint-disable-next-line no-useless-escape
|
|
367
|
+
return new RegExp(`["'\`][^"'\`\\n]*${q}|(?:node|bash|sh|exec|spawn\\w*)\\s+[^\\n]*${q}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Strip comments BEFORE matching. The invocation branch of callerPattern (`node <file>`) matches a
|
|
372
|
+
* usage example in a header comment exactly as it matches a real command line — which is how, after
|
|
373
|
+
* this gate already fixed "a bare prose mention wired a module in v1", a `node scripts/…measure.mjs`
|
|
374
|
+
* line inside a comment quietly re-opened it (an independent regrade, 2026-07-23, found correction-
|
|
375
|
+
* detect-measure.mjs reported "wired" by three comment/string mentions, one of them this file's own
|
|
376
|
+
* HELD reason). Removing comments closes the comment form of that hole.
|
|
377
|
+
*
|
|
378
|
+
* Conservative by construction: over-stripping a `//` that lives inside a string (a URL) can only ever
|
|
379
|
+
* DROP a candidate line, never invent one — and a genuine caller is an import / require / node-exec,
|
|
380
|
+
* never a URL — so this can hide a prose mention but cannot hide a real caller. The `[^:]` guard keeps
|
|
381
|
+
* `https://…` from eating its own line. String LITERALS that name a module (e.g. a documentation
|
|
382
|
+
* string) are deliberately NOT stripped — that is a separate, rarer shape, and the module it would
|
|
383
|
+
* mis-wire here is instead classified honestly in STANDALONE.
|
|
384
|
+
*/
|
|
385
|
+
export function stripComments(src, ext) {
|
|
386
|
+
const jsLike = ['.mjs', '.js', '.cjs', '.ts', '.mts'].includes(ext);
|
|
387
|
+
const shLike = ['.sh', '.yml', '.yaml'].includes(ext);
|
|
388
|
+
if (!jsLike && !shLike) return src; // .json has no line-comment syntax
|
|
389
|
+
// ONLY blank a line that is ENTIRELY a comment (trimmed, it starts with the comment marker). This is
|
|
390
|
+
// the deliberately narrow, provably-safe version. An earlier attempt span-matched `/*…*/` globally and
|
|
391
|
+
// a `/*` sitting inside a line-comment made it swallow real code across many lines — it hid
|
|
392
|
+
// sign-bundle.mjs's genuine caller in self-update.mjs:353 (execFileSync([... 'scripts/sign-bundle.mjs'])).
|
|
393
|
+
// A REAL invocation never lives on a whole-line-comment line, so this cannot hide a caller; block and
|
|
394
|
+
// inline comments are left untouched (a caller-shaped mention there is rare and not worth the span risk),
|
|
395
|
+
// and JSDoc `*` bodies are NOT matched (a `*gen()` line is real code). Every false caller the regrade
|
|
396
|
+
// found — `// … scripts/…measure.mjs` usage examples — is a whole-line `//`, so this closes exactly it.
|
|
397
|
+
const marker = jsLike ? '//' : '#';
|
|
398
|
+
// ADR-056, 2026-07-27: ALSO blank JSDoc continuation lines. The comment documenting the
|
|
399
|
+
// package.json fix, two screens below, spelled both an invocation-shaped path and an
|
|
400
|
+
// `npm run <script>` line as illustrations — and forged TWO false callers with them, flipping the
|
|
401
|
+
// very module it audits back to `wired` twice in a row. The note above says block comments are
|
|
402
|
+
// "left untouched (a caller-shaped mention there is rare…)"; it turned out to be rare right up
|
|
403
|
+
// until someone documented a caller-shaped bug.
|
|
404
|
+
// SAFE BY CONSTRUCTION, and the space matters: a trimmed line starting `*` FOLLOWED BY WHITESPACE
|
|
405
|
+
// (or nothing) is a JSDoc body line. A generator method is `*gen()` — star then a letter — so it is
|
|
406
|
+
// untouched, which is exactly the case the original note refused to risk. Like every other rule
|
|
407
|
+
// here this can only ever DROP a candidate line, never invent one.
|
|
408
|
+
const jsdocBody = /^\*(\s|$)/;
|
|
409
|
+
return src.split('\n')
|
|
410
|
+
.map((l) => {
|
|
411
|
+
const t = l.trim();
|
|
412
|
+
if (t.startsWith(marker)) return '';
|
|
413
|
+
if (jsLike && jsdocBody.test(t)) return '';
|
|
414
|
+
return l;
|
|
415
|
+
})
|
|
416
|
+
.join('\n');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* ── THE THIRD STATE (ADR-056, 2026-07-27) ────────────────────────────────────────────────────────
|
|
421
|
+
* DEFINING an npm script is not INVOKING it. `package.json` line 35 defines `doc:currency` as a
|
|
422
|
+
* command that runs the currency script, and callerPattern's quoted-string branch matches that
|
|
423
|
+
* VALUE — so the line that DEFINES the script counted as a caller of it. The currency script was
|
|
424
|
+
* therefore reported `wired` while nothing in the repo ran it: absent from pre-push, gate.sh,
|
|
425
|
+
* gates.mjs and every workflow. Measured 2026-07-27: `npm run doc:currency` (a REAL invocation)
|
|
426
|
+
* does not match callerPattern at all, while the definition does. The auditor was blind in BOTH
|
|
427
|
+
* directions on the largest instance of exactly the built-but-unwired failure it exists to catch.
|
|
428
|
+
*
|
|
429
|
+
* This is the THIRD time this defect class has landed here, and the shape never changes: **a string
|
|
430
|
+
* that NAMES a module is not a CALL to it.** v1 substring-matched prose; the 2026-07-23 regrade
|
|
431
|
+
* found invocation-shaped usage examples inside `//` comments; this is the package.json form.
|
|
432
|
+
*
|
|
433
|
+
* ⚠ AND THE FIX REPRODUCED IT ONCE MORE, IN ITSELF. The first draft of this very comment spelled the
|
|
434
|
+
* currency script's full filename after the word `node`, as an illustration. `stripComments` blanks
|
|
435
|
+
* only WHOLE-LINE `//` comments — block comments are left untouched, deliberately ("a caller-shaped
|
|
436
|
+
* mention there is rare and not worth the span risk"). So this JSDoc forged a caller and made
|
|
437
|
+
* wired-check itself a "caller" of the module it was auditing, flipping it back to `wired`. Caught
|
|
438
|
+
* 2026-07-27 by checking the row after the fix instead of trusting it. The hole is REAL and still
|
|
439
|
+
* open for block comments; it is recorded here rather than papered over, because the safe rule costs
|
|
440
|
+
* nothing: **never write an invocation-shaped path in a comment in this file.** Name the script, not
|
|
441
|
+
* the command line.
|
|
442
|
+
*
|
|
443
|
+
* NOT fixed by flagging npm scripts `unwired` — `tests/unit/wired-check.test.mjs:45` deliberately
|
|
444
|
+
* accepts them, and rightly: a script a human runs IS a way in. The honest distinction is that an
|
|
445
|
+
* npm script is a HUMAN entry point, not an AUTOMATED one, and collapsing those two is what let
|
|
446
|
+
* doc-currency hide in plain sight for five days. So a module reachable ONLY through an npm script
|
|
447
|
+
* that nothing automated invokes is reported `manual` — true, legible, and impossible to mistake for
|
|
448
|
+
* "this runs in the ship path." Inventing a fourth lie-shaped status was the alternative; DDD-0008
|
|
449
|
+
* says not to, and a status that overclaims is the thing this whole context exists to end.
|
|
450
|
+
*/
|
|
451
|
+
export const NPM_AUTO_LIFECYCLE = new Set([
|
|
452
|
+
'prepare', 'prepublish', 'prepublishOnly', 'prepack', 'postpack',
|
|
453
|
+
'preinstall', 'install', 'postinstall',
|
|
454
|
+
'prestart', 'poststart', 'pretest', 'posttest',
|
|
455
|
+
'preversion', 'version', 'postversion',
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
/** Which package.json script bodies NAME this module? (definitions, not invocations) */
|
|
459
|
+
export function npmScriptsNaming(pkgSrc, modFile) {
|
|
460
|
+
let pkg;
|
|
461
|
+
try { pkg = JSON.parse(pkgSrc); } catch { return []; }
|
|
462
|
+
const scripts = pkg && pkg.scripts;
|
|
463
|
+
if (!scripts || typeof scripts !== 'object') return [];
|
|
464
|
+
const re = callerPattern(modFile);
|
|
465
|
+
return Object.entries(scripts)
|
|
466
|
+
.filter(([, body]) => typeof body === 'string' && (re.test(body) || re.test(`"${body}"`)))
|
|
467
|
+
.map(([name]) => name);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Is any of these npm script names reachable by AUTOMATION rather than by a human typing it?
|
|
472
|
+
* Automated means: npm itself runs it (a lifecycle name), or some file — a workflow, a shell hook,
|
|
473
|
+
* or another composite npm script — invokes it by name.
|
|
474
|
+
*/
|
|
475
|
+
export function npmScriptAutomated(names, files, repo = REPO) {
|
|
476
|
+
for (const n of names) {
|
|
477
|
+
if (NPM_AUTO_LIFECYCLE.has(n)) return { automated: true, via: `npm lifecycle (\`${n}\` is run by npm itself)` };
|
|
478
|
+
}
|
|
479
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
480
|
+
for (const n of names) {
|
|
481
|
+
const re = new RegExp(`(?:npm|pnpm|yarn|npm-run-all|run-s|run-p)\\s+(?:run\\s+)?${esc(n)}(?![\\w:-])`);
|
|
482
|
+
for (const f of files) {
|
|
483
|
+
const rel = path.relative(repo, f).split(path.sep).join('/');
|
|
484
|
+
let src = '';
|
|
485
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
486
|
+
// A composite script in package.json ("test:all": "npm run test:unit && …") IS automation.
|
|
487
|
+
if (re.test(stripComments(src, path.extname(f)))) return { automated: true, via: `${rel} runs \`${n}\`` };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { automated: false, via: null };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Count REAL callers. Tests excluded deliberately: all seven failures had passing tests. */
|
|
494
|
+
export function callersOf(mod, files, repo = REPO) {
|
|
495
|
+
const re = callerPattern(mod.file);
|
|
496
|
+
const hits = [];
|
|
497
|
+
for (const f of files) {
|
|
498
|
+
const rel = path.relative(repo, f).split(path.sep).join('/');
|
|
499
|
+
if (rel === mod.rel) continue; // self
|
|
500
|
+
if (isTestFile(rel)) continue; // a test is not a caller
|
|
501
|
+
let src = '';
|
|
502
|
+
try { src = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
503
|
+
if (re.test(stripComments(src, path.extname(f)))) hits.push(rel);
|
|
504
|
+
}
|
|
505
|
+
return hits;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function audit({ repo = REPO, standalone = STANDALONE, held = HELD } = {}) {
|
|
509
|
+
const dupes = [];
|
|
510
|
+
const seen = new Map();
|
|
511
|
+
for (const [name, why] of standalone) {
|
|
512
|
+
if (seen.has(name)) dupes.push(name); else seen.set(name, why);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const files = callerFiles(repo);
|
|
516
|
+
const all = shippableModules(repo);
|
|
517
|
+
const rows = [];
|
|
518
|
+
for (const m of all) {
|
|
519
|
+
if (seen.has(m.base)) { rows.push({ ...m, state: 'exempt', why: seen.get(m.base) }); continue; }
|
|
520
|
+
if (held[m.base]) { rows.push({ ...m, state: 'held', why: held[m.base] }); continue; }
|
|
521
|
+
const callers = callersOf(m, files, repo);
|
|
522
|
+
let state = callers.length ? 'wired' : 'unwired';
|
|
523
|
+
let why;
|
|
524
|
+
// ADR-056: callers that are ONLY the package.json line defining the script are not automation.
|
|
525
|
+
if (callers.length && callers.every((c) => c === 'package.json')) {
|
|
526
|
+
let pkgSrc = '';
|
|
527
|
+
try { pkgSrc = fs.readFileSync(path.join(repo, 'package.json'), 'utf8'); } catch { /* none */ }
|
|
528
|
+
const names = npmScriptsNaming(pkgSrc, m.file);
|
|
529
|
+
if (names.length) {
|
|
530
|
+
const auto = npmScriptAutomated(names, files, repo);
|
|
531
|
+
if (!auto.automated) {
|
|
532
|
+
state = 'manual';
|
|
533
|
+
const list = names.map((n) => `\`${n}\``).join(', ');
|
|
534
|
+
why = `defined as npm script ${list} and invoked by NOTHING automated — reachable only by a `
|
|
535
|
+
+ `human typing \`npm run ${names[0]}\`. Built and correct; not in any ship path.`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
rows.push({ ...m, state, callers, ...(why ? { why } : {}) });
|
|
540
|
+
}
|
|
541
|
+
return { rows, dupes, inventory: all.length };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* ── WHAT CHANGED, 2026-07-24: TWO NEW CHECK CLASSES ─────────────────────────────────────────────
|
|
546
|
+
*
|
|
547
|
+
* This gate closes THE PREVIOUS GAP: everything above proves a MODULE has a CALLER — an import, a
|
|
548
|
+
* require, an npm/yml `run:` line. That is the right predicate for a module. It is the wrong
|
|
549
|
+
* predicate for a HOOK and for a LESSON, and three real instances found the seam on 2026-07-24:
|
|
550
|
+
*
|
|
551
|
+
* 1. scripts/capability-audit.mjs — a module with no caller. THIS gate already caught it (HELD,
|
|
552
|
+
* above). Proof the existing predicate works when the thing being checked really is a module.
|
|
553
|
+
* 2. plugin/scripts/route-dispatch.sh — a PreToolUse gate, written 2026-07-13, never added to
|
|
554
|
+
* ~/.claude/settings.json. This gate MISSED it, because "wired" for a hook does not mean
|
|
555
|
+
* "some file mentions my filename" — hook-shim.mjs's dispatch table happens to quote every
|
|
556
|
+
* hook's filename, so the module predicate stumbles onto the right answer for anything shipped
|
|
557
|
+
* through hooks.json, BY COINCIDENCE OF QUOTING STYLE, and has ZERO visibility into
|
|
558
|
+
* ~/.claude/settings.json — the one file that proves a hook is actually LIVE on THIS machine
|
|
559
|
+
* rather than merely shipped in the plugin payload. That is a different question, and nothing
|
|
560
|
+
* above asked it.
|
|
561
|
+
* 3. L16-parallel-by-default (~/.config/ruvnet-brain/lessons.json) — RATIFIED, taught FOUR times,
|
|
562
|
+
* and never once delivered in any session, because its trigger `choose-work` was never
|
|
563
|
+
* requested by any event in plugin/scripts/lesson-hooks.sh's dispatch table. A lesson's
|
|
564
|
+
* trigger is not a module either — no import, no require, nothing the predicate above can see.
|
|
565
|
+
*
|
|
566
|
+
* Same root failure as ADR-037's original defect (a gap in the shape of the exact bug the gate
|
|
567
|
+
* exists to prevent), different surface. Two new, narrow predicates, each modelling the REAL
|
|
568
|
+
* mechanism Claude Code / the lesson dispatcher actually uses, not a generic text search:
|
|
569
|
+
*
|
|
570
|
+
* CHECK B — HOOK WIRING. Walks the real reachability chain: plugin/hooks/hooks.json (what we
|
|
571
|
+
* ship) → hook-shim.mjs's own TABLE (resolving its id-based indirection explicitly, since a hook
|
|
572
|
+
* id like "route-dispatch" is not the string "route-dispatch.sh") → this repo's own
|
|
573
|
+
* .claude/settings.json → the user's REAL ~/.claude/settings.json (what is actually installed on
|
|
574
|
+
* THIS machine — never checked before). One further hop is closed by a small fixed-point pass
|
|
575
|
+
* (the unprompted-speech runtime spawns anticipate.sh/lesson-hooks.sh as candidate producers),
|
|
576
|
+
* reusing this file's own invocation-shaped predicate (`callerPattern`/`stripComments`) but
|
|
577
|
+
* restricted to files already PROVEN reachable from a real hook entry point — not "does anything
|
|
578
|
+
* in the repo mention it", which is the weaker, accidentally-right-sometimes question above.
|
|
579
|
+
*
|
|
580
|
+
* CHECK C — LESSON-TRIGGER WIRING. Every ratified/active lesson names a `trigger`
|
|
581
|
+
* (scripts/lesson-store.mjs's TRIGGERS enum). plugin/scripts/lesson-hooks.sh's
|
|
582
|
+
* `case "$EVENT" in` block is the single authority on which triggers any real Claude Code event
|
|
583
|
+
* actually requests. A trigger no branch requests is INERT — the store says armed, the machine
|
|
584
|
+
* says unconnected, exactly the L16 finding — and is reported loudly, by name, every run.
|
|
585
|
+
*
|
|
586
|
+
* BOTH checks read HOOK_PLUMBING/HOOK_HELD in the same spirit as STANDALONE/HELD above: explicit,
|
|
587
|
+
* printed every run, a true reason required — never a silent exemption.
|
|
588
|
+
*
|
|
589
|
+
* THE --check DECISION, made deliberately (not a default carried over from the module check):
|
|
590
|
+
* • Check B (hook wiring) HARD-FAILS on a genuinely new unwired hook — same as an unwired module
|
|
591
|
+
* above, because a hook script IS a shippable module: this repo owns hooks.json, this repo's
|
|
592
|
+
* own .claude/settings.json, and (per HOOK_PLUMBING/HOOK_HELD) can name every known exception.
|
|
593
|
+
* kling-preflight.sh is the one PRE-EXISTING, already-documented gap (STANDALONE says the same
|
|
594
|
+
* thing above) — it is HELD, not unwired, so today's run does not break release.mjs for a
|
|
595
|
+
* condition this task did not introduce and was not asked to fix.
|
|
596
|
+
* • Check C (lesson triggers) is ADVISORY ONLY, NEVER fails --check, by deliberate design: the
|
|
597
|
+
* lesson store lives at ~/.config/ruvnet-brain/lessons.json (or $RUVNET_LESSON_STORE) — per-user,
|
|
598
|
+
* per-machine state OUTSIDE this repo. A fresh machine has none (loadLessons() degrades to []
|
|
599
|
+
* gracefully, not a failure). Hard-failing the ship path on the CONTENTS of a file the repo does
|
|
600
|
+
* not own, cannot reproduce in CI, and varies by whose machine runs it would gate a push on data
|
|
601
|
+
* that has nothing to do with the commit being shipped. It is reported loudly instead — printed
|
|
602
|
+
* every run, impossible to miss — which is the same bar HELD already sets for a known gap: never
|
|
603
|
+
* silent, never blocking.
|
|
604
|
+
*/
|
|
605
|
+
|
|
606
|
+
const HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'SessionEnd', 'PreCompact', 'Stop'];
|
|
607
|
+
const HOOK_EVENT_RE = new RegExp(`(${HOOK_EVENTS.join('|')})[^.\\n]{0,40}?\\b(hook|gate)\\b`, 'i');
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Files that MENTION a hook event in their header but are plumbing, not a hook body: a dispatcher, a
|
|
611
|
+
* shared parser, a shared logger. Needed because the header heuristic below is deliberately loose —
|
|
612
|
+
* hook-input.mjs's own header reads "the ONE parser every PreToolUse gate uses", which matches
|
|
613
|
+
* "PreToolUse gate" exactly as a genuine self-declaration would. Same STANDALONE-style honesty bar:
|
|
614
|
+
* explicit, printed every run, a TRUE reason — the fix for a heuristic false positive is a named
|
|
615
|
+
* exception, not a cleverer regex that will just find the next false positive.
|
|
616
|
+
*/
|
|
617
|
+
const HOOK_PLUMBING = [
|
|
618
|
+
['hook-shim.mjs', 'the Stable Spine dispatcher — routes EVERY event via its own TABLE (parsed below); '
|
|
619
|
+
+ 'it is what hooks.json calls, not a single-event hook body itself'],
|
|
620
|
+
['hook-input.mjs', 'the shared JSON-payload parser "every PreToolUse gate uses" (its own words) — a '
|
|
621
|
+
+ 'library, never itself registered as a hook'],
|
|
622
|
+
['gate-receipt.sh', 'the shared receipt-logger a blocking gate calls just before it exits non-zero — a '
|
|
623
|
+
+ 'library, never itself registered as a hook'],
|
|
624
|
+
['unprompted-runtime.mjs', 'the unprompted-speech chokepoint (ADR-040) — registered once in hooks.json, '
|
|
625
|
+
+ 'and itself spawns anticipate.sh + lesson-hooks.sh as candidate producers; it is the runtime, not a '
|
|
626
|
+
+ 'single-purpose hook body'],
|
|
627
|
+
['update-apply.mjs', 'the Stable Spine writer — a human/nightly CLI (see STANDALONE above), not a hook body'],
|
|
628
|
+
];
|
|
629
|
+
const HOOK_PLUMBING_NAMES = new Set(HOOK_PLUMBING.map(([n]) => n));
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* DELIBERATELY HELD, same bar as HELD above: a hook-intended script genuinely not reachable from
|
|
633
|
+
* hooks.json or any settings.json today, and NOT a gap this check can silently close — the owner's
|
|
634
|
+
* call, already recorded once (STANDALONE's kling-preflight entry above) and reproduced here so
|
|
635
|
+
* --check does not fail the ship path on a pre-existing, already-documented, non-regression condition.
|
|
636
|
+
*/
|
|
637
|
+
const HOOK_HELD = {
|
|
638
|
+
'kling-preflight.sh': 'PreToolUse (Bash) gate — ships inert by design (SECURITY.md), ownership moved to '
|
|
639
|
+
+ 'the Kling skill by ADR-0014 (confirmed live: a copy ships at '
|
|
640
|
+
+ '~/.claude/skills/klingai/scripts/kling-preflight.sh). Not currently wired into plugin/hooks/'
|
|
641
|
+
+ 'hooks.json, this repo\'s .claude/settings.json, or ~/.claude/settings.json — matching '
|
|
642
|
+
+ 'STANDALONE\'s own entry for this file above. Honestly dormant until a user opts in, not a silent '
|
|
643
|
+
+ 'gap this check introduces.',
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/** Read+JSON.parse a file. null on ANY failure (missing, unreadable, malformed) — never throws. */
|
|
647
|
+
function readJsonSafe(file) {
|
|
648
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Every `command` string inside a hooks.json / settings.json shaped `{ hooks: {...} }` document. */
|
|
652
|
+
function commandStrings(hooksNode) {
|
|
653
|
+
const out = [];
|
|
654
|
+
const walk = (node) => {
|
|
655
|
+
if (Array.isArray(node)) { node.forEach(walk); return; }
|
|
656
|
+
if (node && typeof node === 'object') {
|
|
657
|
+
if (typeof node.command === 'string') out.push(node.command);
|
|
658
|
+
for (const v of Object.values(node)) walk(v);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
walk(hooksNode);
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Plugin-script basenames (.mjs/.sh) mentioned literally in one command string. */
|
|
666
|
+
function basenamesIn(cmd) { return cmd.match(/[\w.-]+\.(?:mjs|sh)\b/g) || []; }
|
|
667
|
+
|
|
668
|
+
/** The hook-shim dispatch id passed as hook-shim.mjs's first CLI argument, if this command calls it. */
|
|
669
|
+
function hookShimIdIn(cmd) {
|
|
670
|
+
const m = cmd.match(/hook-shim\.mjs["'`]?\s+([a-zA-Z][\w-]*)/);
|
|
671
|
+
return m ? m[1] : null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** hook-shim.mjs's own dispatch TABLE: id -> file. Parsed, not re-implemented — it IS the authority. */
|
|
675
|
+
function hookShimTable(repo) {
|
|
676
|
+
let src = '';
|
|
677
|
+
try { src = fs.readFileSync(path.join(repo, 'plugin/scripts/hook-shim.mjs'), 'utf8'); } catch { return {}; }
|
|
678
|
+
const map = {};
|
|
679
|
+
const re = /'([\w-]+)':\s*{\s*file:\s*'([\w.-]+)'/g;
|
|
680
|
+
let m; while ((m = re.exec(src))) map[m[1]] = m[2];
|
|
681
|
+
return map;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* A hook body self-declares its event in its header — "a UserPromptSubmit hook", "PreToolUse gate on
|
|
686
|
+
* Bash" — read from a handful of real headers in this repo (ADR-037's own convention: read a few
|
|
687
|
+
* headers to see the shape). Deliberately loose; HOOK_PLUMBING above is the deliberate, honest
|
|
688
|
+
* correction for where it over-fires, rather than a fragile attempt at a perfect regex.
|
|
689
|
+
*/
|
|
690
|
+
function hookHeaderDeclares(src) { return HOOK_EVENT_RE.test(src.slice(0, 1600)); }
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* CHECK B — HOOK WIRING. See the file-level comment above for the full reasoning. Walks the real
|
|
694
|
+
* chain from the real entry points (plugin/hooks/hooks.json, this repo's .claude/settings.json, the
|
|
695
|
+
* user's actual ~/.claude/settings.json) through hook-shim.mjs's id-based indirection, then closes
|
|
696
|
+
* one further hop with a small fixed-point pass restricted to files already proven reachable.
|
|
697
|
+
*/
|
|
698
|
+
export function hookWiringAudit({
|
|
699
|
+
repo = REPO,
|
|
700
|
+
homeSettingsFile = path.join(os.homedir(), '.claude', 'settings.json'),
|
|
701
|
+
held = HOOK_HELD,
|
|
702
|
+
} = {}) {
|
|
703
|
+
const table = hookShimTable(repo);
|
|
704
|
+
const reached = new Map(); // basename -> Set(reason)
|
|
705
|
+
const add = (name, reason) => {
|
|
706
|
+
if (!name) return;
|
|
707
|
+
if (!reached.has(name)) reached.set(name, new Set());
|
|
708
|
+
reached.get(name).add(reason);
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const scanConfig = (file, label) => {
|
|
712
|
+
const doc = readJsonSafe(file);
|
|
713
|
+
if (!doc || !doc.hooks) return;
|
|
714
|
+
for (const cmd of commandStrings(doc.hooks)) {
|
|
715
|
+
for (const b of basenamesIn(cmd)) add(b, label);
|
|
716
|
+
const id = hookShimIdIn(cmd);
|
|
717
|
+
if (id && table[id]) add(table[id], `${label} (hook-shim id "${id}")`);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
scanConfig(path.join(repo, 'plugin/hooks/hooks.json'), 'plugin/hooks/hooks.json');
|
|
721
|
+
scanConfig(path.join(repo, '.claude/settings.json'), '.claude/settings.json (this repo)');
|
|
722
|
+
scanConfig(homeSettingsFile, '~/.claude/settings.json (this machine)');
|
|
723
|
+
|
|
724
|
+
// Fixed point: anything already reachable may itself spawn another plugin script by a real,
|
|
725
|
+
// invocation-shaped reference — reusing the module check's own predicate, never a comment.
|
|
726
|
+
const scriptsDir = path.join(repo, 'plugin/scripts');
|
|
727
|
+
let all = [];
|
|
728
|
+
try { all = fs.readdirSync(scriptsDir).filter((f) => /\.(mjs|sh)$/.test(f)); } catch { /* no dir */ }
|
|
729
|
+
for (let i = 0; i < 10; i++) {
|
|
730
|
+
let changed = false;
|
|
731
|
+
for (const from of [...reached.keys()]) {
|
|
732
|
+
let src = ''; try { src = fs.readFileSync(path.join(scriptsDir, from), 'utf8'); } catch { continue; }
|
|
733
|
+
const stripped = stripComments(src, path.extname(from));
|
|
734
|
+
for (const cand of all) {
|
|
735
|
+
if (reached.has(cand)) continue;
|
|
736
|
+
if (callerPattern(cand).test(stripped)) { add(cand, `spawned by plugin/scripts/${from}`); changed = true; }
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!changed) break;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const rows = [];
|
|
743
|
+
for (const f of all) {
|
|
744
|
+
if (HOOK_PLUMBING_NAMES.has(f)) continue; // plumbing, not a hook body — never part of the census
|
|
745
|
+
let src = ''; try { src = fs.readFileSync(path.join(scriptsDir, f), 'utf8'); } catch { continue; }
|
|
746
|
+
const declared = hookHeaderDeclares(src);
|
|
747
|
+
const isReached = reached.has(f);
|
|
748
|
+
if (!declared && !isReached) continue; // not hook-intended at all — outside the census
|
|
749
|
+
if (isReached) rows.push({ file: f, state: 'wired', sources: [...reached.get(f)] });
|
|
750
|
+
else if (held[f]) rows.push({ file: f, state: 'held', why: held[f] });
|
|
751
|
+
else rows.push({ file: f, state: 'unwired' });
|
|
752
|
+
}
|
|
753
|
+
return { rows, plumbing: HOOK_PLUMBING };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** The trigger tokens requested by lesson-hooks.sh's `case "$EVENT" in` block — the sole authority. */
|
|
757
|
+
function lessonHooksRequestedTriggers(repo) {
|
|
758
|
+
let src = '';
|
|
759
|
+
try { src = fs.readFileSync(path.join(repo, 'plugin/scripts/lesson-hooks.sh'), 'utf8'); } catch { return new Set(); }
|
|
760
|
+
const stripped = stripComments(src, '.sh');
|
|
761
|
+
const requested = new Set();
|
|
762
|
+
const re = /TRIGGERS="([^"]*)"/g;
|
|
763
|
+
let m;
|
|
764
|
+
while ((m = re.exec(stripped))) { for (const t of m[1].split(/\s+/)) if (t) requested.add(t); }
|
|
765
|
+
return requested;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* CHECK C — LESSON-TRIGGER WIRING. See the file-level comment above for the full reasoning
|
|
770
|
+
* (advisory-only, deliberately, because the store is per-user/per-machine state outside this repo).
|
|
771
|
+
*/
|
|
772
|
+
export function lessonTriggerAudit({ repo = REPO, lessonsFile = undefined } = {}) {
|
|
773
|
+
const requested = lessonHooksRequestedTriggers(repo);
|
|
774
|
+
const lessons = loadLessons(lessonsFile);
|
|
775
|
+
const live = lessons.filter((l) => !l.demoted && (l.status === STATUS.RATIFIED || l.status === STATUS.ACTIVE));
|
|
776
|
+
const labelOf = (trigger) => Object.values(TRIGGERS).find((t) => t.key === trigger)?.label || trigger;
|
|
777
|
+
const inert = live
|
|
778
|
+
.filter((l) => !requested.has(l.trigger))
|
|
779
|
+
.map((l) => ({ id: l.id, trigger: l.trigger, label: labelOf(l.trigger) }));
|
|
780
|
+
return { requested: [...requested], checked: live.length, inert };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const invokedDirectly = process.argv[1]
|
|
784
|
+
&& path.resolve(process.argv[1]).endsWith(`wired-check${path.extname(process.argv[1])}`);
|
|
785
|
+
|
|
786
|
+
if (invokedDirectly) {
|
|
787
|
+
const { rows, dupes, inventory } = audit();
|
|
788
|
+
const by = (s) => rows.filter((r) => r.state === s);
|
|
789
|
+
const unwired = by('unwired');
|
|
790
|
+
|
|
791
|
+
const hookAudit = hookWiringAudit();
|
|
792
|
+
const hookBy = (s) => hookAudit.rows.filter((r) => r.state === s);
|
|
793
|
+
const hookUnwired = hookBy('unwired');
|
|
794
|
+
|
|
795
|
+
const lessonAudit = lessonTriggerAudit();
|
|
796
|
+
|
|
797
|
+
if (!argv.includes('--quiet')) {
|
|
798
|
+
console.log(`\n ${inventory} first-party module(s) in the inventory`);
|
|
799
|
+
console.log(` ${by('wired').length} wired · ${by('manual').length} manual · ${by('exempt').length} exempt · `
|
|
800
|
+
+ `${by('held').length} held · ${unwired.length} UNWIRED\n`);
|
|
801
|
+
|
|
802
|
+
for (const u of unwired) console.log(` ✗ ${u.rel} — built, and invoked by nothing`);
|
|
803
|
+
if (unwired.length) {
|
|
804
|
+
console.log(`\n A module with no caller is not a feature. Either wire it to a real user path,`);
|
|
805
|
+
console.log(` or add it to STANDALONE in this file WITH A TRUE REASON.\n`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Every exemption, every run. v1 never printed these, so 3 false reasons rotted unseen for a
|
|
809
|
+
// day inside the gate built to stop exactly that.
|
|
810
|
+
if (by('manual').length) {
|
|
811
|
+
console.log(` ${by('manual').length} MANUAL — an npm script exists, but no automation runs it `
|
|
812
|
+
+ `(ADR-056; this is how the currency gate hid while reporting "wired"):\n`);
|
|
813
|
+
for (const m of by('manual')) console.log(` ▲ ${m.rel}\n ${m.why}\n`);
|
|
814
|
+
}
|
|
815
|
+
console.log(` ${by('exempt').length} exempt — no caller required, and why:\n`);
|
|
816
|
+
for (const e of by('exempt')) console.log(` ○ ${e.rel}\n ${e.why}`);
|
|
817
|
+
|
|
818
|
+
console.log(`\n ${by('held').length} HELD — built, not wired, and the bar each must clear:\n`);
|
|
819
|
+
for (const h of by('held')) console.log(` ⏸ ${h.rel}\n ${h.why}\n`);
|
|
820
|
+
|
|
821
|
+
if (dupes.length) console.log(` ✗ DUPLICATE exemption(s): ${dupes.join(', ')}\n`);
|
|
822
|
+
|
|
823
|
+
// ── CHECK B: HOOK WIRING ──────────────────────────────────────────────────────────────────
|
|
824
|
+
console.log(`\n ── HOOK WIRING — plugin/scripts/*.sh|*.mjs vs plugin/hooks/hooks.json, `
|
|
825
|
+
+ `.claude/settings.json, ~/.claude/settings.json ──\n`);
|
|
826
|
+
console.log(` ${hookAudit.rows.length} hook-intended script(s) in the census`);
|
|
827
|
+
console.log(` ${hookBy('wired').length} wired · ${hookBy('held').length} held · `
|
|
828
|
+
+ `${hookUnwired.length} UNWIRED\n`);
|
|
829
|
+
|
|
830
|
+
for (const u of hookUnwired) {
|
|
831
|
+
console.log(` ✗ plugin/scripts/${u.file} — declares a hook event and is invoked by nothing`);
|
|
832
|
+
}
|
|
833
|
+
if (hookUnwired.length) {
|
|
834
|
+
console.log(`\n A hook script nothing points at will never fire. Wire it into hooks.json or a`);
|
|
835
|
+
console.log(` settings.json, or add it to HOOK_HELD in this file WITH A TRUE REASON.\n`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
for (const h of hookBy('held')) console.log(` ⏸ plugin/scripts/${h.file}\n ${h.why}\n`);
|
|
839
|
+
|
|
840
|
+
for (const w of hookBy('wired')) {
|
|
841
|
+
console.log(` ✓ plugin/scripts/${w.file}\n via ${w.sources.join('; ')}`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
console.log(`\n ${HOOK_PLUMBING.length} plumbing file(s) excluded from the census (dispatchers/`
|
|
845
|
+
+ `parsers/loggers, not hook bodies), and why:\n`);
|
|
846
|
+
for (const [n, why] of HOOK_PLUMBING) console.log(` ○ plugin/scripts/${n}\n ${why}`);
|
|
847
|
+
|
|
848
|
+
// ── CHECK C: LESSON-TRIGGER WIRING (advisory — see the file-level comment for why) ─────────
|
|
849
|
+
console.log(`\n ── LESSON-TRIGGER WIRING — ratified lessons vs plugin/scripts/lesson-hooks.sh's `
|
|
850
|
+
+ `case block ──\n`);
|
|
851
|
+
console.log(` ${lessonAudit.checked} ratified/active lesson(s) checked (requested triggers: `
|
|
852
|
+
+ `${lessonAudit.requested.join(', ') || '(none)'})`);
|
|
853
|
+
console.log(` ${lessonAudit.inert.length} INERT\n`);
|
|
854
|
+
for (const l of lessonAudit.inert) {
|
|
855
|
+
console.log(` ✗ ${l.id} — trigger "${l.trigger}" (${l.label}) is ratified but NO event `
|
|
856
|
+
+ `requests it; this lesson will NEVER be delivered`);
|
|
857
|
+
}
|
|
858
|
+
if (lessonAudit.inert.length) {
|
|
859
|
+
console.log(`\n ADVISORY ONLY — the lesson store is per-user machine state, not part of this`);
|
|
860
|
+
console.log(` repo, so this never fails --check. Fix by adding the trigger to a case branch in`);
|
|
861
|
+
console.log(` plugin/scripts/lesson-hooks.sh.\n`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const bad = unwired.length || dupes.length || hookUnwired.length;
|
|
866
|
+
process.exit(argv.includes('--check') && bad ? 1 : 0);
|
|
867
|
+
}
|