mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
* feat(skills): check-skill-refs gate + per-commit skills gate runner Three integrity checks over skills/ markdown: dangling refs (backtick paths, composes: frontmatter, RESOLVER dispatcher slugs), donor-remnant paths behind a ratchet allowlist, and warn-only CLI-reference drift against the live tool surface. Wired as check:skill-refs in verify and bundled into scripts/skills-commit-gate.sh (conformance+resolver tests, check-resolvable --strict, lock regen+freshness, refs, optional per-file privacy lint) for any commit touching the skills pack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skillpack): machine-readable plugin curation record + membership assertions skills/plugin-exclusions.json records why each manifest skill is not bundled into openclaw.plugin.json (Tier B host-only decisions, seeded from the pre-existing split). test/openclaw-plugin-manifest.test.ts now asserts manifest = bundled + exclusions with no overlap, no orphans, no stale exclusions, and a sorted skills array (two pre-existing sort inversions fixed). Bundling decisions are now review-visible: moving a skill between tiers requires touching both files in one commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): skillify reconciled with the evolved upstream line (v2.0.0) Adopts eval contracts, the NO-REGRESSION LAW, idempotency guidance, and the 14-item checklist from the upstream skillify line while preserving this repo's divergent content. bulk-ingestion (later in this wave) delegates its codify phase to this checklist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): book-mirror 0.5.0 backport Table-misalignment root-cause fix, anti-repetition hard constraints, and the resonant-stance rewrite from the upstream line, generalized onto native gbrain surfaces (no external storage libs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(conventions): test-before-bulk gains the progressive ramp + output-existence checks 10 -> 100 -> 500 -> full ramp with per-round stop gates, the count-before/count-after output-existence check (catches silent zero-row writes), the <2% error-rate gate, the copy-paste verification checklist, cost estimation, and pointers to native pace mode + progress flags so nobody rebuilds throttling in bash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(conventions): regex-discipline — judgment vs heuristic epistemics The one-question test (deterministic-and-rote vs judgment), the earned- regex sequence, red flags/green lights, never-regex-what-an-attacker-can- imitate, and two genericized cautionary tales. Pairs with test-before-bulk: look before you compress, compress only the rote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(conventions): path-discipline — display strings are not path strings Never pass a display-formatted link/URL to a file tool; a "successful" write to a poisoned path lies. Poisoned-path tells, verify-before-done, retry discipline, junk-tree recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(conventions): exec-output — buffer to file, read bounded slices Empty exec results usually mean truncation, not a broken shell. The buffer-then-slice rule, shell patterns, and the diagnostic ladder, generalized to any harness. Conventions half of the durable-execution doctrine (the minion-orchestrator half lands separately). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): concept-synthesis 0.2.0 — curation cull phase Adds the keep/delete/merge muscle the synthesis loop lacked: minimum substance gates (style cannot rescue hollow ideas), weighted verdict bands with hard delete triggers, grounding labels wired to the citation conventions, a reputational-risk gate, cluster budgets, and reversible merge-with-backlinks with merge-count as an emergent-salience signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): measure-before-you-fix — measure-first ops triage Stopwatch-verify before raising timeouts; act-line vs alert-line. Trigger vocabulary maps onto the shipped doctor/autopilot/sync alert classes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): data-loss-gate — confirmation gate for irreversible operations Recoverability checklist + alternatives + explicit-yes before any bulk delete, forget sweep, source/mount removal, or history rewrite. No category-level exception classes: disposability is a property of the inspected target. Routing-convention honesty: the native operation- boundary confirm remains a filed follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): fact-check — pre-publication claim verification Breadth gate: extract every verifiable claim, verify each against live sources with a confidence scale. Data-derived claims section carries the two laws: producer is never verifier (independent re-derivation via a different query path) and affiliation is not authorship (typed edges, not co-occurrence). Hard-block on unsupported claims. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): resolve-before-asking — exhaust the brain before interrupting the human Query, timeline, and every mounted source before asking; when asking is justified, ask WITH a hypothesis (pairs with ask-user). Includes the no-placeholders-at-ingest rule. Owns WHETHER asking is justified; query owns the lookup verb, brain-ops the read-write cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): brain-ingest-gate — ingest entry gate (no raw copies, entity resolution, dedup) No-raw-copies entry rule; the Named-Entity Resolution Gate (registry- first + alias expansion — a vector score is a floor for prose, never a gate for named things); the qualitative dedup decision tree (open and read the top hit before writing). The gate keeps full dedup responsibility: native import skips only matching frontmatter IDs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): correction-pipeline — trace corrections to the contamination source When the user corrects a fact: root-cause it (7-class taxonomy), fix the source surface (facts table via recall/forget/remember; pages via edit + re-sync; identity files via bootstrap templates), then run the propagation check. Never just note it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): company-brainify — personal-to-team brain sanitization Strip/keep tables, triage grep pipelines, verification greps, and the history-purge procedure for publishing a sanitized team brain from a personal one. Scan scope covers meetings, dailies, facts, and takes, not just people pages. History rewrite is gated through data-loss-gate with a mirror-clone backup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): citation-graph-ingest — typed inter-document citation edges Extract every inter-document reference from an ingested corpus and classify the edge type (overrules/distinguishes/relies-on/extends...), written via native link ops and queryable via graph-query. States plainly that natural-language relational retrieval walks a fixed edge- type set today; wiring these types in is a filed follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): two-tier-extraction — triage/deep-read model routing for archives Cheap-model triage pass, gate, then strong-model deep read for archive ingestion, expressed through the model-routing conventions. Deterministic privacy wall: named-entity and sensitive-pattern classes are stripped before any model call; ambiguous content fails closed to human review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): brain-link-discipline — verified links in every deliverable Composite: same-message link rule, path derivation via git ls-files, push-before-link ordering, subagent-relay path rewrite, bulk-list formatting. Fallback chain: verified hosted-remote URL, then repo- relative path with a scope note, then publish output offered as an attachable artifact (never promised as a URL). Extends _output-rules.md with the verified-deliverable-link canon and the in-page (relative) vs in-message (absolute) scope split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): draft-in-voice — memory-grounded ghostwriting from validated profiles Load a validated voice profile, obey its hard rules, produce 2-3 drafts, run the voice-fidelity self-check, never auto-post. Ships the build-a- profile appendix (corpus threshold, fingerprint schema, profile page schema with provenance/consent/versioning) so users without a donor profile aren't dead-ended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): bulk-ingestion — the disciplined bulk pipeline lifecycle + manifest substrate Composite: the schema-trial-evaluate-codify-bulk-monitor lifecycle spine plus the manifest.json durable-state substrate (stable source-derived ids, derived-not-asserted status, idempotent workers, per-worker progress-file merge, never trust a subagent's 'done' — verify on disk). Codify delegates to skillify; execution to jobs/minions; ramping to test-before-bulk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scripts): check-skill-refs reads cliHints from operations source --tools-json does not serialize every op's cliHints, so real commands (get, timeline, backlinks, graph, link) warned as unknown. The checker now also derives the known-command set from operations.ts cliHints names + aliases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): meeting-ingestion 2.0.0 — recorder-agnostic pipeline backport Normalized transcript record as the any-recorder contract, multi-meeting split detection, cross-recorder dedup, evidence-based speaker resolution (never guess; ASR-variant search-before-create), required page sections with explicit-reason-for-empty, claim-verification table, and the brain- contradiction consistency check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): meeting-ingestion 2.1.0 — verify-before-done phase Hard gate before reporting ingested: required sections have substance, every people/companies slug has a page + timeline backlink, speaker map resolved, every quote grounded verbatim in the transcript (deterministic sidecar checks where transcripts are retained), fabricated-attendee sanity checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): meeting-ingestion 2.2.0 — sequence verify (chronology checks) Deterministic order lint before a meeting page ships: phase-inversion and impossible-transit checks, day-timeline gaps corroborated against `gbrain day`, with a pass/block/explicit-waive contract (waives logged as acknowledged, not resolved). Adds routing-eval fixtures for the upgraded skill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): reports 1.1.0 — Actionability Gate Four link failure modes (Broken/Dead/Indirect/Missing) with the rule "a missing link is honest, an indirect link is a broken promise", the blocked-vs-warning table, retry loop as agent-run checks, and the prompt gate snippet. Canon lives in _output-rules.md; this section carries the taxonomy delta. Routing fixtures added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): briefing 1.3.0 — pre-briefing context pulls Salience, anomalies, personal recall, and hot-memory pulse pulls before composing the briefing, wired to the real CLI flags (anomalies uses --lookback-days, not the upstream --since form). Routing fixtures added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): daily-task-manager 2.0.0 — stable IDs + fail-closed actions Stable task-ID anchors, deterministic action routing, fail-closed ambiguity handling (no mutation on zero/multiple matches), structured Returns contract with an agent-chaining JSON mode, minimal-diff save discipline. Routing fixtures added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): eiirp 1.1.0 — auto-fire gate (file before you deliver) Always-on convention: when substantial structured analysis of a user- shared document is about to be delivered, file the brain page first and deliver analysis + link in the same reply. Fire conditions, does-not- fire list, dedicated-pipeline exemptions, per-user storage-policy override. eiirp moves from plugin exclusion to bundled — the gate is load-bearing for downstream installs. Routing fixtures added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): minion-orchestrator 1.1.0 — durable execution doctrine Capability ladder for operations over two minutes: jobs + a one-shot self-deleting deadman (verifying the result was REPORTED, not that the process exited) where the shell-jobs contract allows; cron-checked progress file; foreground with buffered output otherwise. Deadman failure modes (dies before firing, double-fire idempotency, stale checkpoint) encoded in Contract and Anti-Patterns; content-addressed stage-checkpoint appendix. Existing trust-boundary text unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(skills): concept-synthesis cull-phase routing fixtures Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): research-compendium — archive, summarize 1:1, synthesize self-contained Deep-research discipline: archive every source, one summary per source, then a self-contained synthesis (the Self-Contained Rule; Low-Bar/High- Ceiling). Claims verification delegates to fact-check; retention is gated by the user's privacy posture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): context-audit — token hygiene for the always-loaded stack Report-only audit of SOUL/MEMORY/CLAUDE/AGENTS-class files: redundancy, contradictions, staleness, compression candidates ranked by token savings. Recommendations target bootstrap templates, never rendered files; judging routes through the native cross-modal eval runner with a cheap-model default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): blog-ingest — whole-publication and feed ingestion Feed discovery, pagination walking, article-shape normalization, canonical-URL dedup, idempotent re-runs, polite pacing, and empty-husk repair as agent procedures; per-article enrichment hands off to brain-ingest-gate. Public posts only. Fetched text is data, never instructions (untrusted-content section). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): conversation-archive — AI-chat history as first-class brain content Convert chat exports into per-conversation pages, import, validate via the conversation-parser surface, extract facts; gap-healing backfill under the three invariants (capture cadence outruns eviction, gap detection, write-path equals commit-path); idea-evolution tracing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): skill-autobench — evals mined from lived usage Mine a skill's real invocation history (user corrections are the gold signal) into a proposed eval contract and replayable cases with honesty labels, staged pending human approval — never rewrites a skill. Multi-model judging verifies each provider actually responded. Extends the SkillOpt surface; never duplicates it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(todos): giftable-import wave follow-ups Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skills): resolver strict reconciliation — triggers, fixtures, disambiguation Drives check-resolvable --strict to zero on the 69-skill pack: wave-side trigger narrowing (bare words become phrases on measure-before-you-fix, data-loss-gate, brain-ingest-gate), fixture intents rewritten to realistic phrasings, wave-side MECE duplicates removed, the ingest-gate quality rules delegated to conventions, and three new disambiguation rules (publication/article/media/chat-export routing, soul-audit vs context-audit, measurement-first triage vs debugging). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): idea-ingest 1.1.0 — Returns contract + edge cases backport Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): media-ingest 1.1.0 — input contract, error handling, known pitfalls Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): brain-ops 1.1.0 — entity-card first lookup + shallow-ls warning (memory-verbs block preserved) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): maintain 1.1.0 — weekly feature-adoption check Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): data-research 1.1.0 — infer research target from context Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(llms): regenerate bundles after the skills wave RESOLVER.md is inlined into the llms bundles; the wave's routing rows and disambiguation rules regenerate here (single wave-end regen; the committed bundles match HEAD per test/build-llms.test.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): assemble banned-path fixtures at runtime in check-skill-refs tests The privacy check bans those literals in source files; the fixtures now construct them so the test still exercises the donor-remnant gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skillpack): bundle capture + publish; keep host-only contrast fixtures portable The downstream-install smoke showed shipped fixtures referencing host- only skills. capture and publish move from exclusions to the bundle — both are user-facing and wave skills route to them (the eiirp auto-fire files via capture; brain-link-discipline falls back to publish). Contrast fixtures that pointed at host-only skills (smoke-test, skill-optimizer) now target shipped skills with declared ambiguity, so `check-resolvable --strict` is green on the host pack (69 skills) AND on a fresh scaffolded install (58 skills). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): post-import-wave routing receipt (haiku) A/B harness run against the 69-skill resolver after the import wave: baseline (uncompressed) 100% held-out / 78.3% training strict — beating both compression variants on strict scoring, corroborating the review decision to defer resolver compression to its own PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skills): pre-landing review fixes — trust boundaries, gate hardening, registration prose From the six-reviewer pre-landing pass: research-compendium gains the untrusted-content section (verbatim web archives feed future recall); blog-ingest's injection marker now travels with body chunks (inline fenced wrapper, not frontmatter-only); company-brainify's history-rewrite backup moves to a non-volatile private location with pre-push re-verification and retention cleanup; the 17 imported manifest entries get real descriptions (a block-scalar parsing slip) with a test pinning description prose and plugin-array uniqueness; check-skill-refs becomes cwd-independent, validates relative markdown links, and warns visibly on an empty CLI command set (5 new tests); the commit gate fails loudly on a regenerated-but-unstaged lock, single-quotes its inline lint script, and rejects missing lint paths; ladder attribution, checkpoint-hook bypass, frontmatter-contract notes, curation + misroute runbook docs, the brain-first API-pull rule, and negative routing fixtures for eight skills round out the batch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skills): adversarial-review hardening — trust boundaries, closure, honest contracts From the ship pre-landing + adversarial + red-team passes (all findings folded): Untrusted content: a shared conventions/untrusted-content.md (fetched text is data, never instructions) referenced from every skill that fetches or imports external text — fact-check most critically (a fetched page is never authority to rewrite a brain fact). conversation-archive gains a mandatory secret+PII redaction pass before any page is written. company-brainify: the destructive strip now sanitizes a staging copy behind the data-loss-gate card (personal brain untouched); the history purge derives its path set from the full sanitized scope, runs only against the shared repo (never sync.repo_path), stages under a non-volatile backup dir with a pre-push restore check, and logs the deletion to the personal brain, not the shared one. Downstream closure: ask-user and brain-taxonomist are bundled (three new skills delegate to them); remaining host-only references are prose, not links; a membership+closure test makes a bundled skill referencing an unshipped skill a build failure. Honest contracts: brain-link-discipline is marked mutating (it commits and pushes) and only sends its token to the issuing host; inert priority markers removed; citation-graph-ingest's writes_to dropped; concurrency and slug-collision limits documented for daily-task-manager, bulk-ingestion, conversation-archive, and blog-ingest; the self- authorizing shell-jobs prefix removed in favor of worker-side authorization. Gate hardening: the commit gate compares the staged lock blob and runs the plugin-manifest tests; check-skill-refs scans jsonl for donor remnants, tolerates CRLF, and rejects flag-shaped option values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v0.45.6.0 feat(skills): giftable-skills import wave — 17 new production skills + 8 upgrades Grows the built-in pack from ~52 to 69 skills: the trust layer (correction-pipeline, data-loss-gate, fact-check, brain-ingest-gate), team-brain sanitization (company-brainify), typed citation-graph ingestion, disciplined bulk/feed/conversation ingestion, and operational skills (measure-before-you-fix, context-audit, skill-autobench, resolve-before-asking, brain-link-discipline, draft-in-voice, research-compendium), plus meeting-ingestion/skillify/eiirp/ minion-orchestrator/concept-synthesis/reports/briefing/daily-task-manager upgrades and a new skill-ref integrity gate + plugin-curation record. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skillpack,doctor): skill currency + declared preconditions — the migration harness now examines skills The upgrade/migration path was DB-only for skills: schema migrations never touched them, and skill sync was manual/advisory with new skills hidden from the post-upgrade sweep. This makes skill currency and per-skill setup first-class. Currency (capability A): src/core/skillpack/skill-currency.ts classifies every bundled skill against the install as new / drifted / current, by each skill's OWN files (shared conventions on disk no longer misclassify a new skill as drifted). `gbrain skillpack status` reports it; `gbrain skillpack sync` scaffolds only the new ones and never overwrites local edits; the post-upgrade sweep surfaces new skills with the sync command; `gbrain doctor` gains a `skill_currency` check (WARN when new skills are available, drift stays advisory). Preconditions (capability B): a `requires:` frontmatter field (source / dir:<path> / config:<key> / pages:<n>), a pure DI-seam checker in src/core/skillpack/preconditions.ts, `gbrain skillpack setup <skill>` for the static list + hints, and a `skill_preconditions` doctor check that verifies them live against the connected brain. Annotated only the two skills with genuine preconditions (citation-graph-ingest needs a corpus; skill-autobench needs the conversations/ archive). Semantic skill search (capability C) is deliberately deferred as its own subsystem (tenancy + source isolation + embedding backfill) — filed in TODOS, not crammed in unreviewed. Tests: skill-currency, skill-preconditions, skillpack-status (31 new assertions). doctor + skillpack stay engine-safe (currency is filesystem- only; live precondition checks skip cleanly without a brain). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(doctor,skillpack): categorize new skill checks, refresh flag registry, sweep test + noise gate CI on the pushed feature surfaced three real in-branch failures: - doctor-categories: register skill_currency + skill_preconditions under SKILL_CHECK_NAMES (every doctor check must be categorized). - flag registry: doctor's import of findGbrainRoot pulls bundle.ts (which runs git rev-parse --verify) into its flag scan; regenerated the known-flags allowlist to match (harmless over-inclusion, per the generator's design). - upgrade sweep: header renamed to 'Skillpack sweep (post-upgrade)'; new-skill surfacing gated on the host having scaffolded at least one skill, so a zero-skill (opted-out) host stays silent instead of getting 'N new skills' on every upgrade. Tests updated + a case added locking the surfacing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
244 lines
11 KiB
JavaScript
Executable File
244 lines
11 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
// check-skill-refs — three integrity gates over the skills/ markdown tree.
|
|
//
|
|
// 1. DANGLING REFS (fail): every backtick `skills/<x>/...` path, every
|
|
// relative markdown link (`](./x.md)` / `](../x/y.md)`), every frontmatter
|
|
// `composes:` slug, and every `(dispatcher for: a, b)` slug in RESOLVER.md
|
|
// must resolve to an existing file/dir. Placeholder templates
|
|
// (`skills/X/`, `skills/<slug>/`, `{...}` / `<...>` targets, example-slug
|
|
// brain-page paths like `../people/alice-example.md`) and
|
|
// skills/migrations/** are exempt — migrations are historical record,
|
|
// placeholders are documentation idiom.
|
|
// 2. DONOR REMNANTS (fail, allowlist-ratcheted): donor-workspace path prefixes
|
|
// must not appear outside files listed in scripts/skill-refs-allowlist.txt.
|
|
// The allowlist is a ratchet: it may shrink, never silently grow — add a
|
|
// line only with a review-visible commit.
|
|
// 3. CLI REFS (warn only): `gbrain <cmd>` tokens inside fenced code blocks are
|
|
// checked against the CLI's --tools-json surface. Warnings never fail the
|
|
// build; they exist so a skill body promising a nonexistent command is
|
|
// visible in CI logs before a user hits it.
|
|
//
|
|
// Usage: bun scripts/check-skill-refs.mjs [--skills-dir skills/] [--allowlist scripts/skill-refs-allowlist.txt] [--no-cli-refs]
|
|
|
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
import { join, relative, dirname } from 'node:path';
|
|
import { execSync } from 'node:child_process';
|
|
|
|
const args = process.argv.slice(2);
|
|
function argVal(flag, dflt) {
|
|
const i = args.indexOf(flag);
|
|
const v = i >= 0 ? args[i + 1] : undefined;
|
|
// A value that looks like a flag (starts with --) means this option's value
|
|
// was omitted; treat it as missing rather than swallowing the next flag.
|
|
return v && !v.startsWith('--') ? v : dflt;
|
|
}
|
|
const SKILLS_DIR = argVal('--skills-dir', 'skills');
|
|
const ALLOWLIST_PATH = argVal('--allowlist', 'scripts/skill-refs-allowlist.txt');
|
|
const RUN_CLI_REFS = !args.includes('--no-cli-refs');
|
|
|
|
const DONOR_PREFIXES = ['/data/brain', '/data/.openclaw', '/data/gbrain', '/data/tmp'];
|
|
const PLACEHOLDER_RE = /skills\/(X|<[^>]+>|\{[^}]+\}|\$\{[^}]+\}|\.\.\.)\/?/;
|
|
|
|
// Relative-markdown-link exemptions: skill bodies illustrate BRAIN-repo page
|
|
// links (`[Alice Example](../people/alice-example.md)`). Those targets live in
|
|
// a brain repo, not the skills tree — any relative target whose first real
|
|
// path segment is a brain-content top-level dir is a documentation example,
|
|
// not a skills cross-link. Example-slug segments (`*-example`) are likewise
|
|
// placeholders per the privacy rule.
|
|
const BRAIN_CONTENT_DIRS = new Set([
|
|
'people', 'companies', 'meetings', 'daily', 'concepts', 'sources',
|
|
'research', 'projects', 'media', 'conversations', 'analysis', 'notes',
|
|
'ideas', 'takes', 'funds', 'deals',
|
|
]);
|
|
function isPlaceholderLinkTarget(target) {
|
|
if (/[<{$]/.test(target)) return true; // <slug>, {slug}, ${var} templates
|
|
const segs = target.split('/').filter((s) => s && s !== '.' && s !== '..');
|
|
if (segs.length === 0) return true;
|
|
if (BRAIN_CONTENT_DIRS.has(segs[0])) return true; // brain-page path example
|
|
if (segs.some((s) => /-example(\.|\/|$)/.test(s))) return true; // alice-example, acme-example, ...
|
|
return false;
|
|
}
|
|
|
|
function walk(dir) {
|
|
const out = [];
|
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
const p = join(dir, e.name);
|
|
if (e.isDirectory()) out.push(...walk(p));
|
|
// .md feeds every lane; .jsonl feeds the donor-remnant scan only —
|
|
// routing-eval fixtures can carry a donor-workspace path too.
|
|
else if (e.name.endsWith('.md') || e.name.endsWith('.jsonl')) out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if (!existsSync(SKILLS_DIR)) {
|
|
console.error(`check-skill-refs: skills dir not found: ${SKILLS_DIR}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const allowlist = new Set(
|
|
existsSync(ALLOWLIST_PATH)
|
|
? readFileSync(ALLOWLIST_PATH, 'utf8')
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.filter((l) => l && !l.startsWith('#'))
|
|
: [],
|
|
);
|
|
|
|
const files = walk(SKILLS_DIR);
|
|
const failures = [];
|
|
const warnings = [];
|
|
|
|
for (const file of files) {
|
|
// Path identity is always "skills/<path-under-skills-dir>", independent of cwd
|
|
// canonicalization (macOS /var vs /private/var) or an absolute --skills-dir.
|
|
const underSkills = relative(SKILLS_DIR, file);
|
|
const rel = join('skills', underSkills);
|
|
const inMigrations = underSkills.startsWith('migrations/');
|
|
const text = readFileSync(file, 'utf8');
|
|
|
|
// --- 2. donor remnants (skip migrations wholesale) ---
|
|
if (!inMigrations && !allowlist.has(rel)) {
|
|
for (const prefix of DONOR_PREFIXES) {
|
|
if (text.includes(prefix)) {
|
|
const line = text.split('\n').findIndex((l) => l.includes(prefix)) + 1;
|
|
failures.push(`[donor-remnant] ${rel}:${line} — contains "${prefix}" (add to ${ALLOWLIST_PATH} only with review)`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (inMigrations) continue;
|
|
|
|
// The lanes below are markdown-only (backtick refs, relative md-links,
|
|
// frontmatter). .jsonl files are scanned for donor remnants above only.
|
|
if (!file.endsWith('.md')) continue;
|
|
|
|
// --- 1a. backtick skills/ path refs ---
|
|
for (const m of text.matchAll(/`(skills\/[^`\s]+)`/g)) {
|
|
let ref = m[1].replace(/[.,;:]+$/, '');
|
|
if (PLACEHOLDER_RE.test(ref)) continue;
|
|
// strip trailing anchors / line refs like skills/foo/SKILL.md:12
|
|
ref = ref.replace(/:\d+(-\d+)?$/, '').replace(/#.*$/, '');
|
|
if (ref.endsWith('/')) ref = ref.slice(0, -1);
|
|
// Resolve against the PARENT of the skills dir (refs are written as
|
|
// "skills/<x>/..."), never bare cwd — the check must be cwd-independent.
|
|
if (!existsSync(join(SKILLS_DIR, '..', ref))) {
|
|
const line = text.split('\n').findIndex((l) => l.includes(m[1])) + 1;
|
|
failures.push(`[dangling-ref] ${rel}:${line} — \`${m[1]}\` does not exist`);
|
|
}
|
|
}
|
|
|
|
// --- 1d. relative markdown links ---
|
|
// `](./x.md)` / `](../x/y.md)` targets must resolve against the linking
|
|
// file's own directory. http(s) and anchor-only targets never match the
|
|
// leading ./ or ../ pattern; placeholder/example targets are exempt.
|
|
for (const m of text.matchAll(/\]\((\.{1,2}\/[^)\s]+)\)/g)) {
|
|
const raw = m[1];
|
|
const target = raw.split('#')[0];
|
|
if (!target) continue; // anchor-only after a ./ prefix — nothing to resolve
|
|
if (isPlaceholderLinkTarget(target)) continue;
|
|
if (!existsSync(join(dirname(file), target))) {
|
|
const line = text.split('\n').findIndex((l) => l.includes(raw)) + 1;
|
|
failures.push(`[dangling-md-link] ${rel}:${line} — \`](${raw})\` does not resolve from ${rel}'s directory`);
|
|
}
|
|
}
|
|
|
|
// --- 1b. frontmatter composes: slugs ---
|
|
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
if (fmMatch) {
|
|
const fm = fmMatch[1];
|
|
const composesMatch = fm.match(/^composes:\s*(.*)$/m);
|
|
if (composesMatch) {
|
|
const inline = composesMatch[1].trim();
|
|
let slugs = [];
|
|
if (inline && inline !== '|' && !inline.startsWith('#')) {
|
|
slugs = inline.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
} else {
|
|
// block-list form: lines " - slug" following the key
|
|
const after = fm.slice(fm.indexOf(composesMatch[0]) + composesMatch[0].length);
|
|
for (const line of after.split(/\r?\n/)) {
|
|
const lm = line.match(/^\s+-\s+(\S+)/);
|
|
if (lm) slugs.push(lm[1]);
|
|
else if (line.trim() && !line.startsWith(' ')) break;
|
|
}
|
|
}
|
|
for (const slug of slugs) {
|
|
if (!existsSync(join(SKILLS_DIR, slug))) {
|
|
failures.push(`[dangling-composes] ${rel} — composes: "${slug}" is not a skill dir under ${SKILLS_DIR}/`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 1c. RESOLVER.md dispatcher clauses ---
|
|
const resolverPath = join(SKILLS_DIR, 'RESOLVER.md');
|
|
if (existsSync(resolverPath)) {
|
|
const rtext = readFileSync(resolverPath, 'utf8');
|
|
for (const m of rtext.matchAll(/\(dispatcher for:\s*([^)]+)\)/g)) {
|
|
for (const slug of m[1].split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
const cleaned = slug.replace(/`/g, '');
|
|
if (!/^[a-z0-9-]+$/.test(cleaned)) continue; // prose, not a slug
|
|
if (!existsSync(join(SKILLS_DIR, cleaned))) {
|
|
failures.push(`[dangling-dispatcher] ${SKILLS_DIR}/RESOLVER.md — dispatcher slug "${cleaned}" is not a skill dir`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 3. CLI refs (warn-only) ---
|
|
if (RUN_CLI_REFS) {
|
|
let known = null;
|
|
try {
|
|
const raw = execSync('bun src/cli.ts --tools-json 2>/dev/null', { encoding: 'utf8', timeout: 30_000 });
|
|
const parsed = JSON.parse(raw.slice(raw.indexOf('[') >= 0 && raw.indexOf('[') < (raw.indexOf('{') + 1 || Infinity) ? raw.indexOf('[') : raw.indexOf('{')));
|
|
const list = Array.isArray(parsed) ? parsed : parsed.tools || [];
|
|
known = new Set();
|
|
for (const t of list) {
|
|
const n = (t.cliHints && t.cliHints.name) || t.cli_name || t.name;
|
|
if (n) known.add(String(n).replaceAll('_', '-'));
|
|
for (const a of (t.cliHints && t.cliHints.aliases) || []) known.add(String(a));
|
|
}
|
|
} catch {
|
|
warnings.push('[cli-refs] could not load --tools-json; skipping CLI-ref check');
|
|
}
|
|
if (known && known.size === 0) {
|
|
warnings.push('[cli-refs] --tools-json parsed to an EMPTY command set; skipping CLI-ref check (the warn-only lane is not running)');
|
|
}
|
|
if (known && known.size > 0) {
|
|
// top-level commands defined directly in src/cli.ts (not ops): derive from source
|
|
try {
|
|
const cliSrc = readFileSync('src/cli.ts', 'utf8');
|
|
for (const m of cliSrc.matchAll(/(?:command === |case )'([a-z][a-z0-9-]*)'/g)) known.add(m[1]);
|
|
} catch {}
|
|
// ops cliHints that --tools-json does not serialize: read them from source
|
|
try {
|
|
const opsSrc = readFileSync('src/core/operations.ts', 'utf8');
|
|
for (const m of opsSrc.matchAll(/cliHints:\s*\{\s*name:\s*'([a-z][a-z0-9-]*)'/g)) known.add(m[1]);
|
|
for (const m of opsSrc.matchAll(/aliases:\s*\[([^\]]*)\]/g)) {
|
|
for (const a of m[1].matchAll(/'([a-z][a-z0-9-]*)'/g)) known.add(a[1]);
|
|
}
|
|
} catch {}
|
|
for (const file of files) {
|
|
if (file.includes('/migrations/')) continue;
|
|
if (!file.endsWith('.md')) continue; // fenced gbrain-cmd scan is markdown-only
|
|
const rel = relative('.', file);
|
|
const text = readFileSync(file, 'utf8');
|
|
for (const block of text.matchAll(/```[a-z]*\n([\s\S]*?)```/g)) {
|
|
for (const cmd of block[1].matchAll(/(?:^|[|&;(]\s*)gbrain\s+([a-z][a-z0-9-]*)/gm)) {
|
|
if (!known.has(cmd[1])) warnings.push(`[cli-refs] ${rel} — \`gbrain ${cmd[1]}\` not found in CLI surface (warn-only)`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const w of warnings) console.error(`WARN ${w}`);
|
|
if (failures.length) {
|
|
for (const f of failures) console.error(`FAIL ${f}`);
|
|
console.error(`check-skill-refs: ${failures.length} failure(s), ${warnings.length} warning(s)`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`check-skill-refs: OK (${files.length} files scanned, ${warnings.length} warning(s))`);
|