mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +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>
245 lines
9.5 KiB
TypeScript
245 lines
9.5 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const SCRIPT = join(import.meta.dir, '..', '..', 'scripts', 'check-skill-refs.mjs');
|
|
const REPO_ROOT = join(import.meta.dir, '..', '..');
|
|
|
|
function runOn(
|
|
setup: (dir: string) => void,
|
|
allowlist = '',
|
|
opts: { cwd?: (dir: string) => string; cliRefs?: boolean } = {},
|
|
): { code: number; out: string } {
|
|
const dir = mkdtempSync(join(tmpdir(), 'skill-refs-'));
|
|
try {
|
|
const skills = join(dir, 'skills');
|
|
mkdirSync(skills, { recursive: true });
|
|
setup(skills);
|
|
const allowPath = join(dir, 'allow.txt');
|
|
writeFileSync(allowPath, allowlist);
|
|
const args = [SCRIPT, '--skills-dir', skills, '--allowlist', allowPath];
|
|
if (!opts.cliRefs) args.push('--no-cli-refs');
|
|
const res = spawnSync('bun', args, {
|
|
encoding: 'utf8',
|
|
cwd: opts.cwd ? opts.cwd(dir) : dir,
|
|
});
|
|
return { code: res.status ?? -1, out: `${res.stdout}\n${res.stderr}` };
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe('check-skill-refs', () => {
|
|
test('passes on a clean tree with resolving refs and placeholders', () => {
|
|
const { code, out } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
mkdirSync(join(skills, 'beta'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), '---\nname: alpha\n---\nSee `skills/beta/SKILL.md` and the template `skills/<slug>/SKILL.md` and `skills/{name}/SKILL.md`.\n');
|
|
writeFileSync(join(skills, 'beta', 'SKILL.md'), '---\nname: beta\n---\nbody\n');
|
|
});
|
|
expect(out).toContain('OK');
|
|
expect(code).toBe(0);
|
|
});
|
|
|
|
test('fails on a dangling backtick skills/ path', () => {
|
|
const { code, out } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), 'Read `skills/ghost/SKILL.md` for details.\n');
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(out).toContain('dangling-ref');
|
|
});
|
|
|
|
test('fails on a composes: slug that is not a skill dir', () => {
|
|
const { code, out } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), '---\nname: alpha\ncomposes: ghost-skill, alpha\n---\nbody\n');
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(out).toContain('dangling-composes');
|
|
expect(out).toContain('ghost-skill');
|
|
});
|
|
|
|
test('fails on a dangling dispatcher slug in RESOLVER.md', () => {
|
|
const { code, out } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), 'body\n');
|
|
writeFileSync(join(skills, 'RESOLVER.md'), '| "do things" (dispatcher for: alpha, ghost) | `skills/alpha/SKILL.md` |\n');
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(out).toContain('dangling-dispatcher');
|
|
expect(out).toContain('ghost');
|
|
});
|
|
|
|
test('fails on a donor path outside the allowlist, passes when allowlisted', () => {
|
|
// Assemble the banned prefix at runtime so this test file itself passes
|
|
// the repo-wide privacy check (which bans the literal in source files).
|
|
const bannedPath = ['/data', 'brain', 'notes.md'].join('/');
|
|
const setup = (skills: string) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), `Writes go to ${bannedPath}\n`);
|
|
};
|
|
const fail = runOn(setup);
|
|
expect(fail.code).toBe(1);
|
|
expect(fail.out).toContain('donor-remnant');
|
|
const pass = runOn(setup, 'skills/alpha/SKILL.md\n');
|
|
expect(pass.code).toBe(0);
|
|
});
|
|
|
|
test('scans .jsonl (routing-eval) files for donor remnants; allowlist ratchets them too', () => {
|
|
// Assemble the banned prefix at runtime (repo-wide privacy check bans the literal).
|
|
const bannedPath = ['/data', 'brain', 'fixture.md'].join('/');
|
|
const setup = (skills: string) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), '---\nname: alpha\n---\nclean body\n');
|
|
writeFileSync(
|
|
join(skills, 'alpha', 'routing-eval.jsonl'),
|
|
`{"utterance":"do a thing","expected":"alpha","note":"${bannedPath}"}\n`,
|
|
);
|
|
};
|
|
const fail = runOn(setup);
|
|
expect(fail.code).toBe(1);
|
|
expect(fail.out).toContain('donor-remnant');
|
|
expect(fail.out).toContain('routing-eval.jsonl');
|
|
const pass = runOn(setup, 'skills/alpha/routing-eval.jsonl\n');
|
|
expect(pass.code).toBe(0);
|
|
});
|
|
|
|
test('a .jsonl file is not subject to the markdown-only lanes (backtick/composes)', () => {
|
|
// A dangling `skills/ghost/...` inside a .jsonl string must NOT fail — the
|
|
// dangling-ref lane is markdown-only; only donor remnants scan .jsonl.
|
|
const { code, out } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), '---\nname: alpha\n---\nclean body\n');
|
|
writeFileSync(
|
|
join(skills, 'alpha', 'routing-eval.jsonl'),
|
|
'{"utterance":"read `skills/ghost/SKILL.md`","expected":"alpha"}\n',
|
|
);
|
|
});
|
|
expect(code).toBe(0);
|
|
expect(out).toContain('OK');
|
|
});
|
|
|
|
test('exempts skills/migrations wholesale', () => {
|
|
const { code } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'migrations'));
|
|
writeFileSync(join(skills, 'migrations', 'v0.1.0.md'), `Old world: ${['/data', 'brain'].join('/')} and \`skills/long-gone/SKILL.md\`\n`);
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), 'clean body\n');
|
|
});
|
|
expect(code).toBe(0);
|
|
});
|
|
|
|
test('fails on a block-list composes: form with a ghost slug', () => {
|
|
const { code, out } = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(
|
|
join(skills, 'alpha', 'SKILL.md'),
|
|
'---\nname: alpha\ncomposes:\n - ghost-two\n - alpha\n---\nbody\n',
|
|
);
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(out).toContain('dangling-composes');
|
|
expect(out).toContain('ghost-two');
|
|
});
|
|
|
|
test('exits 2 when --skills-dir does not exist', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'skill-refs-'));
|
|
try {
|
|
const res = spawnSync('bun', [SCRIPT, '--skills-dir', join(dir, 'nope'), '--no-cli-refs'], {
|
|
encoding: 'utf8',
|
|
cwd: dir,
|
|
});
|
|
expect(res.status).toBe(2);
|
|
expect(`${res.stdout}\n${res.stderr}`).toContain('skills dir not found');
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('is cwd-independent: same results when run from a sibling dir', () => {
|
|
const setup = (skills: string) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
mkdirSync(join(skills, 'beta'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), 'See `skills/beta/SKILL.md`.\n');
|
|
writeFileSync(join(skills, 'beta', 'SKILL.md'), 'body\n');
|
|
};
|
|
const fromParent = runOn(setup);
|
|
const fromSibling = runOn(setup, '', {
|
|
cwd: (dir) => {
|
|
const sibling = join(dir, 'elsewhere');
|
|
mkdirSync(sibling, { recursive: true });
|
|
return sibling;
|
|
},
|
|
});
|
|
expect(fromParent.code).toBe(0);
|
|
expect(fromSibling.code).toBe(0);
|
|
|
|
// Dangling refs must ALSO fail identically regardless of cwd.
|
|
const dangling = (skills: string) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), 'See `skills/ghost/SKILL.md`.\n');
|
|
};
|
|
const dParent = runOn(dangling);
|
|
const dSibling = runOn(dangling, '', {
|
|
cwd: (dir) => {
|
|
const sibling = join(dir, 'elsewhere');
|
|
mkdirSync(sibling, { recursive: true });
|
|
return sibling;
|
|
},
|
|
});
|
|
expect(dParent.code).toBe(1);
|
|
expect(dSibling.code).toBe(1);
|
|
expect(dSibling.out).toContain('dangling-ref');
|
|
});
|
|
|
|
test('fails on a dangling relative markdown link; skips placeholder link targets', () => {
|
|
const fail = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(join(skills, 'alpha', 'SKILL.md'), 'Read [the missing doc](./missing.md) first.\n');
|
|
});
|
|
expect(fail.code).toBe(1);
|
|
expect(fail.out).toContain('dangling-md-link');
|
|
|
|
const pass = runOn((skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
mkdirSync(join(skills, 'beta'));
|
|
writeFileSync(join(skills, 'beta', 'SKILL.md'), 'body\n');
|
|
writeFileSync(
|
|
join(skills, 'alpha', 'SKILL.md'),
|
|
[
|
|
'Real link: [beta](../beta/SKILL.md).',
|
|
'Brain-page example: [Alice Example](../people/alice-example.md).',
|
|
'Company example: [Acme](../companies/acme-example.md).',
|
|
'Template: [any skill](../<slug>/SKILL.md) and [var]({topic}/index.md).',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
});
|
|
expect(pass.code).toBe(0);
|
|
expect(pass.out).toContain('OK');
|
|
});
|
|
|
|
test('cli-refs warn lane: unknown gbrain command in a fenced block warns without failing', () => {
|
|
// The script derives the known-command set from src/cli.ts +
|
|
// src/core/operations.ts of the CWD repo, so this one runs from the real
|
|
// repo root against a temp skills dir.
|
|
const { code, out } = runOn(
|
|
(skills) => {
|
|
mkdirSync(join(skills, 'alpha'));
|
|
writeFileSync(
|
|
join(skills, 'alpha', 'SKILL.md'),
|
|
'Run:\n\n```bash\ngbrain not-a-real-command --flag\n```\n',
|
|
);
|
|
},
|
|
'',
|
|
{ cliRefs: true, cwd: () => REPO_ROOT },
|
|
);
|
|
expect(out).toContain('[cli-refs]');
|
|
expect(out).toContain('not-a-real-command');
|
|
expect(code).toBe(0); // warn-only lane never fails the build
|
|
}, 30_000);
|
|
});
|