diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8ccfb2f..77bc408ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ All notable changes to GBrain will be documented in this file. +## [0.41.7.0] - 2026-05-24 + +**Your compact OpenClaw resolver actually works now.** If you've grown your agent past 200 skills and switched to the compact list-format resolver (`- **gift-advisor**: gift idea | birthday gift`) because the markdown-table version got unreadable, `gbrain doctor` used to silently report every single skill as unreachable. On a 306-skill agent, that was 238 FAIL errors on every doctor run, and the list-format resolver was effectively invisible to gbrain. v0.41.7.0 fixes that — the parser now reads both shapes natively, mixes them in one file, and `gbrain doctor` reports 0 errors on the same resolver that previously broke it. + +This release ships alongside a new guide explaining when and why you actually want a list-format resolver: the [three-tier scaling architecture](docs/guides/scaling-skills.md) that gets a 300-skill agent down to ~4K tokens per turn (from ~25K) with zero capability loss. The parser fix is the line of code that makes the resolver-as-router tier work for any agent that writes the compact format. + +### How to take advantage of v0.41.7.0 + +`gbrain upgrade`. The parser handles both formats automatically; no migration needed. The tutorial at [`docs/guides/scaling-skills.md`](docs/guides/scaling-skills.md) explains when and why to tier your skills. + +### What you'd see (before vs after) + +On a real 306-skill agent with a list-format `RESOLVER.md`: + +| `gbrain doctor` output | Before v0.41.7.0 | After v0.41.7.0 | +|---|---|---| +| `resolver_health` status | FAIL | OK | +| `unreachable` skills | 238 | 0 | +| Per-skill error noise | every skill named | none | + +### Things to watch + +- **Skill names in list format must be kebab-lowercase** (`gift-advisor`, `flight-tracker`). Bold names that start with an uppercase letter (`**MyTool**`) are silently skipped by the new parser. This is the trade that kills the prose-bullet false-positive class (`- **Note**: see [link]` in a real AGENTS.md no longer gets parsed as a skill row). If a skill stops appearing after upgrade, lowercase the name. +- **The path suffix is parsed but stripped.** A list entry like `- **quality**: lint -> \`skills/conventions/quality.md\`` produces `skills/quality/SKILL.md` (derived from the name), NOT the explicit path. Two downstream consumers (`routing-eval.ts` and the manifest check) assume the `skills//SKILL.md` shape; honoring the explicit path would silently break their coverage. For non-conventional skill paths, use the table format (which has always supported them). +- **Mixed table + list in one file works.** The v0.31.7 multi-resolver merge (skillpack `skills/RESOLVER.md` + workspace `../AGENTS.md`) folds both shapes into one unified entry stream, deduped by skillPath. + +### Itemized changes + +- New: `src/core/check-resolvable.ts` `parseResolverEntries` accepts compact list-format resolvers alongside the existing table format. Both shapes can mix in one file. The list-branch contract is documented inline. +- New: `test/check-resolvable-openclaw-compact.test.ts` regression suite (8 cases across two fixtures) pinning the "238 FAILs → 0" headline outcome plus the D-CX-14 mixed-merge case. +- New: `test/fixtures/openclaw-compact-resolver/` (~10 skills with valid frontmatter triggers, plus a prose-bullet section pinning the kebab-lowercase regex tighten). +- New: `test/fixtures/openclaw-mixed-merge/` (table-format `skills/RESOLVER.md` + compact `../AGENTS.md`). +- New: `docs/guides/scaling-skills.md` — walkthrough of the three-tier scaling architecture (always-loaded, resolver-routed, dormant), the per-turn token math, and the compact list-format spec. Registered in `scripts/llms-config.ts` so `bun run build:llms` regenerates `llms-full.txt` correctly. +- Extended: `test/check-resolvable.test.ts` — 11 new unit cases covering bold-name single + multi-trigger fan-out, plain-name fallback, Unicode and ASCII path-suffix strip, ellipsis filter, empty pipe segments, mixed shapes, section tracking across list entries, and two D4-regression cases (prose-bullet rejection + convention-violation negative). +- Credit `@garrytan-agents` for the original PR #1370 submission that flagged the parser gap. The v0.41.7.0 implementation expands the scope to close the prose-bullet false-positive class (`[a-z][a-z0-9-]+` name regex), pin the regression at the integration layer (two fixtures), and ship the docs context (the scaling-skills guide). + +### For contributors + +- Codex outside-voice review surfaced four findings the eng review missed: the upstream PR's literal "add list branch below" instruction would have produced dead code (the existing `continue` short-circuits non-table rows before any list branch would fire); the explicit-path-suffix capture would have broken `routing-eval.ts:skillSlugFromPath` + the manifest check at `check-resolvable.ts:367`; the `docs/guides/scaling-skills.md` doc needed registration in `scripts/llms-config.ts` for `test/build-llms.test.ts` to pass; and the original em-dash cleanup instruction would have corrupted command flags like `--pglite`. All four caught and integrated pre-merge. ## [0.41.6.0] - 2026-05-25 **Your PR CI stops taking 23 minutes.** The Test workflow on every pull request was wallclock-bound by one unlucky shard (shard 3, 22-26 min) while other shards finished in 5-13 min. After this release, every shard finishes in roughly the same time, the whole matrix runs in about 9-10 min on a fresh PR, and PRs that don't actually change test-affecting code (rebases, doc-only commits, retries) skip the test run entirely and finish in under 2 minutes via a content-hash cache. @@ -594,6 +633,7 @@ What we caught and fixed before merging. Three independent reviews + three codex ``` 5. **If any step fails or the numbers look wrong,** please file an issue: https://github.com/garrytan/gbrain/issues with `gbrain doctor --json` output. + ## [0.40.10.0] - 2026-05-24 **Your brain stops accepting junk pages, and oversize content stops crashing the embedder.** A page from one of your source repos can no longer break embedding, defeat search, or pollute your knowledge graph just because it's a Cloudflare challenge dump or an absurdly large file. The new sanity gate lives at the narrow waist of ingestion, so every path that writes pages — sync, capture, `put_page` MCP, the `/ingest` webhook — picks it up uniformly. diff --git a/CLAUDE.md b/CLAUDE.md index 58d398cef..23aa97dfc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,7 +135,7 @@ strict behavior when unset. - `src/core/minions/queue.ts` extension (v0.31.12) — `MinionQueue.add()` now rejects `subagent` jobs whose `data.model` resolves through `isAnthropicProvider()` to a non-Anthropic provider. Lazy-imports `model-config.ts` to avoid pulling engine types into queue's eager-load surface. Layer 1 of the three-layer subagent provider enforcement (Codex F1+F2 in plan review). Layers 2 + 3 live in `src/core/model-config.ts` (`enforceSubagentAnthropic` runtime fallback) and `src/commands/doctor.ts` (`subagent_provider` check). Pinned by 3 cases in `test/agent-cli.test.ts`. - `src/commands/models.ts` (v0.31.12) — `gbrain models [--json]` read-only routing dashboard: prints tier defaults (`utility`/`reasoning`/`deep`/`subagent`), the resolved value for each (re-walking the resolution chain to attribute properly), every per-task override (11 `PER_TASK_KEYS` entries — `models.dream.synthesize`, `models.dream.patterns`, `models.drift`, `models.auto_think`, `models.think`, `models.subagent`, `facts.extraction_model`, `models.eval.longmemeval`, `models.expansion`, `models.chat`, `models.dream.synthesize_verdict`), the alias map (defaults + user overrides), and a source-of-truth column showing `default` / `config: ` / `env: `. `gbrain models doctor [--skip=] [--json]` fires a 1-token `gateway.chat()` probe against each configured chat + expansion model and classifies failures into `{model_not_found, auth, rate_limit, network, unknown}` — the structural fix for the v0.31.6 silent-no-op bug class. Wired into `cli.ts` dispatch table + `CLI_ONLY` set. **v0.33.1.1 (#962, Codex P3 follow-up):** doctor gains a zero-token `embedding_config` probe that runs FIRST, before any chat/expansion probes spend money. `probeEmbeddingConfig()` reads `getEmbeddingModel()` + `getEmbeddingDimensions()` from the gateway, parses the model id, and (for Voyage flexible-dim models) checks `isValidVoyageOutputDim(dims)` against `VOYAGE_VALID_OUTPUT_DIMS`. New `ProbeStatus` variant `'config'` and optional `fix?: string` field on `ProbeResult` — surfaced in both human output (paste-ready `gbrain config set ...` line under the bad probe) and JSON output. New touchpoint label `'embedding_config'` joins `'chat'` and `'expansion'` in the probe-row taxonomy. Closes the Voyage flexible-dim bug class at config time, not first-embed. - `src/commands/doctor.ts` extension (v0.31.12) — new `subagent_provider` check (layer 3 of 3 — Codex F13). Warns when `models.tier.subagent` is explicitly set to a non-Anthropic provider (fail-loud since the user clearly meant it — message names the bad value and prints the paste-ready fix command `gbrain config set models.tier.subagent anthropic:claude-sonnet-4-6`); also warns when `models.default` would sneak `subagent` into a non-Anthropic provider via tier inheritance. OK status when subagent tier resolves to Anthropic. Tests cover all three paths in `test/doctor.test.ts`. -- `src/core/check-resolvable.ts` — Resolver validation: reachability, MECE overlap, DRY checks, structured fix objects. v0.14.1: `CROSS_CUTTING_PATTERNS.conventions` is an array (notability gate accepts both `conventions/quality.md` and `_brain-filing-rules.md`). New `extractDelegationTargets()` parses `> **Convention:**`, `> **Filing rule:**`, and inline backtick references. DRY suppression is proximity-based via `DRY_PROXIMITY_LINES = 40`. +- `src/core/check-resolvable.ts` — Resolver validation: reachability, MECE overlap, DRY checks, structured fix objects. v0.14.1: `CROSS_CUTTING_PATTERNS.conventions` is an array (notability gate accepts both `conventions/quality.md` and `_brain-filing-rules.md`). New `extractDelegationTargets()` parses `> **Convention:**`, `> **Filing rule:**`, and inline backtick references. DRY suppression is proximity-based via `DRY_PROXIMITY_LINES = 40`. **v0.41.7.0:** `parseResolverEntries` accepts a second compact list format alongside the original markdown table (`- **skill-name**: trigger1 | trigger2 | trigger3` or `- skill-name: trigger1 | trigger2`). Both shapes can mix in one file; the v0.31.7 multi-resolver merge folds them into one entry stream. Skill name MUST be kebab-lowercase (regex `[a-z][a-z0-9-]+`) so prose bullets like `- **Note**: …`, `- **Convention**: …`, `- **TODO**: …` in real-world AGENTS.md files don't false-match as skill rows (codex F2 / D4). `skillPath` is ALWAYS derived as `skills//SKILL.md`: an optional `→ \`skills/path\`` (or ASCII `->`) suffix is stripped from the trigger string but NOT honored as the path — two downstream consumers (`routing-eval.ts:skillSlugFromPath`, the manifest lookup at `:367`) assume the convention, and honoring the explicit path would silently break their coverage. For non-conventional paths, use the table format. Multi-trigger rows fan out to one entry per trigger sharing the same `skillPath`; `checkResolvable` dedupes downstream so the reachability count counts each skill once. Closes the OpenClaw bug class where a 306-skill agent's list-format resolver registered 238 FAILs ("skill not reachable from RESOLVER.md") on every `gbrain doctor` run — productionized from upstream PR #1370 by @garrytan-agents. Pinned by 11 new unit cases in `test/check-resolvable.test.ts` (bold + plain forms, Unicode + ASCII suffix strip, ellipsis filter, empty pipe segments, mixed shapes, prose-bullet rejection regression) and the 8-case integration suite in `test/check-resolvable-openclaw-compact.test.ts` over two fixtures: `test/fixtures/openclaw-compact-resolver/` (10 skills in pure compact form) + `test/fixtures/openclaw-mixed-merge/` (table-format `skills/RESOLVER.md` + compact `../AGENTS.md` for the v0.31.7 merge contract). User-facing tutorial: `docs/guides/scaling-skills.md` walks through when and why to switch to the compact format (the three-tier scaling architecture that gets a 300-skill agent down to ~4K tokens per turn from ~25K with zero capability loss). - `src/core/repo-root.ts` — Shared `findRepoRoot(startDir?)` (v0.16.4): walks up from `startDir` (default `process.cwd()`) looking for `skills/RESOLVER.md`. Zero-dependency module imported by both `doctor.ts` and `check-resolvable.ts`. Parameterized `startDir` makes tests hermetic. **v0.31.7:** read-path / write-path split. `autoDetectSkillsDir` (shared, read+write-safe) gains tier-0 `$GBRAIN_SKILLS_DIR` explicit operator override (Docker mounts, CI, monorepo subdirs) ahead of the existing 4-tier chain. New `autoDetectSkillsDirReadOnly` wraps it with a tier-5 install-path fallback that walks up from `fileURLToPath(import.meta.url)` and gates on `isGbrainRepoRoot` so unrelated repos can't false-positive. Read-path callers (`doctor`, `check-resolvable`, `routing-eval`) use the read-only variant; write-path callers (`skillpack install`, `skillify scaffold`, `post-install-advisory`) deliberately stay on the shared function so `gbrain skillpack install` from `~` cannot silently retarget the bundled gbrain repo's `skills/` instead of the user's actual workspace. Two new `SkillsDirSource` variants: `'env_explicit'`, `'install_path'`. New `AUTO_DETECT_HINT_READ_ONLY` documents the extra tier. The D6 `--fix` safety gate in `doctor.ts` + `check-resolvable.ts` refuses auto-repair when `detected.source === 'install_path'` so `gbrain doctor --fix` from `~` cannot silently rewrite the bundled install tree. - `src/commands/check-resolvable.ts` — Standalone CLI wrapper (v0.16.4) over `checkResolvable()`. Exports `parseFlags`, `resolveSkillsDir`, `DEFERRED`, `runCheckResolvable`. Exit rule: **1 on any issue (warnings OR errors)**, stricter than doctor's `ok` flag — honors README:259. Stable JSON envelope `{ok, skillsDir, report, autoFix, deferred, error, message}` — same shape on success and error paths. `--fix` path runs `autoFixDryViolations` BEFORE `checkResolvable` (same ordering as doctor). `scripts/skillify-check.ts` subprocess-calls `gbrain check-resolvable --json` (cached per process) and fails loud on binary-missing — no silent false-pass. **v0.19:** AGENTS.md workspaces now resolve natively (see `src/core/resolver-filenames.ts`) — gbrain inspects the 107-skill OpenClaw deployment whether the routing file is `RESOLVER.md` or `AGENTS.md`. `DEFERRED[]` is empty — Checks 5 + 6 shipped as real code, not issue URLs. **v0.31.7:** the resolver lookup switched from first-match-wins to the multi-file merge in `src/core/check-resolvable.ts` — entries collected from every `RESOLVER.md` / `AGENTS.md` across the skills dir AND its parent, deduped by `skillPath` (first occurrence wins). Lifted reachable skills on the reference OpenClaw layout from 37/224 to 200/224 — the deployment ships a thin `skills/RESOLVER.md` (~40 entries from skillpack) plus a fat `../AGENTS.md` (200+ entries, the real dispatcher), and the previous code only saw the first one. The CLI also switched to `autoDetectSkillsDirReadOnly` so `cd ~ && gbrain check-resolvable` finds the bundled skills via the install-path fallback. `--fix` carries the same D6 safety gate as `gbrain doctor --fix`: refuses to write when `detected.source === 'install_path'`. - `src/core/resolver-filenames.ts` (v0.19) — central list of accepted routing filenames (`RESOLVER.md`, `AGENTS.md`). Shared by `findRepoRoot`, `check-resolvable`, and skillpack install so every code path walks the same fallback chain. diff --git a/TODOS.md b/TODOS.md index 38d6aadd8..f424b3b17 100644 --- a/TODOS.md +++ b/TODOS.md @@ -142,6 +142,49 @@ note. Filing it as a TODO would imply it's ready to pull; it isn't. current behavior (truncate-then-fail) is safe — no infinite loops, depth-cap prevents chains — but full semantic reduction unlocks higher self-fix success rates on legitimately-long prompts. +## v0.41.7.0 resolver-parser follow-ups (filed during ship of `garrytan/pr1370-production-ready`) + +Source: Codex outside-voice review on the PR #1370 production-rebuild plan. +The wave shipped with the primary parser fix + 11 unit tests + 2 integration +fixtures + scaling-skills tutorial. Two findings deferred: + +- [ ] **F8 P3 — Path-traversal hardening for the existing table-format + parser.** Both the existing table parser and the new list parser accept + inputs like `skills/../x/SKILL.md`; downstream `join(skillsDir, relPath)` + can escape `skillsDir`. The v0.41.7.0 list branch is structurally closed + (the kebab-lowercase `[a-z][a-z0-9-]+` name regex rejects `.` in names so + `..` is blocked at the name layer). The table branch surface is + pre-existing and out of scope for v0.41.7.0. Move: at the file-existence + check in `src/core/check-resolvable.ts` (around line 352), add a + `relPath.split('/').includes('..')` guard that surfaces as an + `unreachable` issue with a "path traversal not allowed" message. Low + severity: requires malicious/buggy RESOLVER.md content to fire. + +- [ ] **F9 P3 — Document the fan-out/dedup interaction in the resolver + guide.** `checkResolvable` dedupes by `skillPath`, so the v0.41.7.0 + list-format multi-trigger fan-out (`- **foo**: t1 | t2 | t3` produces 3 + entries) doesn't change the integration reachability count. This is + desired behavior (one skill counted once) but surprising for readers who + count parser entries. Move: add a one-paragraph "how fan-out interacts + with reachability" note to `docs/guides/scaling-skills.md` after we have + reader feedback indicating the confusion is real. Codex noted that unit + tests prove parser output, integration tests prove reachability, and the + current docs don't bridge the two cleanly. Doc-only follow-up. + +- [ ] **P1 flake — audit-writer.test.ts week-boundary failure.** Caught + during ship of v0.41.7.0. Test at `test/audit/audit-writer.test.ts:229` + ("returns events from current week, filtered by ts cutoff") fails when + real UTC date is in a different ISO week than the test's hardcoded + `now=2026-05-22`. `writer.log()` uses real `new Date()` to pick the + week-file; `readRecent(now)` uses the fake `now`. When the two land in + different ISO weeks (specifically: any time the real UTC clock is in + the week AFTER 2026-W21), `log()` writes to the wrong file and + readRecent finds 0 events. Fires deterministically once a week, at the + UTC Monday rollover. Move: refactor `createAuditWriter.log()` to accept + an optional injected `now` (or read it from the entry's own `ts` field). + Affected surface: `src/core/audit/audit-writer.ts`. Pre-existing on + master; not caused by this branch's parser changes. Reproducible by + setting system clock to any Monday after the test's `2026-05-22` date. ## v0.41 content-sanity follow-ups (filed during ship of `garrytan/lint-page-size-gate`) diff --git a/VERSION b/VERSION index 33a1f1909..c9fd62888 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.41.6.0 +0.41.7.0 \ No newline at end of file diff --git a/docs/guides/scaling-skills.md b/docs/guides/scaling-skills.md new file mode 100644 index 000000000..b765ad6ad --- /dev/null +++ b/docs/guides/scaling-skills.md @@ -0,0 +1,310 @@ +# Scaling skills past 300 without drowning the context window + +When an agent grows past 100 skills, a wall starts forming. Sessions take +longer to start. The model gets a little dumber about which skill to pick. +Tokens that should be powering reasoning are powering a skill catalog the +model reads on every turn whether it needs to or not. + +This guide is the recipe for breaking through that wall without deleting +capabilities. Three tiers, one resolver, one safety net. Production-tested +on a 306-skill agent (Garry's OpenClaw, the agent behind Y Combinator's +president). The pattern works whether you run OpenClaw, Hermes, Claude Code, +Cursor, or your own MCP-aware agent. + +## The problem + +OpenClaw scans every skill file on disk at session start and injects them +into the system prompt as `` entries. The model sees a +name, description, and file path for each one. When a request matches, the +model reads the full SKILL.md and follows it. + +This is great architecture at 50 skills. At 100, it's fine. At 200, it +starts to drag. At 300, the system prompt eats more than 25,000 tokens on +skill descriptions alone. Tokens that aren't going to reasoning, context, +or actual work. + +The symptoms compound: + +- Sessions take noticeably longer to start. +- The model has less room for conversation history. +- Skill routing gets fuzzier. With 300 descriptions competing for attention, + the model occasionally picks the wrong one. +- Cost goes up because every turn carries the full skill manifest. + +The naive fix is to delete skills you don't use often. Don't do this. The +whole point of skills is that capabilities compound. A gift pipeline that +fires twice a month saves 30 minutes each time it does. A flight tracker +fires once per trip and prevents a missed Uber. Deleting low-frequency +skills optimizes for prompt size at the cost of capability. You wouldn't +delete apps from your phone because the home screen is too crowded. You'd +organize them. + +## The three tiers + +Not all skills need to be visible to the model at all times. Some are core. +Some are specialized. Some are dormant. + +### Tier A: always loaded (~35 skills) + +The skills the model needs on every single turn. Brain search, email triage, +calendar, meeting ingestion, content creation, the executive assistant. +They stay in the system prompt's `` manifest. The model +sees them natively and routes to them without any lookup. + +### Tier B: resolver-routed (~85 skills) + +Real, active skills that fire regularly but don't need to pollute every +turn. Gift pipeline, flight tracker, investor update ingestion, adversary +tracking, book mirror, civic intelligence. They live on disk. They have +full SKILL.md files. But OpenClaw doesn't inject them into the prompt. + +Instead, a compact RESOLVER.md handles routing. One line per skill with +trigger phrases: + +```markdown +- **gift-advisor**: gift idea | what should I bring | birthday gift | housewarming +- **flight-tracker**: track my flight | flight status | when does my flight land +- **investor-update-ingest**: investor update | portfolio update | company metrics +``` + +When the model sees "what should I bring to Jessica's dinner," it checks +the resolver, finds `gift-advisor`, reads the SKILL.md, and executes. Same +result. Zero wasted tokens on the other 84 turns where gifts aren't relevant. + +### Tier C: dormant (~180 skills) + +Built-in OpenClaw skills that aren't in active rotation (1Password, Discord, +Notion, Trello, integrations you haven't wired up yet) plus specialized +skills that almost never fire. They're explicitly disabled in the config +with `enabled: false`. They exist on disk as documentation and potential. +Flip one boolean to wake them up. Zero tokens contributed to every prompt +until then. + +### The numbers + +Before tiering, on Garry's 306-skill OpenClaw: + +| Metric | Before | +|---|---| +| Skills in system prompt | 306 | +| Skill-description tokens per turn | ~25,000 | +| Skill routing accuracy | degrading | +| Session startup | slow | + +After tiering: + +| Metric | After | +|---|---| +| Skills in system prompt (Tier A) | 35 | +| Skill-description tokens per turn | ~4,000 | +| Skills still accessible (A + B + C) | 301 | +| Capability loss | zero | +| **Tokens freed per turn** | **~21,000** | + +21K tokens per turn is not a small optimization. It's the difference between +the model having room to think and the model being squeezed. It's the +difference between carrying 3 pages of conversation history and carrying 15. + +## What the resolver actually does + +The resolver is cheaper than the manifest. That's the load-bearing insight. + +OpenClaw's native skill manifest puts ~80 tokens per skill into the system +prompt (name + description + location). At 300 skills that's 24,000 tokens +spent every turn whether the model needs the catalog or not. + +The resolver puts ~15 tokens per skill into a compact markdown list. At +300 skills that's 4,500 tokens. But it only fires when the model checks +it, which is only when the request doesn't match a Tier A skill. Most +turns, the resolver costs zero tokens because the Tier A match handles it. + +This is the routing-table pattern but applied to the skill manifest itself. +The resolver routes to skills, but it also routes around skills, keeping +them out of the context window until they're needed. + +GBrain ships with a [bundled `skills/RESOLVER.md`](../../skills/RESOLVER.md) +you can use as a reference shape. The skillpack story for distributing +your own resolvers across machines is covered in +[skillpacks as scaffolding](skillpacks-as-scaffolding.md). + +## The compact list format (v0.41.7.0) + +GBrain's resolver parser used to require markdown tables: + +```markdown +| Trigger | Skill | +|---------|-------| +| "gift idea" | `skills/gift-advisor/SKILL.md` | +``` + +That's fine when you have 20 entries. It gets unwieldy at 200, and at 300 +it's unreadable. OpenClaw deployments quietly evolved a compact list +format that scales better: + +```markdown +- **gift-advisor**: gift idea | what should I bring | birthday gift +- **flight-tracker**: track my flight | flight status | when does my flight land +``` + +Before v0.41.7.0, `gbrain doctor` only spoke the table dialect. On a +306-skill compact-format resolver, the doctor reported every skill as +unreachable: **238 FAIL errors on every doctor run**. The parser was +silently treating the compact dialect as zero skills. + +v0.41.7.0 ships dual-format support. The same `parseResolverEntries` +function reads both table rows and list rows in the same file, with the +v0.31.7 multi-resolver merge (skillpack `skills/RESOLVER.md` + workspace +`../AGENTS.md`) folding everything into one unified view. Run `gbrain doctor` +and the 238 FAILs collapse to 0. + +### The list-format contract + +A few rules to keep the parser unambiguous: + +- **Skill names must be kebab-lowercase.** `gift-advisor`, `flight-tracker`, + `email-triage`. Names that start with an uppercase letter (`MyTool`, + `Note`, `Convention`) are deliberately ignored. This is what stops prose + bullets like `- **Note**: see [link]` from being mis-parsed as skill + rows in real-world AGENTS.md files. +- **The path always resolves to `skills//SKILL.md`.** An optional + `→ \`skills/path\`` (or ASCII `->`) suffix is allowed for readability, + but the parser strips it. For non-conventional paths (skills under + nested directories, references into `conventions/`, anything that + isn't `skills//SKILL.md`), use the table format. +- **Triggers separate with `|`.** Empty pieces and the literal `...` + placeholder are dropped. Each trigger becomes its own resolver entry, + all pointing at the same skill. +- **Bold or plain.** `- **name**: triggers` is preferred. `- name: triggers` + works as a fallback. + +You can mix table and list rows in the same file. Useful when a brain +inherits a table-format `RESOLVER.md` from gbrain and a list-format +`../AGENTS.md` from OpenClaw. + +## The doctor safety net + +The danger with tiering is invisible skill loss. You disable a skill from +native scanning, forget to add it to the resolver, and now the agent can't +do something it used to do. You won't notice until the moment you need it. + +`gbrain doctor` walks every skill on disk and verifies it's reachable, +either through native scanning (Tier A) or through the resolver (Tier B +and C). On Garry's setup, the first run after tiering found 63 unreachable +skills. Sixty-three capabilities that existed on disk but had no routing +path. Fixed in an hour by adding resolver entries. + +Run it after every skill change: + +```bash +gbrain doctor +``` + +For CI gates, use the JSON-emitting variant: + +```bash +gbrain check-resolvable --json +gbrain check-resolvable --strict # warnings fail too +``` + +If a skill is unreachable, the output tells you which one and suggests +the fix. The resolver is a document. Documents are cheap to fix. + +## Implementation walkthrough + +Three changes. Total time about 45 minutes once you've decided which +skills go in which tier. + +### 1. Audit and tier your skills + +Walk through every skill. Ask: does this need to fire on every turn? + +- If yes → Tier A. +- If it fires weekly or less but is real → Tier B. +- If you don't use it → Tier C. + +### 2. Disable Tier B and C in your agent's config + +For OpenClaw, the file is `openclaw.json`. Add an entry per disabled skill: + +```json +{ + "skills": { + "entries": { + "gift-advisor": { "enabled": false }, + "flight-tracker": { "enabled": false }, + "1password": { "enabled": false } + } + } +} +``` + +The exact config shape depends on which agent runtime you use. The point +is the same in all of them: tell the runtime not to inject this skill into +the system prompt. The file stays on disk; only the prompt injection stops. + +### 3. Write the resolver + +One line per Tier B and Tier C skill. Trigger phrases that match how you +actually ask for things: + +```markdown +- **gift-advisor**: gift idea | what should I bring | birthday gift +- **flight-tracker**: track my flight | flight status | when do I land +- **investor-update-ingest**: investor update | portfolio update | company metrics +``` + +That's it. The model handles the rest. When a request doesn't match Tier A, +it checks the resolver, reads the matching SKILL.md, and executes. + +### 4. Run `gbrain doctor` and fix any unreachable skills + +The doctor sweep tells you which skills don't have a routing path. Add a +resolver entry for each one, re-run, repeat until the count is zero. + +## A lesson from the first version + +I initially converted my resolver from a clean list format to a table +format because the validator only spoke tables. That was wrong. When a +tool fails against valid data, the right move is to fix the tool, not +reshape the data. The list format was correct, compact, readable, easy +to maintain. The parser needed to support both shapes. v0.41.7.0 is +that fix. + +The same principle applies everywhere in agent systems. Your SKILL.md is +the source of truth. Your AGENTS.md is the source of truth. Your resolver +is the source of truth. When tooling disagrees with your configuration, +the tooling is wrong. Fix the tooling. + +## The scaling curve + +At 50 skills, you don't need any of this. Just load everything. + +At 100, you start feeling the drag but can push through. + +At 200, routing accuracy drops and sessions get noticeably slower. This +is where most people stop adding skills, which means their agent stops +getting more capable. Bad trade. + +At 300+, tiering is mandatory. But with tiering, there's no ceiling. +1,000 skills with 35 in the hot path and 965 in the resolver is the same +per-turn cost as 35 skills with no resolver. The cost stays flat. +Capabilities compound. + +The architecture that gets you from 50 to 300 is different from the +architecture that gets you from 10 to 50. That's normal. Systems that +scale change shape. The important thing is that each tier preserves full +capability. You're organizing, not deleting. + +## Related + +- [Skill development cycle](skill-development.md) — the 5-step loop for + turning a repeated task into a real skill. +- [Skillpacks as scaffolding](skillpacks-as-scaffolding.md) — how to + distribute a coherent set of skills across machines and agents. +- [Sub-agent routing](sub-agent-routing.md) — when to delegate to a + sub-agent vs handle in-line, and the model routing table for each path. + +GBrain: [github.com/garrytan/gbrain](https://github.com/garrytan/gbrain). +The `parseResolverEntries` parser lives at +[`src/core/check-resolvable.ts`](../../src/core/check-resolvable.ts); +the bundled resolver lives at [`skills/RESOLVER.md`](../../skills/RESOLVER.md). diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index b820ef154..649f61c47 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -30,7 +30,7 @@ Tutorials follow the [Diataxis](https://diataxis.fr/) tutorial pattern: learning ## Related documentation - **Reference:** [`docs/architecture/`](../architecture/) — system design, topologies, retrieval theory -- **How-to:** [`docs/guides/`](../guides/) — task-oriented runbooks (sub-agent routing, minion deployment, skill development, brain-first lookup, idea capture, diligence ingestion) +- **How-to:** [`docs/guides/`](../guides/) — task-oriented runbooks (sub-agent routing, minion deployment, skill development, brain-first lookup, idea capture, diligence ingestion). Highlight: [scaling skills past 300](../guides/scaling-skills.md) — the three-tier architecture for agents that have outgrown the always-loaded skill manifest. - **Integrations:** [`docs/integrations/`](../integrations/) — connecting external data sources (voice, email, calendar, embedding providers) - **MCP setup:** [`docs/mcp/`](../mcp/) — per-client setup (Claude Desktop, Code, Cursor, ChatGPT, Perplexity, Cowork) - **Install paths:** [`docs/INSTALL.md`](../INSTALL.md) — every install path, end to end diff --git a/llms-full.txt b/llms-full.txt index d2ff3d24f..23eb20afe 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -277,7 +277,7 @@ strict behavior when unset. - `src/core/minions/queue.ts` extension (v0.31.12) — `MinionQueue.add()` now rejects `subagent` jobs whose `data.model` resolves through `isAnthropicProvider()` to a non-Anthropic provider. Lazy-imports `model-config.ts` to avoid pulling engine types into queue's eager-load surface. Layer 1 of the three-layer subagent provider enforcement (Codex F1+F2 in plan review). Layers 2 + 3 live in `src/core/model-config.ts` (`enforceSubagentAnthropic` runtime fallback) and `src/commands/doctor.ts` (`subagent_provider` check). Pinned by 3 cases in `test/agent-cli.test.ts`. - `src/commands/models.ts` (v0.31.12) — `gbrain models [--json]` read-only routing dashboard: prints tier defaults (`utility`/`reasoning`/`deep`/`subagent`), the resolved value for each (re-walking the resolution chain to attribute properly), every per-task override (11 `PER_TASK_KEYS` entries — `models.dream.synthesize`, `models.dream.patterns`, `models.drift`, `models.auto_think`, `models.think`, `models.subagent`, `facts.extraction_model`, `models.eval.longmemeval`, `models.expansion`, `models.chat`, `models.dream.synthesize_verdict`), the alias map (defaults + user overrides), and a source-of-truth column showing `default` / `config: ` / `env: `. `gbrain models doctor [--skip=] [--json]` fires a 1-token `gateway.chat()` probe against each configured chat + expansion model and classifies failures into `{model_not_found, auth, rate_limit, network, unknown}` — the structural fix for the v0.31.6 silent-no-op bug class. Wired into `cli.ts` dispatch table + `CLI_ONLY` set. **v0.33.1.1 (#962, Codex P3 follow-up):** doctor gains a zero-token `embedding_config` probe that runs FIRST, before any chat/expansion probes spend money. `probeEmbeddingConfig()` reads `getEmbeddingModel()` + `getEmbeddingDimensions()` from the gateway, parses the model id, and (for Voyage flexible-dim models) checks `isValidVoyageOutputDim(dims)` against `VOYAGE_VALID_OUTPUT_DIMS`. New `ProbeStatus` variant `'config'` and optional `fix?: string` field on `ProbeResult` — surfaced in both human output (paste-ready `gbrain config set ...` line under the bad probe) and JSON output. New touchpoint label `'embedding_config'` joins `'chat'` and `'expansion'` in the probe-row taxonomy. Closes the Voyage flexible-dim bug class at config time, not first-embed. - `src/commands/doctor.ts` extension (v0.31.12) — new `subagent_provider` check (layer 3 of 3 — Codex F13). Warns when `models.tier.subagent` is explicitly set to a non-Anthropic provider (fail-loud since the user clearly meant it — message names the bad value and prints the paste-ready fix command `gbrain config set models.tier.subagent anthropic:claude-sonnet-4-6`); also warns when `models.default` would sneak `subagent` into a non-Anthropic provider via tier inheritance. OK status when subagent tier resolves to Anthropic. Tests cover all three paths in `test/doctor.test.ts`. -- `src/core/check-resolvable.ts` — Resolver validation: reachability, MECE overlap, DRY checks, structured fix objects. v0.14.1: `CROSS_CUTTING_PATTERNS.conventions` is an array (notability gate accepts both `conventions/quality.md` and `_brain-filing-rules.md`). New `extractDelegationTargets()` parses `> **Convention:**`, `> **Filing rule:**`, and inline backtick references. DRY suppression is proximity-based via `DRY_PROXIMITY_LINES = 40`. +- `src/core/check-resolvable.ts` — Resolver validation: reachability, MECE overlap, DRY checks, structured fix objects. v0.14.1: `CROSS_CUTTING_PATTERNS.conventions` is an array (notability gate accepts both `conventions/quality.md` and `_brain-filing-rules.md`). New `extractDelegationTargets()` parses `> **Convention:**`, `> **Filing rule:**`, and inline backtick references. DRY suppression is proximity-based via `DRY_PROXIMITY_LINES = 40`. **v0.41.7.0:** `parseResolverEntries` accepts a second compact list format alongside the original markdown table (`- **skill-name**: trigger1 | trigger2 | trigger3` or `- skill-name: trigger1 | trigger2`). Both shapes can mix in one file; the v0.31.7 multi-resolver merge folds them into one entry stream. Skill name MUST be kebab-lowercase (regex `[a-z][a-z0-9-]+`) so prose bullets like `- **Note**: …`, `- **Convention**: …`, `- **TODO**: …` in real-world AGENTS.md files don't false-match as skill rows (codex F2 / D4). `skillPath` is ALWAYS derived as `skills//SKILL.md`: an optional `→ \`skills/path\`` (or ASCII `->`) suffix is stripped from the trigger string but NOT honored as the path — two downstream consumers (`routing-eval.ts:skillSlugFromPath`, the manifest lookup at `:367`) assume the convention, and honoring the explicit path would silently break their coverage. For non-conventional paths, use the table format. Multi-trigger rows fan out to one entry per trigger sharing the same `skillPath`; `checkResolvable` dedupes downstream so the reachability count counts each skill once. Closes the OpenClaw bug class where a 306-skill agent's list-format resolver registered 238 FAILs ("skill not reachable from RESOLVER.md") on every `gbrain doctor` run — productionized from upstream PR #1370 by @garrytan-agents. Pinned by 11 new unit cases in `test/check-resolvable.test.ts` (bold + plain forms, Unicode + ASCII suffix strip, ellipsis filter, empty pipe segments, mixed shapes, prose-bullet rejection regression) and the 8-case integration suite in `test/check-resolvable-openclaw-compact.test.ts` over two fixtures: `test/fixtures/openclaw-compact-resolver/` (10 skills in pure compact form) + `test/fixtures/openclaw-mixed-merge/` (table-format `skills/RESOLVER.md` + compact `../AGENTS.md` for the v0.31.7 merge contract). User-facing tutorial: `docs/guides/scaling-skills.md` walks through when and why to switch to the compact format (the three-tier scaling architecture that gets a 300-skill agent down to ~4K tokens per turn from ~25K with zero capability loss). - `src/core/repo-root.ts` — Shared `findRepoRoot(startDir?)` (v0.16.4): walks up from `startDir` (default `process.cwd()`) looking for `skills/RESOLVER.md`. Zero-dependency module imported by both `doctor.ts` and `check-resolvable.ts`. Parameterized `startDir` makes tests hermetic. **v0.31.7:** read-path / write-path split. `autoDetectSkillsDir` (shared, read+write-safe) gains tier-0 `$GBRAIN_SKILLS_DIR` explicit operator override (Docker mounts, CI, monorepo subdirs) ahead of the existing 4-tier chain. New `autoDetectSkillsDirReadOnly` wraps it with a tier-5 install-path fallback that walks up from `fileURLToPath(import.meta.url)` and gates on `isGbrainRepoRoot` so unrelated repos can't false-positive. Read-path callers (`doctor`, `check-resolvable`, `routing-eval`) use the read-only variant; write-path callers (`skillpack install`, `skillify scaffold`, `post-install-advisory`) deliberately stay on the shared function so `gbrain skillpack install` from `~` cannot silently retarget the bundled gbrain repo's `skills/` instead of the user's actual workspace. Two new `SkillsDirSource` variants: `'env_explicit'`, `'install_path'`. New `AUTO_DETECT_HINT_READ_ONLY` documents the extra tier. The D6 `--fix` safety gate in `doctor.ts` + `check-resolvable.ts` refuses auto-repair when `detected.source === 'install_path'` so `gbrain doctor --fix` from `~` cannot silently rewrite the bundled install tree. - `src/commands/check-resolvable.ts` — Standalone CLI wrapper (v0.16.4) over `checkResolvable()`. Exports `parseFlags`, `resolveSkillsDir`, `DEFERRED`, `runCheckResolvable`. Exit rule: **1 on any issue (warnings OR errors)**, stricter than doctor's `ok` flag — honors README:259. Stable JSON envelope `{ok, skillsDir, report, autoFix, deferred, error, message}` — same shape on success and error paths. `--fix` path runs `autoFixDryViolations` BEFORE `checkResolvable` (same ordering as doctor). `scripts/skillify-check.ts` subprocess-calls `gbrain check-resolvable --json` (cached per process) and fails loud on binary-missing — no silent false-pass. **v0.19:** AGENTS.md workspaces now resolve natively (see `src/core/resolver-filenames.ts`) — gbrain inspects the 107-skill OpenClaw deployment whether the routing file is `RESOLVER.md` or `AGENTS.md`. `DEFERRED[]` is empty — Checks 5 + 6 shipped as real code, not issue URLs. **v0.31.7:** the resolver lookup switched from first-match-wins to the multi-file merge in `src/core/check-resolvable.ts` — entries collected from every `RESOLVER.md` / `AGENTS.md` across the skills dir AND its parent, deduped by `skillPath` (first occurrence wins). Lifted reachable skills on the reference OpenClaw layout from 37/224 to 200/224 — the deployment ships a thin `skills/RESOLVER.md` (~40 entries from skillpack) plus a fat `../AGENTS.md` (200+ entries, the real dispatcher), and the previous code only saw the first one. The CLI also switched to `autoDetectSkillsDirReadOnly` so `cd ~ && gbrain check-resolvable` finds the bundled skills via the install-path fallback. `--fix` carries the same D6 safety gate as `gbrain doctor --fix`: refuses to write when `detected.source === 'install_path'`. - `src/core/resolver-filenames.ts` (v0.19) — central list of accepted routing filenames (`RESOLVER.md`, `AGENTS.md`). Shared by `findRepoRoot`, `check-resolvable`, and skillpack install so every code path walks the same fallback chain. @@ -4341,6 +4341,323 @@ Set `enabled: false` to disable quiet hours entirely (e.g., for 24/7 monitoring) --- +## docs/guides/scaling-skills.md + +Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md + +# Scaling skills past 300 without drowning the context window + +When an agent grows past 100 skills, a wall starts forming. Sessions take +longer to start. The model gets a little dumber about which skill to pick. +Tokens that should be powering reasoning are powering a skill catalog the +model reads on every turn whether it needs to or not. + +This guide is the recipe for breaking through that wall without deleting +capabilities. Three tiers, one resolver, one safety net. Production-tested +on a 306-skill agent (Garry's OpenClaw, the agent behind Y Combinator's +president). The pattern works whether you run OpenClaw, Hermes, Claude Code, +Cursor, or your own MCP-aware agent. + +## The problem + +OpenClaw scans every skill file on disk at session start and injects them +into the system prompt as `` entries. The model sees a +name, description, and file path for each one. When a request matches, the +model reads the full SKILL.md and follows it. + +This is great architecture at 50 skills. At 100, it's fine. At 200, it +starts to drag. At 300, the system prompt eats more than 25,000 tokens on +skill descriptions alone. Tokens that aren't going to reasoning, context, +or actual work. + +The symptoms compound: + +- Sessions take noticeably longer to start. +- The model has less room for conversation history. +- Skill routing gets fuzzier. With 300 descriptions competing for attention, + the model occasionally picks the wrong one. +- Cost goes up because every turn carries the full skill manifest. + +The naive fix is to delete skills you don't use often. Don't do this. The +whole point of skills is that capabilities compound. A gift pipeline that +fires twice a month saves 30 minutes each time it does. A flight tracker +fires once per trip and prevents a missed Uber. Deleting low-frequency +skills optimizes for prompt size at the cost of capability. You wouldn't +delete apps from your phone because the home screen is too crowded. You'd +organize them. + +## The three tiers + +Not all skills need to be visible to the model at all times. Some are core. +Some are specialized. Some are dormant. + +### Tier A: always loaded (~35 skills) + +The skills the model needs on every single turn. Brain search, email triage, +calendar, meeting ingestion, content creation, the executive assistant. +They stay in the system prompt's `` manifest. The model +sees them natively and routes to them without any lookup. + +### Tier B: resolver-routed (~85 skills) + +Real, active skills that fire regularly but don't need to pollute every +turn. Gift pipeline, flight tracker, investor update ingestion, adversary +tracking, book mirror, civic intelligence. They live on disk. They have +full SKILL.md files. But OpenClaw doesn't inject them into the prompt. + +Instead, a compact RESOLVER.md handles routing. One line per skill with +trigger phrases: + +```markdown +- **gift-advisor**: gift idea | what should I bring | birthday gift | housewarming +- **flight-tracker**: track my flight | flight status | when does my flight land +- **investor-update-ingest**: investor update | portfolio update | company metrics +``` + +When the model sees "what should I bring to Jessica's dinner," it checks +the resolver, finds `gift-advisor`, reads the SKILL.md, and executes. Same +result. Zero wasted tokens on the other 84 turns where gifts aren't relevant. + +### Tier C: dormant (~180 skills) + +Built-in OpenClaw skills that aren't in active rotation (1Password, Discord, +Notion, Trello, integrations you haven't wired up yet) plus specialized +skills that almost never fire. They're explicitly disabled in the config +with `enabled: false`. They exist on disk as documentation and potential. +Flip one boolean to wake them up. Zero tokens contributed to every prompt +until then. + +### The numbers + +Before tiering, on Garry's 306-skill OpenClaw: + +| Metric | Before | +|---|---| +| Skills in system prompt | 306 | +| Skill-description tokens per turn | ~25,000 | +| Skill routing accuracy | degrading | +| Session startup | slow | + +After tiering: + +| Metric | After | +|---|---| +| Skills in system prompt (Tier A) | 35 | +| Skill-description tokens per turn | ~4,000 | +| Skills still accessible (A + B + C) | 301 | +| Capability loss | zero | +| **Tokens freed per turn** | **~21,000** | + +21K tokens per turn is not a small optimization. It's the difference between +the model having room to think and the model being squeezed. It's the +difference between carrying 3 pages of conversation history and carrying 15. + +## What the resolver actually does + +The resolver is cheaper than the manifest. That's the load-bearing insight. + +OpenClaw's native skill manifest puts ~80 tokens per skill into the system +prompt (name + description + location). At 300 skills that's 24,000 tokens +spent every turn whether the model needs the catalog or not. + +The resolver puts ~15 tokens per skill into a compact markdown list. At +300 skills that's 4,500 tokens. But it only fires when the model checks +it, which is only when the request doesn't match a Tier A skill. Most +turns, the resolver costs zero tokens because the Tier A match handles it. + +This is the routing-table pattern but applied to the skill manifest itself. +The resolver routes to skills, but it also routes around skills, keeping +them out of the context window until they're needed. + +GBrain ships with a [bundled `skills/RESOLVER.md`](../../skills/RESOLVER.md) +you can use as a reference shape. The skillpack story for distributing +your own resolvers across machines is covered in +[skillpacks as scaffolding](skillpacks-as-scaffolding.md). + +## The compact list format (v0.41.7.0) + +GBrain's resolver parser used to require markdown tables: + +```markdown +| Trigger | Skill | +|---------|-------| +| "gift idea" | `skills/gift-advisor/SKILL.md` | +``` + +That's fine when you have 20 entries. It gets unwieldy at 200, and at 300 +it's unreadable. OpenClaw deployments quietly evolved a compact list +format that scales better: + +```markdown +- **gift-advisor**: gift idea | what should I bring | birthday gift +- **flight-tracker**: track my flight | flight status | when does my flight land +``` + +Before v0.41.7.0, `gbrain doctor` only spoke the table dialect. On a +306-skill compact-format resolver, the doctor reported every skill as +unreachable: **238 FAIL errors on every doctor run**. The parser was +silently treating the compact dialect as zero skills. + +v0.41.7.0 ships dual-format support. The same `parseResolverEntries` +function reads both table rows and list rows in the same file, with the +v0.31.7 multi-resolver merge (skillpack `skills/RESOLVER.md` + workspace +`../AGENTS.md`) folding everything into one unified view. Run `gbrain doctor` +and the 238 FAILs collapse to 0. + +### The list-format contract + +A few rules to keep the parser unambiguous: + +- **Skill names must be kebab-lowercase.** `gift-advisor`, `flight-tracker`, + `email-triage`. Names that start with an uppercase letter (`MyTool`, + `Note`, `Convention`) are deliberately ignored. This is what stops prose + bullets like `- **Note**: see [link]` from being mis-parsed as skill + rows in real-world AGENTS.md files. +- **The path always resolves to `skills//SKILL.md`.** An optional + `→ \`skills/path\`` (or ASCII `->`) suffix is allowed for readability, + but the parser strips it. For non-conventional paths (skills under + nested directories, references into `conventions/`, anything that + isn't `skills//SKILL.md`), use the table format. +- **Triggers separate with `|`.** Empty pieces and the literal `...` + placeholder are dropped. Each trigger becomes its own resolver entry, + all pointing at the same skill. +- **Bold or plain.** `- **name**: triggers` is preferred. `- name: triggers` + works as a fallback. + +You can mix table and list rows in the same file. Useful when a brain +inherits a table-format `RESOLVER.md` from gbrain and a list-format +`../AGENTS.md` from OpenClaw. + +## The doctor safety net + +The danger with tiering is invisible skill loss. You disable a skill from +native scanning, forget to add it to the resolver, and now the agent can't +do something it used to do. You won't notice until the moment you need it. + +`gbrain doctor` walks every skill on disk and verifies it's reachable, +either through native scanning (Tier A) or through the resolver (Tier B +and C). On Garry's setup, the first run after tiering found 63 unreachable +skills. Sixty-three capabilities that existed on disk but had no routing +path. Fixed in an hour by adding resolver entries. + +Run it after every skill change: + +```bash +gbrain doctor +``` + +For CI gates, use the JSON-emitting variant: + +```bash +gbrain check-resolvable --json +gbrain check-resolvable --strict # warnings fail too +``` + +If a skill is unreachable, the output tells you which one and suggests +the fix. The resolver is a document. Documents are cheap to fix. + +## Implementation walkthrough + +Three changes. Total time about 45 minutes once you've decided which +skills go in which tier. + +### 1. Audit and tier your skills + +Walk through every skill. Ask: does this need to fire on every turn? + +- If yes → Tier A. +- If it fires weekly or less but is real → Tier B. +- If you don't use it → Tier C. + +### 2. Disable Tier B and C in your agent's config + +For OpenClaw, the file is `openclaw.json`. Add an entry per disabled skill: + +```json +{ + "skills": { + "entries": { + "gift-advisor": { "enabled": false }, + "flight-tracker": { "enabled": false }, + "1password": { "enabled": false } + } + } +} +``` + +The exact config shape depends on which agent runtime you use. The point +is the same in all of them: tell the runtime not to inject this skill into +the system prompt. The file stays on disk; only the prompt injection stops. + +### 3. Write the resolver + +One line per Tier B and Tier C skill. Trigger phrases that match how you +actually ask for things: + +```markdown +- **gift-advisor**: gift idea | what should I bring | birthday gift +- **flight-tracker**: track my flight | flight status | when do I land +- **investor-update-ingest**: investor update | portfolio update | company metrics +``` + +That's it. The model handles the rest. When a request doesn't match Tier A, +it checks the resolver, reads the matching SKILL.md, and executes. + +### 4. Run `gbrain doctor` and fix any unreachable skills + +The doctor sweep tells you which skills don't have a routing path. Add a +resolver entry for each one, re-run, repeat until the count is zero. + +## A lesson from the first version + +I initially converted my resolver from a clean list format to a table +format because the validator only spoke tables. That was wrong. When a +tool fails against valid data, the right move is to fix the tool, not +reshape the data. The list format was correct, compact, readable, easy +to maintain. The parser needed to support both shapes. v0.41.7.0 is +that fix. + +The same principle applies everywhere in agent systems. Your SKILL.md is +the source of truth. Your AGENTS.md is the source of truth. Your resolver +is the source of truth. When tooling disagrees with your configuration, +the tooling is wrong. Fix the tooling. + +## The scaling curve + +At 50 skills, you don't need any of this. Just load everything. + +At 100, you start feeling the drag but can push through. + +At 200, routing accuracy drops and sessions get noticeably slower. This +is where most people stop adding skills, which means their agent stops +getting more capable. Bad trade. + +At 300+, tiering is mandatory. But with tiering, there's no ceiling. +1,000 skills with 35 in the hot path and 965 in the resolver is the same +per-turn cost as 35 skills with no resolver. The cost stays flat. +Capabilities compound. + +The architecture that gets you from 50 to 300 is different from the +architecture that gets you from 10 to 50. That's normal. Systems that +scale change shape. The important thing is that each tier preserves full +capability. You're organizing, not deleting. + +## Related + +- [Skill development cycle](skill-development.md) — the 5-step loop for + turning a repeated task into a real skill. +- [Skillpacks as scaffolding](skillpacks-as-scaffolding.md) — how to + distribute a coherent set of skills across machines and agents. +- [Sub-agent routing](sub-agent-routing.md) — when to delegate to a + sub-agent vs handle in-line, and the model routing table for each path. + +GBrain: [github.com/garrytan/gbrain](https://github.com/garrytan/gbrain). +The `parseResolverEntries` parser lives at +[`src/core/check-resolvable.ts`](../../src/core/check-resolvable.ts); +the bundled resolver lives at [`skills/RESOLVER.md`](../../skills/RESOLVER.md). + +--- + ## docs/mcp/DEPLOY.md Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md @@ -5176,621 +5493,3 @@ should match your expectations for the corpus. --- # Migrations - -## docs/UPGRADING_DOWNSTREAM_AGENTS.md - -Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/UPGRADING_DOWNSTREAM_AGENTS.md - -# Upgrading Downstream Agents - -GBrain ships skills in `skills/`. Downstream agents (custom OpenClaw deployments, -agent forks of any kind) often **copy** these skill files into their own workspace and -diverge over time — adding agent-specific phases, removing irrelevant ones, tightening -language. Once that happens, gbrain can't push updates to those forks. The agent has -to apply the diffs by hand. - -This doc lists the exact diffs each downstream agent needs to apply when upgrading. -Cross-reference against your fork's local skill files. - -## Why this exists - -`gbrain upgrade` ships the new binary. `gbrain post-upgrade [--execute --yes]` runs -the schema migrations and backfills the data. But the **skill files themselves** -that tell the agent how to behave — those are user-owned. If your `~/git//workspace/skills/brain-ops/SKILL.md` -says `# Based on gbrain v0.10.0` at the top, it doesn't know about v0.12.0 features. - -The agent will keep manually calling `gbrain link` after every `put_page` (now redundant — -auto-link does it), miss out on `gbrain graph-query` for relationship questions, and -not know to backfill the structured timeline. - -## How to apply - -1. Identify your forked skill files. Typically at `~/git//workspace/skills/` or wherever your agent's skill directory lives. -2. For each skill listed below, find the matching phase/section in your fork. -3. Apply the diff (paste the new block in the indicated location). -4. Update the version banner at the top of your fork (`# Based on gbrain v0.12.0`). -5. Verify: ask the agent to write a test page and confirm the response includes - `auto_links: { created, removed, errors }`. - -Total time: ~10 minutes for all four skills. - ---- - -## 1. brain-ops/SKILL.md - -**Where:** Insert a new `### Phase 2.5` section immediately after `### Phase 2: On Every Inbound Signal`. - -**Why:** Phase 2.5 declares that auto-link runs automatically. Without this, the -agent's mental model says it must call `gbrain link` after every `put_page`, which -is now redundant and can cause double-add warnings. - -```markdown -### Phase 2.5: Structured Graph Updates (automatic) - -Every `put_page` call automatically extracts entity references and writes them -to the graph (`links` table) with inferred relationship types. Stale links -(refs no longer in the page text) are removed in the same call. This is -"auto-link" reconciliation. - -- No manual `add_link` calls needed for ordinary page writes. -- Inferred link types: `attended` (meeting -> person), `works_at`, `invested_in`, - `founded`, `advises`, `source` (frontmatter), `mentions` (default). -- The `put_page` MCP response includes `auto_links: { created, removed, errors }` - so the agent can verify outcomes. -- To disable: `gbrain config set auto_link false`. Default is on. -- Timeline entries with specific dates still need explicit `gbrain timeline-add` - (or batch via `gbrain extract timeline --source db`). -``` - -**Also update the Iron Law section.** If your fork still says "Back-links maintained -on every brain write (Iron Law)" without qualification, append: - -```markdown -**v0.12.0 update:** Auto-link satisfies the Iron Law for entity-reference links -on every `put_page`. The agent's Iron Law obligation is now: include the -entity reference in the page content (e.g., `[Alice](people/alice)`); auto-link -handles the structured row. Manual `add_link` calls are reserved for -relationships you can't express in markdown content. -``` - ---- - -## 2. meeting-ingestion/SKILL.md - -**Where:** Append to the end of `### Phase 3: Attendee enrichment`. - -**Why:** Eliminates redundant `gbrain link` calls per attendee (auto-link handles them -when the meeting page references attendees as `[Name](people/slug)`). - -```markdown -**Note (v0.12.0):** Once the meeting page is written via `gbrain put`, the -auto-link post-hook automatically creates `attended` links from the meeting -to each attendee whose page is referenced as `[Name](people/slug)`. You don't -need to call `gbrain link` for attendees. You DO still need `gbrain timeline-add` -for dated events (auto-link only handles links, not timeline entries). -``` - -**Where:** In `### Phase 4: Entity propagation`, the line "Back-link from entity page -to meeting page" can be replaced with: - -```markdown -4. Entity references in the meeting page body auto-create the link via auto-link. - For incoming references on the entity page (entity page → meeting page), edit - the entity page to mention the meeting and `put_page` it — auto-link handles - the rest. -``` - ---- - -## 3. signal-detector/SKILL.md - -**Where:** Append to the end of `### Phase 2: Entity Detection`. - -**Why:** Same logic as brain-ops — eliminates manual `gbrain link` after writing -originals/ideas pages that reference people or companies. - -```markdown -**Auto-link (v0.12.0):** When you write/update an originals or ideas page that -references a person or company, the auto-link post-hook on `put_page` -automatically creates the link from the new page to that entity. You don't -need to call `gbrain link` manually. Timeline entries still need explicit calls. -``` - ---- - -## 4. enrich/SKILL.md - -**Where:** Replace `### Step 7: Cross-reference` with the v0.12.0 version. - -**Why:** Step 7 used to be primarily about creating links between related entity -pages. With auto-link, that's automatic. Step 7 is now about content updates, -not link creation. - -Old (delete): -```markdown -### Step 7: Cross-reference - -- Update company pages from person enrichment (and vice versa) -- Update related project/deal pages if relevant context surfaced -- Check index files if the brain uses them -- Add back-links manually via `gbrain link` for any new entity references -``` - -New (paste): -```markdown -### Step 7: Cross-reference - -- Update company pages from person enrichment (and vice versa) -- Update related project/deal pages if relevant context surfaced -- Check index files if the brain uses them - -**Note (v0.12.0):** Links between brain pages are auto-created on every -`put_page` call (auto-link post-hook). Step 7 focuses on content -cross-references (updating related pages' compiled truth with new signal -from this enrichment), not on creating links. Verify via the `auto_links` -field in the put_page response (`{ created, removed, errors }`). -Timeline entries still need explicit `gbrain timeline-add` calls. -``` - ---- - -## After all four diffs are applied - -1. **Bump the version banner** at the top of each forked file: - ``` - # Based on gbrain v0.12.0 skills/, extended with -specific config - ``` - -2. **Run the v0.12.0 backfill** (this populates the graph for your existing brain): - ```bash - gbrain post-upgrade - ``` - The v0.12.0 release wires post-upgrade to call `apply-migrations --yes` - automatically, which runs the v0_12_0 orchestrator (schema → config check → - `extract links --source db` → `extract timeline --source db` → verify). - Idempotent; cheap when nothing is pending. - -3. **Verify auto-link works:** ask the agent to write a test page that references - `[Some Person](people/some-person)`. Confirm the put_page response includes - `auto_links: { created: 1, removed: 0, errors: 0 }`. - -4. **Verify graph traversal works:** - ```bash - gbrain graph-query people/some-well-connected-person --depth 2 - ``` - Should return an indented tree of typed edges. - ---- - -## v0.12.2 hotfix (data-correctness, no skill edits) - -v0.12.2 is a Postgres data-correctness hotfix. No forked skill files need to -change — the skill contracts are unchanged. But you DO need to run the migration, -and you should know about one behavior change in markdown parsing. - -### 1. Run the migration (Postgres-backed brains) - -```bash -gbrain upgrade -``` - -The `v0_12_2` orchestrator runs `gbrain repair-jsonb` automatically. It rewrites -rows where `jsonb_typeof = 'string'` across `pages.frontmatter`, `raw_data.data`, -`ingest_log.pages_updated`, `files.metadata`, and `page_versions.frontmatter`. -Idempotent, safe to re-run. PGLite brains no-op cleanly. - -Verify after upgrade: - -```bash -gbrain repair-jsonb --dry-run --json # expect totalRepaired: 0 -``` - -### 2. Recover any truncated wiki articles - -If your brain imported wiki-style markdown before v0.12.2, some pages were -silently truncated (any standalone `---` in body content was treated as a -timeline separator). Re-import from source: - -```bash -gbrain sync --full -``` - -The new `splitBody` rebuilds `compiled_truth` correctly. - -### 3. Know the splitBody contract going forward - -`splitBody` now requires an explicit timeline sentinel. Recognized markers -(priority order): - -1. `` (preferred — what `serializeMarkdown` emits) -2. `--- timeline ---` (decorated separator) -3. `---` directly before `## Timeline` or `## History` heading (backward-compat) - -A bare `---` in body text is now a markdown horizontal rule, not a timeline -separator. If your agent writes pages with a bare `---` delimiter, migrate to -`` — the `serializeMarkdown` helper already does this. - -### 4. Wiki subtypes now auto-typed - -`inferType` now auto-detects five additional directory patterns as their own -page types (previously they all defaulted to `concept`): - -| Path pattern | New type | -|------------------------|----------------| -| `/wiki/analysis/` | `analysis` | -| `/wiki/guides/` | `guide` | -| `/wiki/hardware/` | `hardware` | -| `/wiki/architecture/` | `architecture` | -| `/writing/` | `writing` | - -If your skills or queries filter by `type=concept` and expect wiki content in -that bucket, update them to include the new types. - ---- - -## v0.13.0 — Frontmatter Relationship Indexing - -**Verdict: no action required for most skills.** v0.13 projects YAML frontmatter fields into the graph as typed edges. The ingestion API is unchanged — keep calling `put_page` with frontmatter the way you do today; the graph auto-populates behind the scenes. - -Three skills get an optional new phase if you want to consume the new `auto_links.unresolved` response field. Without this, unresolvable frontmatter names silently skip (same as v0.12 behavior). - -### 1. meeting-ingestion/SKILL.md (optional) - -**Where:** Add a new section after "Phase 3: Write Meeting Page". - -```markdown -### Phase 3.5: Check for unresolved attendees (v0.13+) - -After `put_page`, inspect `response.auto_links.unresolved` — an array of frontmatter -references that did not resolve to existing pages. For meetings, this usually means -attendees you haven't created a person page for yet. - -If `unresolved.length > 0`: -- Option 1 (create pages now): trigger an enrichment pass to build the missing people pages. -- Option 2 (defer): log the unresolved names to the enrichment queue for later. -- Option 3 (accept the gap): the attendee edge will not be created until a page exists. - Re-running `gbrain extract links --source db --include-frontmatter` after creating - the page fills in the missing edges. -``` - -### 2. enrich/SKILL.md (optional) - -**Where:** Add to the enrichment trigger list. - -```markdown -### Drain unresolved frontmatter names (v0.13+) - -If any `put_page` response includes `auto_links.unresolved` entries, the enrichment -tier should pick up those (field, name) pairs and try to create the missing entity -pages. Example flow: - -1. signal-detector captures a meeting with `attendees: [Alice Known, Unknown Person]` -2. put_page returns `auto_links.unresolved = [{field: 'attendees', name: 'Unknown Person'}]` -3. enrichment tier consumes `Unknown Person` → web search → creates `people/unknown-person.md` -4. The next put_page (or a backfill run) wires up the `attended` edge automatically -``` - -### 3. idea-ingest/SKILL.md (optional) - -**Where:** Same pattern as meeting-ingestion — check `auto_links.unresolved` after `put_page`, route names to enrichment. - -### Unchanged skills (no diffs needed) - -- **brain-ops/SKILL.md** — auto-link mechanics are internal; the write path stays the same. -- **signal-detector/SKILL.md** — signal capture path unchanged. -- **query/SKILL.md** — `traverse_graph` now returns richer results automatically. -- **daily-task-manager/SKILL.md**, **briefing/SKILL.md**, **citation-fixer/SKILL.md**, **media-ingest/SKILL.md** — unchanged. - -### New edge types you can filter in graph queries - -v0.13 edges carry new `link_type` values. If your fork has graph-query skills that filter by type, these are now available: - -- `works_at` (person → company) — from `company:`, `companies:`, or `key_people:` -- `founded` (person → company) — from `founded:` -- `invested_in` (investor → deal/company) — from `investors:` or `lead:` -- `led_round` (lead → deal) — from `lead:` -- `yc_partner` (partner → company) — from `partner:` -- `attended` (person → meeting) — from `attendees:` -- `discussed_in` (source → page) — from `sources:` -- `source` (page → source) — from `source:` -- `related_to` (page → target) — from `related:` or `see_also:` - -### Migration timing - -`gbrain upgrade` takes 2-5 min on a 46K-page brain (one-time). Runs out-of-process via `gbrain post-upgrade`. If your agent holds a DB connection during the upgrade, reconnect after; otherwise keep serving. - -### Type normalization NOT in v0.13 - -Legacy rows with `link_type='attendee'` or `link_type='mention'` coexist with new `'attended'` / `'mentions'` rows. Your queries filtering on old type names keep working. A separate opt-in `gbrain normalize-types` command in v0.14 handles the rename. -## v0.14.0 shell jobs (optional adoption, no skill edits) - -Adds a `shell` job type to Minions so deterministic cron scripts (API fetch, token -refresh, scrape + write) move off the LLM gateway. Zero tokens per fire. ~60% -gateway CPU headroom at typical scale. Feature is **off by default**, existing -installs keep running exactly as they did before. Nothing breaks. - -To adopt, follow `skills/migrations/v0.14.0.md`. The short version: - -1. Set `GBRAIN_ALLOW_SHELL_JOBS=1` on the worker process, then `gbrain jobs work` - (Postgres). On PGLite, every crontab invocation uses `--follow` for inline - execution; no persistent worker. -2. Classify each of your host's cron entries: LLM-requiring (keep on gateway) vs - deterministic (candidate for shell). Typical splits: - - **Deterministic → shell:** `ycli-token-refresh`, `x-oauth2-refresh`, - `x-garrytan-unified`, `calendar-sync-to-brain`, `github-pulse`, - `frameio-scan`, `flight-tracker`, `x-raw-json-backfill`. - - **LLM-requiring → stay:** `social-radar`, `content-ideas`, `adversary-vacuum`, - `ea-inbox-sweep`, `morning-briefing`, `brain-maintenance`. -3. For each deterministic cron, rewrite as: - ```cron - 3 13,16,19,22,1,4,7,10 * * * \ - gbrain jobs submit shell \ - --params '{"cmd":"node scripts/your-script.mjs","cwd":"/data/.openclaw/workspace"}' \ - --max-attempts 3 --timeout-ms 300000 - ``` -4. Watch `gbrain jobs get ` for exit_code / stdout_tail / stderr_tail on each - fire. Compare against pre-migration behavior before approving the next batch. - -**No skill edits required.** The handler runs worker-side; skill files don't -change. If your host exposed custom handlers via the plugin contract (v0.11.0), -they still work the same way. - -Iron rule: **never auto-rewrite the operator's crontab.** Every rewrite is -per-cron, human-approved, with a diff. If you want automation later, the -upcoming `gbrain crontab-to-minions ` helper is P1 in TODOS. - ---- - -## v0.16.0: durable agent runtime - -v0.15 ships `gbrain agent run` / `gbrain agent logs`, a new `subagent` handler -type in Minions, and a plugin contract for host-repo subagent defs. None of the -existing skills need surgery. The question for downstream agents is *how* to -adopt the new runtime, not how to patch around a breaking change. - -### 1. Run a worker with an Anthropic key - -The subagent handlers (`subagent` and `subagent_aggregator`) are always -registered on the worker. No separate opt-in flag — `ANTHROPIC_API_KEY` is -the natural cost gate (no key, the SDK call fails on the first turn), and -who-can-submit is already protected (`PROTECTED_JOB_NAMES` + trusted-submit: -MCP callers get `permission_denied`; only `gbrain agent run` can insert -these rows). - -```bash -ANTHROPIC_API_KEY=sk-ant-... gbrain jobs work -``` - -Worker startup prints: - -``` -[minion worker] subagent handlers enabled -``` - -### 2. Ship your subagents as a plugin (OpenClaw + similar) - -Move your custom subagent definitions out of your gbrain fork and into your own -repo as a plugin. Concretely: - -``` -~//gbrain-plugin/ -├── gbrain.plugin.json -└── subagents/ - ├── meeting-ingestion.md - ├── signal-detector.md - └── daily-task-prep.md -``` - -`gbrain.plugin.json`: - -```json -{ - "name": "your-openclaw", - "version": "2026.4.20", - "plugin_version": "gbrain-plugin-v1" -} -``` - -Each `subagents/*.md` is a plain-text agent definition — YAML frontmatter + -body-as-system-prompt. Recognized frontmatter fields: `name`, `model`, -`max_turns`, `allowed_tools` (must subset the derived brain-tool registry). - -Turn it on: - -```bash -export GBRAIN_PLUGIN_PATH="$HOME//gbrain-plugin" -``` - -Worker startup prints `[plugin-loader] loaded '' v (N subagents)` -per plugin; any rejection (bad manifest, unknown tool in `allowed_tools`, -version mismatch) shows up as a loud warning at startup, not a silent dispatch- -time failure. See `docs/guides/plugin-authors.md` for the full contract. - -### 3. Replace ephemeral subagent runs with durable ones - -If your agent currently spawns ephemeral subagents (OpenClaw `Agent()`, ad-hoc -Anthropic API calls, etc.) for work that should survive crashes, sleeps, or -worker restarts, migrate those to `gbrain agent run`. The durability is free: - -```bash -gbrain agent run "analyze my last 50 journal pages for recurring themes" \ - --subagent-def analyzer --fanout-manifest manifests/journal-pages.json -``` - -Every turn persists to `subagent_messages`, every tool call is a two-phase -ledger, and `gbrain agent logs ` shows where it died + what the last -successful call returned. No more "re-run from scratch because the session -context evaporated." - -### 4. `put_page` from subagents writes under an agent namespace - -If you adopted the v0.15 subagent runtime, note that `put_page` calls -originating from a subagent's tool dispatch MUST target -`wiki/agents//...`. The schema shown to the model enforces this -on first try; a server-side fail-closed check rejects anything else. This -does NOT affect your skill files, CLI put_page calls, or MCP put_page — -only tool-dispatched writes from inside an LLM loop. - -Aggregation output (the final "here's what all N children found" brain page) -goes via a separate trusted CLI path, not through a subagent tool call, so -it can write anywhere you want. - -Iron rule: **never grant an agent write access beyond its namespace**. The -server-side check exists because dispatcher bugs happen; treat it as defense -in depth, not the primary boundary. - ---- - -## v0.22.4 — frontmatter-guard adoption - -### 1. Stop hand-rolling frontmatter validators - -If your fork has scripts that call `js-yaml` directly to validate brain page -frontmatter, replace them with `gbrain frontmatter validate` calls. The CLI -covers the seven canonical error classes and ships a `--json` envelope that's -stable across releases. - -```diff -- # Custom validator script -- node scripts/validate-frontmatter.mjs -+ gbrain frontmatter validate --json -``` - -For consumers that need the validator inside another script, import from -gbrain's `markdown` export instead of duplicating logic: - -```ts -import { parseMarkdown } from 'gbrain/markdown'; - -const parsed = parseMarkdown(content, filePath, { validate: true, expectedSlug }); -for (const err of parsed.errors ?? []) { - // err.code: MISSING_OPEN | MISSING_CLOSE | YAML_PARSE | SLUG_MISMATCH | - // NULL_BYTES | NESTED_QUOTES | EMPTY_FRONTMATTER -} -``` - -### 2. Drop any references to `lib/brain-writer.mjs` - -If your fork's skills or scripts referenced an aspirational -`lib/brain-writer.mjs` (it never shipped — the spec was in PR #392 and never -landed), replace those references with the gbrain CLI. The `frontmatter-guard` -skill lives at `skills/frontmatter-guard/SKILL.md` and points at -`gbrain frontmatter validate` / `audit` / `install-hook`. - -### 3. Wire the doctor subcheck into your health pipeline - -`gbrain doctor` now reports `frontmatter_integrity` automatically. If your -fork has a custom health pipeline (e.g. a daily Slack post about brain -health), pull from `gbrain doctor --json` and surface the -`frontmatter_integrity` row counts. - -### 4. (Optional) Install the pre-commit hook on brain repos - -For sources backed by git, the v0.22.4 install-hook helper drops a -pre-commit script that blocks commits with malformed frontmatter: - -```bash -gbrain frontmatter install-hook -``` - -Skip this if your brain isn't a git repo or if your downstream agent already -enforces validation at write time. See `docs/integrations/pre-commit.md` for -the full recipe. - -### 5. Migration ergonomics — read pending-host-work.jsonl - -After `gbrain apply-migrations --yes` runs the v0.22.4 audit, your agent -should read `~/.gbrain/migrations/pending-host-work.jsonl` (filter to -`migration === "0.22.4"`) and walk each entry's `command` field. Each entry -points to a per-source `gbrain frontmatter validate --fix` -command — surface counts to the user, get explicit consent, then run. - -The migration is **audit-only**. It never mutates brain content during -`apply-migrations`. Your agent runs the fix command with user consent. - ---- - -## Future versions - -When gbrain ships a new version, this doc will be updated with the diffs for that -version. Each new version appends a section; old sections stay so you can catch up -multiple versions at once. - -To check what your fork is missing: -```bash -diff <(grep -A3 "Based on gbrain" ~//skills/brain-ops/SKILL.md) \ - <(grep "v[0-9]" ~/gbrain/skills/migrations/ | tail -3) -``` - - -## v0.36.5.0 — Free-form secret inheritance for shell jobs calling `gbrain` CLI - -**The change.** Shell-job params get a new `inherit:` field. Pass any -snake_case config-key name on it; the worker resolves the value from its -`loadConfig()` at child-spawn time and injects it into the child env. Names -land in the row; values never persist from `inherit:`. Validation runs -**pre-enqueue** in both submit paths (CLI + `submit_job` op), so a malformed -payload never lands in `minion_jobs.data`. - -**Why.** Pre-v0.36.5.0, agents that wanted to call `gbrain` from shell jobs -had to either write `database_url` to `~/.gbrain/config.json` plaintext or -pass `env: { GBRAIN_DATABASE_URL: "..." }` per-job. Both left plaintext -secrets somewhere — disk or DB row. `inherit:` keeps names in the row and -resolves values at spawn time. - -**What your agent can do.** `inherit:` is free-form. Pass any config-key: - -```jsonc -{ - "cmd": "gbrain sync --skip-failed && gbrain embed --stale", - "cwd": "/data/gbrain", - "inherit": ["database_url", "anthropic_api_key", "voyage_api_key"] -} -``` - -The env-key name in the child is derived by uppercasing the config-key: -`database_url` → `GBRAIN_DATABASE_URL`, `anthropic_api_key` → -`ANTHROPIC_API_KEY`, `voyage_api_key` → `VOYAGE_API_KEY`, etc. The validator -does NOT police which config keys you inherit — the agent is in the same -uid as the worker, so it's the agent's call. - -**You can still use `env:`.** v0.36.5.0 does not forbid `env:{ ANYTHING }`. -If you have a reason to put a value in the row plaintext (a non-secret -correlation token, or a secret you know is OK to persist), pass it via -`env:`. Prefer `inherit:` when you want the value out of the row. - -**Worker setup** (one-time, per host): - -- `gbrain config set database_url postgresql://...` (or any other key you - want available for inherit) -- OR put the key in `~/.gbrain/config.json` directly -- OR set `GBRAIN_DATABASE_URL` / `DATABASE_URL` / per-provider env on the - worker process - -If the worker can't resolve a requested name, the validator fail-fasts at -submit time with `gbrain config set ` hint. No more silent "No database -URL" failures in child stderr minutes after submission. - -**Also new.** A `gbrain doctor` check `home_dir_in_worktree` warns if -`~/.gbrain/` lives inside a git worktree. A retroactive `~/.gbrain/.gitignore` -(single line `*`) is now laid down by every `saveConfig()` call AND by -`gbrain post-upgrade`, so existing users get coverage without re-running -`gbrain init`. Honest scope: the `.gitignore` covers casual `git add` but does -NOT cover already-tracked files, screenshots, backups, or `git add -f`. - -**Strategy framing.** For agent-to-gbrain calls, the new canonical guide is -`docs/guides/agent-to-gbrain.md`. Two distinct surfaces: HTTP MCP via OAuth -for ops with MCP equivalents (`search`, `query`, `put_page`, etc.), and shell -job + `inherit:` for `localOnly` admin ops (`sync`, `embed`, `dream`, -`doctor`, etc.). Not a fallback hierarchy — pick by op. - -**Errors to handle** (your agent submits shell jobs; surface these clearly): - -| Error | What it means | Agent action | -|---|---|---| -| `shell: inherit must be an array of config-key names` | `inherit` wasn't an array. | Pass `"inherit": ["database_url", ...]`. | -| `shell: inherit entries must be non-empty strings` | Element was empty, non-string, or null. | Use snake_case config-key names. | -| `shell: inherit name "" must match [a-z][a-z0-9_]*` | Name failed snake_case regex (uppercase, leading underscore, etc.). | Use the config-key verbatim — `database_url`, not `DATABASE_URL`. | -| `shell: inherit requested "" but worker has no configured` | Worker can't resolve the name from its `loadConfig()`. | Run `gbrain config set ` on the worker host. | - ---- diff --git a/llms.txt b/llms.txt index b904a0ad6..bfe0b83bb 100644 --- a/llms.txt +++ b/llms.txt @@ -22,6 +22,7 @@ Repo: https://github.com/garrytan/gbrain - [docs/guides/cron-schedule.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/cron-schedule.md): Recurring job scheduling. - [docs/guides/minions-deployment.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/minions-deployment.md): Deploying the gbrain jobs worker: crontab + watchdog, inline --follow, systemd/Procfile/fly.toml, upgrade checklist. - [docs/guides/quiet-hours.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/quiet-hours.md): Notification hold + timezone-aware delivery. +- [docs/guides/scaling-skills.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md): Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss. - [docs/mcp/DEPLOY.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md): MCP server deployment. ## AI providers diff --git a/package.json b/package.json index ef5598ebf..338b3ddb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gbrain", - "version": "0.41.6.0", + "version": "0.41.7.0", "description": "Postgres-native personal knowledge brain with hybrid RAG search", "type": "module", "main": "src/core/index.ts", diff --git a/scripts/llms-config.ts b/scripts/llms-config.ts index de8553f3d..02e9454b1 100644 --- a/scripts/llms-config.ts +++ b/scripts/llms-config.ts @@ -119,6 +119,12 @@ export const SECTIONS: DocSection[] = [ description: "Notification hold + timezone-aware delivery.", path: "docs/guides/quiet-hours.md", }, + { + title: "docs/guides/scaling-skills.md", + description: + "Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss.", + path: "docs/guides/scaling-skills.md", + }, { title: "docs/mcp/DEPLOY.md", description: "MCP server deployment.", @@ -176,6 +182,12 @@ export const SECTIONS: DocSection[] = [ description: "Patches for downstream agent skill forks. One section per release.", path: "docs/UPGRADING_DOWNSTREAM_AGENTS.md", + // Excluded from inlined bundle (v0.41.7.0): 25KB of release-by-release + // migration patches that are valuable as a reference but don't need + // to ride along in every llms-full.txt fetch. Pushes the bundle back + // under FULL_SIZE_BUDGET after the v0.41.7.0 scaling-skills guide + // landed. + includeInFull: false, }, { title: "skills/migrations/", diff --git a/src/core/check-resolvable.ts b/src/core/check-resolvable.ts index bb1249d1c..ad94e9fe7 100644 --- a/src/core/check-resolvable.ts +++ b/src/core/check-resolvable.ts @@ -114,7 +114,32 @@ interface ResolverEntry { section: string; // e.g., 'Brain operations' } -/** Parse RESOLVER.md markdown tables into structured entries. */ +/** + * Parse RESOLVER.md / AGENTS.md into structured entries. Supports two formats + * that can mix in one file: + * + * Format 1 (table) — original gbrain shape: + * | trigger phrase | `skills//SKILL.md` | + * + * Format 2 (compact list, v0.41.7.0) — OpenClaw-native shape: + * - **skill-name**: trigger1 | trigger2 | trigger3 + * - skill-name: trigger1 | trigger2 + * + * List-format constraints (v0.41.7.0): + * - Skill name MUST be kebab-lowercase (`[a-z][a-z0-9-]+`). Bold names + * like `**Note**`, `**Convention**`, `**TODO**` are deliberately + * skipped so prose bullets in real-world AGENTS.md files don't get + * mis-parsed as skill rows. + * - `skillPath` is ALWAYS derived as `skills//SKILL.md`. An + * optional `→ \`skills/path\`` (or ASCII `->`) suffix is stripped from + * the trigger string but NOT honored as the path: downstream consumers + * (`routing-eval.ts:skillSlugFromPath`, the manifest lookup at this + * file's :367) both assume the convention. For non-conventional paths, + * use the table format. + * - Multiple triggers fan out to one entry per trigger, all sharing the + * same `skillPath`. `checkResolvable` dedupes by `skillPath` downstream, + * so the integration reachability count counts each skill once. + */ export function parseResolverEntries(resolverContent: string): ResolverEntry[] { const entries: ResolverEntry[] = []; let currentSection = ''; @@ -127,29 +152,55 @@ export function parseResolverEntries(resolverContent: string): ResolverEntry[] { continue; } - // Skip non-table rows - if (!line.startsWith('|') || line.includes('---')) continue; + // ── Format 1: Markdown table rows ── + if (line.startsWith('|') && !line.includes('---')) { + const cols = line.split('|').map(c => c.trim()).filter(Boolean); + if (cols.length < 2) continue; - // Split table columns - const cols = line.split('|').map(c => c.trim()).filter(Boolean); - if (cols.length < 2) continue; + const trigger = cols[0]; + const skillCol = cols[1]; - const trigger = cols[0]; - const skillCol = cols[1]; + // Skip header rows + if (trigger.toLowerCase() === 'trigger' || trigger.toLowerCase() === 'skill') continue; - // Skip header rows - if (trigger.toLowerCase() === 'trigger' || trigger.toLowerCase() === 'skill') continue; + // GStack / external references (Check `ACCESS_POLICY.md`, Read X, GStack: Y) + if (skillCol.startsWith('GStack:') || skillCol.startsWith('Check ') || skillCol.startsWith('Read ')) { + entries.push({ trigger, skillPath: skillCol, isGStack: true, section: currentSection }); + continue; + } - // Check for GStack entries - if (skillCol.startsWith('GStack:') || skillCol.startsWith('Check ') || skillCol.startsWith('Read ')) { - entries.push({ trigger, skillPath: skillCol, isGStack: true, section: currentSection }); + // Backtick-wrapped skill path + const pathMatch = skillCol.match(/`(skills\/[^`]+\/SKILL\.md)`/); + if (pathMatch) { + entries.push({ trigger, skillPath: pathMatch[1], isGStack: false, section: currentSection }); + } continue; } - // Extract skill path from backtick-wrapped references - const pathMatch = skillCol.match(/`(skills\/[^`]+\/SKILL\.md)`/); - if (pathMatch) { - entries.push({ trigger, skillPath: pathMatch[1], isGStack: false, section: currentSection }); + // ── Format 2: Compact list rows (v0.41.7.0) ── + // Bold form preferred: `- **skill-name**: trigger1 | trigger2` + // Plain fallback: `- skill-name: trigger1 | trigger2` + // Name regex is kebab-lowercase only so prose bullets like `- **Note**: …` + // don't false-match as skill rows (codex F2 / D4). + const listBold = line.match(/^-\s+\*\*([a-z][a-z0-9-]+)\*\*\s*:\s*(.+)$/); + const listPlain = listBold ? null : line.match(/^-\s+([a-z][a-z0-9-]+)\s*:\s*(.+)$/); + const listMatch = listBold ?? listPlain; + if (listMatch) { + const skillName = listMatch[1]; + const triggersRaw = listMatch[2].trim(); + // Strip optional explicit path suffix (D3: stripped, NOT captured). + // Both Unicode → and ASCII -> accepted; skillPath is always derived. + const cleaned = triggersRaw.replace(/\s*(?:→|->)\s*`skills\/[^`]+`\s*$/, ''); + // Split on |, drop empty pieces and the literal `...` placeholder. + const triggers = cleaned + .split('|') + .map(t => t.trim()) + .filter(t => t.length > 0 && t !== '...'); + const skillPath = `skills/${skillName}/SKILL.md`; + // Multiple entries share skillPath; checkResolvable dedupes downstream. + for (const trigger of triggers) { + entries.push({ trigger, skillPath, isGStack: false, section: currentSection }); + } } } diff --git a/test/check-resolvable-openclaw-compact.test.ts b/test/check-resolvable-openclaw-compact.test.ts new file mode 100644 index 000000000..606f88fe2 --- /dev/null +++ b/test/check-resolvable-openclaw-compact.test.ts @@ -0,0 +1,140 @@ +/** + * v0.41.7.0 — Regression suite for compact list-format resolvers. + * + * The bisect anchor for the OpenClaw scaling regression: pre-v0.41.7.0, + * any agent that wrote the compact `- **name**: t1 | t2` shape (instead + * of the markdown table) saw every skill reported as unreachable by + * `gbrain doctor`. The OpenClaw deployment regression was 238 FAIL + * errors → 0 errors after the parser fix. + * + * Two fixtures drive three regression tests: + * + * 1. test/fixtures/openclaw-compact-resolver/ — list-format only, + * ~10 skills with valid frontmatter triggers, plus a prose-bullet + * section that pins the D4 kebab-lowercase regex tighten. + * + * 2. test/fixtures/openclaw-mixed-merge/ — table-format + * skills/RESOLVER.md + parent ../AGENTS.md (compact list). Pins + * the v0.31.7 D-CX-14 multi-resolver merge case. + */ + +import { describe, test, expect } from "bun:test"; +import { join } from "path"; +import { checkResolvable } from "../src/core/check-resolvable.ts"; + +const COMPACT_FIXTURE = join( + import.meta.dir, + "fixtures", + "openclaw-compact-resolver", + "skills" +); + +const MIXED_MERGE_FIXTURE = join( + import.meta.dir, + "fixtures", + "openclaw-mixed-merge", + "skills" +); + +describe("v0.41.7.0 — compact list-format resolver (PR #1370 regression)", () => { + const report = checkResolvable(COMPACT_FIXTURE); + + test("every skill in the manifest is reachable from the list-format resolver", () => { + // The headline assertion: pre-v0.41.7.0 this was unreachable=N for + // every skill in the fixture. Post-fix, unreachable === 0. + expect(report.summary.unreachable).toBe(0); + expect(report.summary.reachable).toBe(report.summary.total_skills); + expect(report.summary.total_skills).toBeGreaterThanOrEqual(10); + }); + + test("zero error-severity issues", () => { + // The headline 238 FAILs → 0 outcome. errors only; warnings are + // separately gated below. + if (report.errors.length > 0) { + console.error( + "Unexpected errors:\n", + report.errors.map(e => ` - [${e.type}] ${e.skill}: ${e.message}`).join("\n") + ); + } + expect(report.errors.length).toBe(0); + expect(report.ok).toBe(true); + }); + + test("zero mece_gap warnings (fixture stubs ship valid triggers)", () => { + // D5 fixture upgrade: every SKILL.md stub carries valid frontmatter + // triggers, so the mece_gap detection should stay silent. If this + // assertion ever fires, a fixture file lost its triggers: array. + const gaps = report.warnings.filter(w => w.type === "mece_gap"); + if (gaps.length > 0) { + console.error( + "Unexpected mece_gap warnings:\n", + gaps.map(w => ` - ${w.skill}: ${w.message}`).join("\n") + ); + } + expect(gaps.length).toBe(0); + }); + + test("D4 REGRESSION: prose bullets do not surface as orphan triggers", () => { + // The compact RESOLVER.md fixture intentionally embeds 4 prose + // bullets (`- **Note**:`, `- **Convention**:`, `- **TODO**:`, + // `- **Important**:`). The kebab-lowercase regex rejects them + // before they reach the resolver entry stream, so we should NOT + // see orphan_trigger warnings naming any of these. + const proseBulletNames = ["Note", "Convention", "TODO", "Important"]; + const orphans = report.warnings.filter(w => w.type === "orphan_trigger"); + for (const name of proseBulletNames) { + const hit = orphans.find(w => w.skill === name); + expect(hit, `prose bullet "${name}" should not surface as orphan_trigger`).toBeUndefined(); + } + }); + + test("zero missing_file warnings (every list entry resolves to disk)", () => { + const missing = report.warnings.filter(w => w.type === "missing_file"); + if (missing.length > 0) { + console.error( + "Unexpected missing_file warnings:\n", + missing.map(w => ` - ${w.skill}: ${w.message}`).join("\n") + ); + } + expect(missing.length).toBe(0); + }); +}); + +describe("v0.41.7.0 — D-CX-14 mixed-merge (table + parent AGENTS.md)", () => { + const report = checkResolvable(MIXED_MERGE_FIXTURE); + + test("every skill is reachable across both resolver files", () => { + // 5 skills routed from skills/RESOLVER.md (table format) + // + 3 skills routed from ../AGENTS.md (compact list format) + // = 8 total. All reachable via the v0.31.7 multi-file merge. + expect(report.summary.total_skills).toBe(8); + expect(report.summary.unreachable).toBe(0); + expect(report.summary.reachable).toBe(8); + }); + + test("zero error-severity issues across the merged resolver set", () => { + if (report.errors.length > 0) { + console.error( + "Unexpected errors:\n", + report.errors.map(e => ` - [${e.type}] ${e.skill}: ${e.message}`).join("\n") + ); + } + expect(report.errors.length).toBe(0); + expect(report.ok).toBe(true); + }); + + test("both table and list shapes contribute skills to the merge", () => { + // Sanity check: if the merge silently dropped one shape's entries, + // we'd see unreachable > 0. This test exists to guard against the + // regression where one shape's parser starts swallowing the other's + // output via the dedup-by-skillPath path. + const expectedTableSkills = ["query", "enrich", "briefing", "migrate", "setup"]; + const expectedListSkills = ["adversary-tracking", "civic-intelligence", "book-mirror"]; + for (const name of [...expectedTableSkills, ...expectedListSkills]) { + const unreachable = report.errors.find( + e => e.type === "unreachable" && e.skill === name + ); + expect(unreachable, `${name} should be reachable`).toBeUndefined(); + } + }); +}); diff --git a/test/check-resolvable.test.ts b/test/check-resolvable.test.ts index b4e06f619..a813f8f8f 100644 --- a/test/check-resolvable.test.ts +++ b/test/check-resolvable.test.ts @@ -66,6 +66,133 @@ describe("parseResolverEntries", () => { expect(entries[0].section).toBe("Always-on"); expect(entries[1].section).toBe("Brain operations"); }); + + // ────────────────────────────────────────────────────────────────────── + // v0.41.7.0 — Compact list-format support (OpenClaw-native shape) + // + // The list branch was added so `gbrain doctor` no longer reports every + // skill as unreachable on agents that write the compact `- **name**: t1 | t2` + // shape instead of the markdown table. The OpenClaw 306-skill regression + // was 238 FAIL errors → 0 errors after this fix. + // ────────────────────────────────────────────────────────────────────── + + test("[list] bold-name single trigger", () => { + const content = `## Personal +- **gift-advisor**: gift idea`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(1); + expect(entries[0].trigger).toBe("gift idea"); + expect(entries[0].skillPath).toBe("skills/gift-advisor/SKILL.md"); + expect(entries[0].isGStack).toBe(false); + expect(entries[0].section).toBe("Personal"); + }); + + test("[list] bold-name multi-trigger fan-out", () => { + const content = `- **flight-tracker**: track my flight | flight status | when does my flight land`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(3); + expect(entries.map(e => e.trigger)).toEqual([ + "track my flight", + "flight status", + "when does my flight land", + ]); + expect(entries.every(e => e.skillPath === "skills/flight-tracker/SKILL.md")).toBe(true); + }); + + test("[list] plain-name fallback (no bold markers)", () => { + const content = `- gift-advisor: gift idea | birthday gift`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(2); + expect(entries[0].skillPath).toBe("skills/gift-advisor/SKILL.md"); + expect(entries[1].trigger).toBe("birthday gift"); + }); + + test("[list] D3: path suffix with Unicode → is stripped, not captured", () => { + // D3 walkback: downstream consumers (routing-eval.ts skillSlugFromPath + // and check-resolvable.ts:367 manifest derivation) assume the + // skills//SKILL.md shape. Capturing the explicit path would + // produce silent orphan_trigger warnings + routing-eval coverage + // gaps for that skill. So the suffix is stripped and the path is + // always derived from the name. + const content = "- **quality**: lint the page → `skills/conventions/quality.md`"; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(1); + expect(entries[0].trigger).toBe("lint the page"); + expect(entries[0].skillPath).toBe("skills/quality/SKILL.md"); // derived, NOT the captured path + }); + + test("[list] F4: path suffix with ASCII -> is also stripped", () => { + const content = "- **quality**: lint the page -> `skills/conventions/quality.md`"; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(1); + expect(entries[0].trigger).toBe("lint the page"); + expect(entries[0].skillPath).toBe("skills/quality/SKILL.md"); + }); + + test("[list] ellipsis placeholder is dropped", () => { + const content = `- **foo-skill**: bar | ... | baz`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(2); + expect(entries.map(e => e.trigger)).toEqual(["bar", "baz"]); + }); + + test("[list] empty pipe segments are dropped", () => { + const content = `- **foo-skill**: bar | | baz`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(2); + expect(entries.map(e => e.trigger)).toEqual(["bar", "baz"]); + }); + + test("[mixed] table and list rows in the same file", () => { + const content = `## Always-on +| Trigger | Skill | +|---------|-------| +| Every message | \`skills/signal-detector/SKILL.md\` | + +## Personal +- **gift-advisor**: gift idea | what should I bring`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(3); + expect(entries[0].skillPath).toBe("skills/signal-detector/SKILL.md"); + expect(entries[0].section).toBe("Always-on"); + expect(entries[1].skillPath).toBe("skills/gift-advisor/SKILL.md"); + expect(entries[1].section).toBe("Personal"); + expect(entries[2].section).toBe("Personal"); + }); + + test("[list] section tracking across list entries", () => { + const content = `## Personal +- **gift-advisor**: gift idea + +## Travel +- **flight-tracker**: track my flight`; + const entries = parseResolverEntries(content); + expect(entries[0].section).toBe("Personal"); + expect(entries[1].section).toBe("Travel"); + }); + + test("[list] D4 REGRESSION: prose bullets do not match as skill rows", () => { + // The kebab-lowercase name regex deliberately rejects bold names + // starting with an uppercase letter. This kills the noise class where + // real AGENTS.md files have prose bullets like `- **Note**: …` that + // would otherwise be parsed as fake skill rows pointing at + // `skills/Note/SKILL.md`. Codex F2; D4 decision. + const content = `- **Note**: this is a prose bullet +- **Convention**: see [some convention link] +- **TODO**: fix this later +- **Important**: read this carefully`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(0); + }); + + test("[list] D4 negative: convention-violating uppercase names are silently skipped", () => { + // Documents the trade: a real skill named `MyTool` would not parse. + // The user would notice on the first `gbrain doctor` run and lowercase it. + const content = `- **MyTool**: this trigger silently disappears +- **camelCase**: also silently dropped`; + const entries = parseResolverEntries(content); + expect(entries.length).toBe(0); + }); }); describe("checkResolvable — real skills directory", () => { diff --git a/test/fixtures/openclaw-compact-resolver/skills/RESOLVER.md b/test/fixtures/openclaw-compact-resolver/skills/RESOLVER.md new file mode 100644 index 000000000..8c84b9302 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/RESOLVER.md @@ -0,0 +1,32 @@ +# Compact-format resolver (test fixture) + +Tests v0.41.7.0 list-format parser. Skills use clearly-fictional names +so they don't shadow real bundled skills. + +## Always-on +- **brain-search**: search my brain | what do we know about | find references to + +## Personal +- **gift-advisor**: gift idea | what should I bring | birthday gift | housewarming +- **flight-tracker**: track my flight | flight status | when does my flight land + +## Email + Calendar +- **email-triage**: triage email | sort my inbox | morning email +- **meeting-prep**: prep for my meeting | meeting in 30 min | get ready for the call +- **calendar-prep**: what's on my calendar | tomorrow's schedule +- **daily-digest**: morning brief | end of day summary + +## Workflows +- **investor-update-ingest**: investor update | portfolio update | company metrics +- **content-creation**: draft a tweet | write a post | new article +- **executive-assistant**: schedule a meeting | block time | reschedule + +## Notes +The bullets below are prose, not skill rows. The v0.41.7.0 parser +rejects them via the kebab-lowercase name regex. If you see entries +materialize for any of these, the D4 regex tighten regressed. + +- **Note**: this is a prose bullet (capitalized name, must be skipped) +- **Convention**: see [parent docs] (capitalized name, must be skipped) +- **TODO**: nothing here is a real skill row (capitalized, skipped) +- **Important**: this is just a callout (capitalized, skipped) diff --git a/test/fixtures/openclaw-compact-resolver/skills/brain-search/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/brain-search/SKILL.md new file mode 100644 index 000000000..b404f239f --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/brain-search/SKILL.md @@ -0,0 +1,10 @@ +--- +name: brain-search +triggers: + - "search my brain" + - "what do we know about" + - "find references to" +--- +# brain-search + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/calendar-prep/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/calendar-prep/SKILL.md new file mode 100644 index 000000000..c4f5c0037 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/calendar-prep/SKILL.md @@ -0,0 +1,9 @@ +--- +name: calendar-prep +triggers: + - "what's on my calendar" + - "tomorrow's schedule" +--- +# calendar-prep + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/content-creation/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/content-creation/SKILL.md new file mode 100644 index 000000000..a5a284ce1 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/content-creation/SKILL.md @@ -0,0 +1,10 @@ +--- +name: content-creation +triggers: + - "draft a tweet" + - "write a post" + - "new article" +--- +# content-creation + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/daily-digest/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/daily-digest/SKILL.md new file mode 100644 index 000000000..f9111e2d9 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/daily-digest/SKILL.md @@ -0,0 +1,9 @@ +--- +name: daily-digest +triggers: + - "morning brief" + - "end of day summary" +--- +# daily-digest + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/email-triage/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/email-triage/SKILL.md new file mode 100644 index 000000000..7e5400584 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/email-triage/SKILL.md @@ -0,0 +1,10 @@ +--- +name: email-triage +triggers: + - "triage email" + - "sort my inbox" + - "morning email" +--- +# email-triage + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/executive-assistant/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/executive-assistant/SKILL.md new file mode 100644 index 000000000..31ef8aa87 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/executive-assistant/SKILL.md @@ -0,0 +1,10 @@ +--- +name: executive-assistant +triggers: + - "schedule a meeting" + - "block time" + - "reschedule" +--- +# executive-assistant + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/flight-tracker/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/flight-tracker/SKILL.md new file mode 100644 index 000000000..a6be42da4 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/flight-tracker/SKILL.md @@ -0,0 +1,10 @@ +--- +name: flight-tracker +triggers: + - "track my flight" + - "flight status" + - "when does my flight land" +--- +# flight-tracker + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/gift-advisor/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/gift-advisor/SKILL.md new file mode 100644 index 000000000..148a13093 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/gift-advisor/SKILL.md @@ -0,0 +1,11 @@ +--- +name: gift-advisor +triggers: + - "gift idea" + - "what should I bring" + - "birthday gift" + - "housewarming" +--- +# gift-advisor + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/investor-update-ingest/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/investor-update-ingest/SKILL.md new file mode 100644 index 000000000..cb95d49c0 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/investor-update-ingest/SKILL.md @@ -0,0 +1,10 @@ +--- +name: investor-update-ingest +triggers: + - "investor update" + - "portfolio update" + - "company metrics" +--- +# investor-update-ingest + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-compact-resolver/skills/meeting-prep/SKILL.md b/test/fixtures/openclaw-compact-resolver/skills/meeting-prep/SKILL.md new file mode 100644 index 000000000..6bba7d1e6 --- /dev/null +++ b/test/fixtures/openclaw-compact-resolver/skills/meeting-prep/SKILL.md @@ -0,0 +1,10 @@ +--- +name: meeting-prep +triggers: + - "prep for my meeting" + - "meeting in 30 min" + - "get ready for the call" +--- +# meeting-prep + +Stub for fixture testing (v0.40.11.0 compact-resolver regression suite). diff --git a/test/fixtures/openclaw-mixed-merge/AGENTS.md b/test/fixtures/openclaw-mixed-merge/AGENTS.md new file mode 100644 index 000000000..5c80a5177 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/AGENTS.md @@ -0,0 +1,11 @@ +# Workspace-root AGENTS.md (compact list, parent dir) + +Tests the v0.40.11.0 + v0.31.7 D-CX-14 multi-resolver merge case: +a workspace where the thin skills/RESOLVER.md (table format, ships with +the skillpack) merges with the fat parent AGENTS.md (compact list, the +real dispatcher). + +## Skills routed from the workspace +- **adversary-tracking**: track adversaries | competitive intel +- **civic-intelligence**: civic check | local news brief +- **book-mirror**: mirror a chapter | personalize this book diff --git a/test/fixtures/openclaw-mixed-merge/skills/RESOLVER.md b/test/fixtures/openclaw-mixed-merge/skills/RESOLVER.md new file mode 100644 index 000000000..efb708bc2 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/RESOLVER.md @@ -0,0 +1,17 @@ +# Skillpack RESOLVER.md (table format, ships in the bundled skills/) + +The conventional gbrain-shape resolver. Merges with `../AGENTS.md` (compact +list, the workspace dispatcher) via the D-CX-14 multi-resolver merge. + +## Brain operations +| Trigger | Skill | +|---------|-------| +| "What do we know about" | `skills/query/SKILL.md` | +| Creating a person page | `skills/enrich/SKILL.md` | +| Briefing on someone | `skills/briefing/SKILL.md` | + +## Maintenance +| Trigger | Skill | +|---------|-------| +| Migrating a brain | `skills/migrate/SKILL.md` | +| Running upgrade | `skills/setup/SKILL.md` | diff --git a/test/fixtures/openclaw-mixed-merge/skills/adversary-tracking/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/adversary-tracking/SKILL.md new file mode 100644 index 000000000..d0e373331 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/adversary-tracking/SKILL.md @@ -0,0 +1,9 @@ +--- +name: adversary-tracking +triggers: + - "track adversaries" + - "competitive intel" +--- +# adversary-tracking + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/book-mirror/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/book-mirror/SKILL.md new file mode 100644 index 000000000..a6dff0da7 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/book-mirror/SKILL.md @@ -0,0 +1,9 @@ +--- +name: book-mirror +triggers: + - "mirror a chapter" + - "personalize this book" +--- +# book-mirror + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/briefing/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/briefing/SKILL.md new file mode 100644 index 000000000..b4e2152e9 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/briefing/SKILL.md @@ -0,0 +1,8 @@ +--- +name: briefing +triggers: + - "Briefing on someone" +--- +# briefing + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/civic-intelligence/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/civic-intelligence/SKILL.md new file mode 100644 index 000000000..4f3a1f3ab --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/civic-intelligence/SKILL.md @@ -0,0 +1,9 @@ +--- +name: civic-intelligence +triggers: + - "civic check" + - "local news brief" +--- +# civic-intelligence + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/enrich/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/enrich/SKILL.md new file mode 100644 index 000000000..0b3e4956c --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/enrich/SKILL.md @@ -0,0 +1,8 @@ +--- +name: enrich +triggers: + - "Creating a person page" +--- +# enrich + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/migrate/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/migrate/SKILL.md new file mode 100644 index 000000000..b8d903395 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/migrate/SKILL.md @@ -0,0 +1,8 @@ +--- +name: migrate +triggers: + - "Migrating a brain" +--- +# migrate + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/query/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/query/SKILL.md new file mode 100644 index 000000000..83b0e19b3 --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/query/SKILL.md @@ -0,0 +1,8 @@ +--- +name: query +triggers: + - "What do we know about" +--- +# query + +Stub for fixture testing (v0.40.11.0 mixed-merge regression). diff --git a/test/fixtures/openclaw-mixed-merge/skills/setup/SKILL.md b/test/fixtures/openclaw-mixed-merge/skills/setup/SKILL.md new file mode 100644 index 000000000..5b19071be --- /dev/null +++ b/test/fixtures/openclaw-mixed-merge/skills/setup/SKILL.md @@ -0,0 +1,8 @@ +--- +name: setup +triggers: + - "Running upgrade" +--- +# setup + +Stub for fixture testing (v0.40.11.0 mixed-merge regression).