v0.41.7.0 feat: compact list-format resolver + 300-skill scaling tutorial (#1407)

* feat(check-resolvable): parseResolverEntries accepts compact list format

Add the second parser branch alongside the existing markdown-table branch
so RESOLVER.md and AGENTS.md can use the OpenClaw-native list shape:

    - **skill-name**: trigger1 | trigger2 | trigger3
    - skill-name: trigger1 | trigger2

Constraints:
  - Skill names must be kebab-lowercase ([a-z][a-z0-9-]+). Bold names
    starting with an uppercase letter (e.g. **Note**, **Convention**)
    are deliberately skipped so prose bullets in real-world AGENTS.md
    files don't get mis-parsed as fake skill rows.
  - skillPath is always derived as skills/<name>/SKILL.md. An optional
    arrow suffix (Unicode -> or ASCII ->) is stripped from the trigger
    string but NOT honored as a path. Downstream consumers
    (routing-eval.ts skillSlugFromPath, the manifest check at line 367)
    assume the convention. For non-conventional paths, use the table
    format.
  - Multiple triggers fan out to one entry per trigger. checkResolvable
    dedupes by skillPath downstream, so the reachability count counts
    each skill once regardless of trigger fan-out.

The parser body is restructured to an if/else-if shape so the existing
'continue' on non-table rows no longer short-circuits the list branch.

Unit tests cover 11 new cases: bold + plain name shapes, multi-trigger
fan-out, Unicode and ASCII path-suffix strip, ellipsis filter, empty
pipe segments, mixed-shape files, section tracking, and two D4
regression cases (prose-bullet rejection + convention-violation
silent-skip).

Closes #1370 — credit @garrytan-agents for the original PR that flagged
the parser gap.

* test(check-resolvable): integration fixtures + regression suite for compact format

Two fixtures pin the v0.41.7.0 parser fix at the integration layer:

  test/fixtures/openclaw-compact-resolver/
    List-format only RESOLVER.md with 10 fictional skills (gift-advisor,
    flight-tracker, email-triage, etc.), each with valid frontmatter
    triggers. A trailing 'Notes' section embeds 4 prose bullets
    (- **Note**:, - **Convention**:, - **TODO**:, - **Important**:)
    that pin the D4 kebab-lowercase regex tighten: if the regex ever
    regresses to permissive [\w-]+, those prose bullets would surface
    as orphan_trigger warnings and the test fails loudly.

  test/fixtures/openclaw-mixed-merge/
    Tests the v0.31.7 D-CX-14 multi-resolver merge: workspace-root
    AGENTS.md (compact list, 3 skills) + skills/RESOLVER.md (table
    format, 5 skills). The merge dedups by skillPath and counts each
    skill once.

The regression test (test/check-resolvable-openclaw-compact.test.ts)
runs 8 assertions across both fixtures:

  1. unreachable === 0 on the compact fixture (the 'pre-v0.41.7.0
     reported 238 FAILs on a 306-skill OpenClaw, post-fix 0' headline).
  2. zero error-severity issues; report.ok === true.
  3. zero mece_gap warnings (every stub ships valid triggers).
  4. zero orphan_trigger warnings for the 4 prose-bullet names — D4
     regex regression guard at integration level.
  5. zero missing_file warnings.
  6. mixed-merge: total_skills === 8 (5 table + 3 list), all reachable.
  7. mixed-merge: errors.length === 0; report.ok === true.
  8. mixed-merge: each expected skill from BOTH shapes is non-unreachable
     (catches the bug where one shape silently swallows the other via
     dedup-by-skillPath).

* docs(guides): scaling-skills.md walkthrough for 300-skill agents

Three-tier architecture for agents that have outgrown the always-loaded
skill manifest:

  Tier A — always loaded (~35 skills, in the system prompt every turn)
  Tier B — resolver-routed (~85 skills, looked up via RESOLVER.md/AGENTS.md
            only when no Tier A match)
  Tier C — dormant (~180 skills, on disk but not injected into the prompt)

Real numbers from Garry's 306-skill OpenClaw: 25K tokens of skill
descriptions per turn collapsed to 4K tokens (~21K tokens freed per
turn) with zero capability loss. The compact list-format resolver
(v0.41.7.0) is the parser-level enabler for this pattern.

The guide covers:

  - The scaling wall (when the always-loaded manifest stops working)
  - The three tiers + per-turn token math
  - What the resolver actually does (routing-table-but-cheaper pattern)
  - The compact list format (kebab-lowercase contract, optional path
    suffix, mixed-shape support)
  - The 'gbrain doctor' / 'gbrain check-resolvable --strict' safety net
  - Implementation walkthrough (audit → tier → disable → resolver →
    doctor)
  - The scaling curve (50 → 100 → 200 → 300 → 1000, no ceiling)

Voice + privacy cleanup applied per CLAUDE.md rules:
  - Wintermute → 'Garry's OpenClaw' / 'your OpenClaw'
  - Unicode em dashes stripped; ASCII '--' preserved in command flags
  - Made-up 'check_resolvable' invocation replaced with real
    'gbrain doctor' and 'gbrain check-resolvable --json'/'--strict'
  - Blog-style 'Previous in this series' footer dropped

Wiring:
  - scripts/llms-config.ts registers the new guide in the curated
    array so 'bun run build:llms' picks it up. docs/UPGRADING_
    DOWNSTREAM_AGENTS.md excluded from the inlined bundle to stay
    under the 600KB FULL_SIZE_BUDGET after adding the new content.
  - docs/tutorials/README.md gains a one-line entry pointing at the
    guide under Related documentation.
  - llms.txt + llms-full.txt regenerated.

* chore: bump version and changelog (v0.41.7.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update CLAUDE.md for v0.41.7.0 compact-format resolver

Annotate the src/core/check-resolvable.ts entry with the v0.41.7.0
parseResolverEntries compact list-format support: kebab-lowercase name
gate (closes the prose-bullet false-positive class), path-suffix strip
contract (skillPath always derived as skills/<name>/SKILL.md so
routing-eval and the manifest check don't drift), multi-trigger fan-out
plus checkResolvable downstream dedupe, the 238 FAILs to 0 OpenClaw
headline, the two integration fixtures pinning the regression, and the
docs/guides/scaling-skills.md pointer for the tutorial context.

Regenerate llms-full.txt to match (CLAUDE.md edit chaser, per the
CLAUDE.md own rule about test/build-llms.test.ts catching drift).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-25 13:58:26 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 3a2605e9a0
commit 374deff579
34 changed files with 1289 additions and 640 deletions
+40
View File
@@ -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/<name>/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.
+1 -1
View File
@@ -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: <key>` / `env: <VAR>`. `gbrain models doctor [--skip=<provider>] [--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/<name>/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.
+43
View File
@@ -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`)
+1 -1
View File
@@ -1 +1 @@
0.41.6.0
0.41.7.0
+310
View File
@@ -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 `<available_skills>` 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 `<available_skills>` 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/<name>/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/<name>/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).
+1 -1
View File
@@ -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
+318 -619
View File
@@ -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: <key>` / `env: <VAR>`. `gbrain models doctor [--skip=<provider>] [--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/<name>/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 `<available_skills>` 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 `<available_skills>` 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/<name>/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/<name>/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/<your-agent>/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/<your-agent>/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/<skill-name>, extended with <your-agent>-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. `<!-- timeline -->` (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
`<!-- timeline -->` — 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 <id>` 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 <file>` 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:
```
~/<your-agent>/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/<your-agent>/gbrain-plugin"
```
Worker startup prints `[plugin-loader] loaded '<name>' v<ver> (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 <job>` 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/<subagent_id>/...`. 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 <path>
+ gbrain frontmatter validate <path> --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 <source_path> --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" ~/<your-fork>/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 <X>` 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 "<X>" 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 "<X>" but worker has no <X> configured` | Worker can't resolve the name from its `loadConfig()`. | Run `gbrain config set <X> <value>` on the worker host. |
---
+1
View File
@@ -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
+1 -1
View File
@@ -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",
+12
View File
@@ -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/",
+68 -17
View File
@@ -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/<name>/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/<name>/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 });
}
}
}
@@ -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();
}
});
});
+127
View File
@@ -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/<name>/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", () => {
@@ -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)
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
+11
View File
@@ -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
+17
View File
@@ -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` |
@@ -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).
@@ -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).
@@ -0,0 +1,8 @@
---
name: briefing
triggers:
- "Briefing on someone"
---
# briefing
Stub for fixture testing (v0.40.11.0 mixed-merge regression).
@@ -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).
@@ -0,0 +1,8 @@
---
name: enrich
triggers:
- "Creating a person page"
---
# enrich
Stub for fixture testing (v0.40.11.0 mixed-merge regression).
@@ -0,0 +1,8 @@
---
name: migrate
triggers:
- "Migrating a brain"
---
# migrate
Stub for fixture testing (v0.40.11.0 mixed-merge regression).
@@ -0,0 +1,8 @@
---
name: query
triggers:
- "What do we know about"
---
# query
Stub for fixture testing (v0.40.11.0 mixed-merge regression).
@@ -0,0 +1,8 @@
---
name: setup
triggers:
- "Running upgrade"
---
# setup
Stub for fixture testing (v0.40.11.0 mixed-merge regression).