v0.24.0: production-hardening pass on the skillify loop (#387)

* feat: v0.19.0 — skillify loop + AGENTS.md compat + brain-first convention

This is the v0.19.0 release. The branch ships four new CLI commands, a
refactor to check-resolvable, and an expansion of the brain-first
convention for sub-agent tool discovery. The original commit message
described only the convention expansion, undercounting the scope by ~5x;
this amend captures the full release.

NEW COMMANDS

- gbrain skillify scaffold <name>     — 4 stub files + idempotent resolver row
- gbrain skillify check [path]        — 10-item post-task audit (promoted)
- gbrain skillpack list / install     — curated 25-skill bundle, atomic install
- gbrain skillpack diff <name>        — per-file diff preview
- gbrain routing-eval                 — dedicated CI verb for Check 5 fixtures

CHECK-RESOLVABLE REFACTOR

- Accepts AGENTS.md as a resolver file alongside RESOLVER.md, at either
  the skills directory or one level up (workspace root layout).
- Auto-derives the skill manifest by walking skills/*/SKILL.md when
  manifest.json is missing.
- Splits ResolvableReport into errors[] + warnings[] so advisory checks
  (filing audit, routing gaps, DRY violations) don't break CI by default.
- New --strict opt-in flag promotes warnings to exit 1.

BRAIN-FIRST CONVENTION

- skills/conventions/brain-first.md expanded from 5-step lookup guide to
  full sub-agent reference: tool inventory, lookup chain, score thresholds,
  authority hierarchy, sync rules, entity page conventions, sub-agent
  propagation rule.

PRODUCTION-READINESS HARDENING (this branch's review pass)

- routing-eval --llm: emits stderr placeholder notice + runs structural
  layer only. README, CHANGELOG, CLI help all rewritten consistently.
  Was a silent no-op against documented contract.
- skillpack installer: receipt comment in fence (cumulative-slugs="...")
  preserves single-skill-install accumulation while letting install --all
  prune removed bundle skills cleanly. Unknown rows preserved + stderr
  warning for the operating agent. Pre-v0.19 fences upgrade silently.
- skillify scaffold: resolver-row regex broadened to detect backticked,
  quoted, and bare path forms. No duplicate row on --force after the
  user normalizes formatting.
- scripts/check-privacy.sh: now wired into package.json test chain so
  the wintermute-ban rule is actually enforced. New regression test.
- E2E Tier 2 (LLM skills) promoted from schedule-only to required per-PR
  CI. Local Tier 1 + Tier 2 verified clean.
- Stale v0.17/v0.18 version labels rewritten across new files.

TESTS

- test/routing-eval-cli.test.ts: 4 cases covering --llm warn semantics
- test/privacy-script-wired.test.ts: regression guard for CI wiring
- test/skillpack-install.test.ts: 4 new cases for receipt + cumulative
  + unknown-row preserve+warn + pre-v0.19 upgrade path
- test/skillify-scaffold.test.ts: 4 new cases for broadened regex

VERIFICATION

- bun test: 2237 pass / 18 known PGLite-contention flakes (CI green;
  documented as P3 dev-experience in TODOS.md)
- bun run typecheck: clean
- bun run test:e2e: 18/19 files green (1 pre-existing flake on master,
  not caused by this branch — verified via git stash)
- llms.txt + llms-full.txt regenerated to match README + CHANGELOG

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

* fix: scrub banned fork name from public artifacts

The privacy guard wired into the test chain in this branch caught 5
pre-existing references to the banned OpenClaw fork name in CHANGELOG.md
(2x), skills/migrations/v0.19.0.md (1x), src/cli.ts (1x), and
src/commands/sync.ts (1x). All originated in master's v0.19.0 release
notes and migration doc when the privacy script existed but wasn't
wired into CI yet.

Replacements per CLAUDE.md privacy mapping:
- Origin-story copy (CHANGELOG layer narratives, code comments naming
  the production deployment that drove the feature) → "Garry's OpenClaw"
- Reader-facing migration step → "your OpenClaw"

No code semantics changed. Comments + headings only.

Verification: scripts/check-privacy.sh exits 0, full CI guard chain
green (privacy + jsonb + progress + wasm + typecheck).

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

* chore: bump VERSION to 0.24.0 + new CHANGELOG entry

Bump branch version above master's v0.21.0 per CLAUDE.md
"CHANGELOG + VERSION are branch-scoped" rule. The new v0.24.0 entry at
the top of CHANGELOG covers what THIS branch adds vs master:

- routing-eval --llm honesty pass (4-surface contract drift fix)
- skillpack installer cumulative-receipt + unknown-row preserve+warn
  (the Codex-caught regression that would have shipped in master if
  the original v0.19.0 had landed without this branch's review pass)
- skillify scaffold resolver-row regex broadening (backtick + quoted
  + bare forms; idempotency contract preserved under hand-editing)
- 5 banned-name leaks scrubbed from public artifacts
- check-privacy.sh wired into CI test chain + regression guard test
- 7 stale v0.17/v0.18 version labels rewritten across 5 files
- Tier 2 (LLM-skills E2E) promoted from schedule-only to required per-PR

VERSION 0.21.0 → 0.24.0
package.json version field synced.
llms.txt + llms-full.txt regenerated (no content drift; sizes match).

Test suite: 62/62 green across the 5 test files this branch added or
extended (routing-eval-cli, privacy-script-wired, skillpack-install,
skillify-scaffold, build-llms).

CI guards: privacy + jsonb + progress + wasm + typecheck all clean.

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

* docs: update project documentation for v0.24.0

Auto-discovered drift via /document-release after the v0.24.0 hardening
pass landed. All factual corrections clearly warranted by the diff.

CLAUDE.md:
- Skillpack installer: documented the cumulative-slugs receipt comment,
  install --all prune semantics, unknown-row preserve+warn behavior,
  and pre-v0.24 silent upgrade. Was previously vague about
  "tracks a skill manifest so install --update diffs cleanly" without
  explaining what the receipt is or why it matters.
- routing-eval: replaced the false claim that --llm "opts into a Haiku
  tie-break layer for CI." Now correctly describes the placeholder
  semantic landed in v0.24.0 (stderr notice + structural-only run).

README.md:
- Skillpack section: added one paragraph on the receipt comment + the
  user-visible stderr message for hand-added rows. Connects the safe
  rerun promise to the v0.24.0 implementation that actually enforces it.

CONTRIBUTING.md:
- Running tests section: now recommends `bun run test` (full CI guard
  chain + typecheck + tests) before pushing. Names each guard so new
  contributors understand what catches what. The privacy guard (newly
  wired in v0.24.0) is one of these — without `bun run test` you'd skip
  it locally and find out from CI.

llms-full.txt: regenerated to reflect CLAUDE.md changes.

Verification: full guard chain green locally (privacy + jsonb + progress
+ wasm + typecheck).

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

---------

Co-authored-by: Garry Tan <garry@ycombinator.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-30 23:17:54 -07:00
committed by GitHub
co-authored by Garry Tan Claude Opus 4.7
parent 579722d9dc
commit 4fc1246606
30 changed files with 873 additions and 101 deletions
+4 -1
View File
@@ -44,7 +44,10 @@ jobs:
tier2:
name: Tier 2 (LLM Skills)
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
# Runs on every push/PR now (promoted from schedule-only in v0.19.0).
# Tier 1 must pass first; Tier 2 uses OPENAI_API_KEY + ANTHROPIC_API_KEY
# from repo/org secrets. Nightly + manual triggers still supported via
# the workflow-level `on:` list.
needs: tier1
services:
postgres:
+107 -9
View File
@@ -2,6 +2,102 @@
All notable changes to GBrain will be documented in this file.
## [0.24.0] - 2026-04-26
## **The skillify loop stops lying. Privacy guard runs, `--llm` is honest, ghost rows go away.**
## **Plus: Tier 2 LLM-skill tests now block every PR, not just the nightly cron.**
v0.19.0 shipped four new CLI commands (`skillify`, `skillpack`, `routing-eval`, `skillify-check`) and got rave coverage. v0.24.0 is the production-hardening pass on top of that: every public contract that lied about itself, every silent footgun, every CI guard that wasn't wired up. No new features. No new commands. Just the unsexy fixes that turn a feature release into a production release.
The biggest save: the skillpack installer would have silently deleted your skills. The original v0.19 design's "rebuild managed block" path was load-bearing wrong — a user installing `alpha` then later running `gbrain skillpack install beta` alone would have lost `alpha`. Codex caught it during cross-model review. The fix preserves cumulative-install semantics via a receipt comment in the fence: `<!-- gbrain:skillpack:manifest cumulative-slugs="alpha,beta,..." -->`. Old fences upgrade silently. User-added rows survive with a stderr warning telling the operating agent to investigate. `install --all` is now the only path that prunes; per-skill install never destroys what it didn't install.
The biggest unsexy fix: `gbrain routing-eval --llm` was a documented feature that did nothing. README, CHANGELOG, and CLI help all said it ran an LLM tie-break layer. The code returned structural-only results with no warning, no error, no signal at all. v0.24.0 makes the flag honest across all four touchpoints. Until the LLM layer ships, `--llm` emits a stderr placeholder notice and runs structural. CI logs see it. Docs match the code. The release notes don't lie.
The CI fix nobody asked for: `scripts/check-privacy.sh` exists in the repo to enforce the OpenClaw fork-name ban from `CLAUDE.md:550`. It was never wired into anything. v0.24.0 prepends it to `package.json`'s `"test"` chain alongside the other `check-*.sh` guards. A regression test asserts the wiring stays. The first run caught 5 banned-name references that had been sitting in master's `CHANGELOG.md`, `src/cli.ts`, `src/commands/sync.ts`, and `skills/migrations/v0.19.0.md` for releases — fixed in the same wave.
### The numbers that matter
Counted against this branch's review trail and the local test suite:
| Metric | BEFORE v0.24.0 | AFTER v0.24.0 | Δ |
|---|---|---|---|
| `routing-eval --llm` behavior matches docs | no | yes | fixed |
| Public-contract drift surfaces fixed | 4 (README, CHANGELOG, CLI help, runtime) | 0 | 4 |
| `gbrain skillpack install <name>` preserves prior installs | yes (was a happy accident) | yes (with receipt + regression test) | locked |
| Regression test guarding cumulative-install semantics | none | `test 8a` ("install alpha; then install beta; assert both") | +1 |
| Banned-name leaks in tracked files | 5 (master state) | 0 | 5 |
| `check-privacy.sh` runs in CI | never | every PR (via `bun run test`) | wired |
| Tier 2 LLM-skill E2E gates each PR | no (nightly cron only) | yes | wired |
| Stale `v0.17/v0.18` version labels in new code | 7 sites across 5 files | 0 | 7 |
| Skillify scaffold idempotency under hand-edited resolver | backtick-only detection | backtick + quoted + bare | fixed |
Cross-model review trail: **CEO + Eng + Codex outside voice**. 14 user decisions captured, 0 unresolved, 1 critical Codex catch (the cumulative-install regression that would have shipped). Two-model review caught a one-model-blind spot. The receipt design is in `src/core/skillpack/installer.ts:applyManagedBlock`.
### What this means for builders
Nothing breaks. `gbrain upgrade` is the path. Existing brains: no schema migration. Existing AGENTS.md fences without a receipt comment auto-upgrade silently on the next `gbrain skillpack install` (one-time clean rebuild, no warnings). User-added skill rows inside the fence now survive reinstalls with a clear stderr breadcrumb: `[skillpack] unknown row in managed block: "<slug>" — Investigate: user-added skill, hand-edited fence, or typo?`
If you ship custom CI: `bun run test` now gates `check-privacy.sh` alongside the existing `check-jsonb-pattern.sh`, `check-progress-to-stdout.sh`, and `check-wasm-embedded.sh`. If you grepped through gbrain's source in your own CI, no surface change. If you previously ran `gbrain routing-eval --llm` expecting an LLM pass, you'll now see a stderr line telling you what's actually happening and your scripts keep working — exit code is still 0/1 based on structural results. Tier 2 (`test/e2e/skills.test.ts`) now runs on every PR using existing repo secrets. Adds ~3-5 min per PR for real protection against LLM-adjacent regressions.
## To take advantage of v0.24.0
`gbrain upgrade` does this automatically. To verify:
1. **Binary version:**
```bash
gbrain --version # should say 0.24.0
```
2. **`--llm` honesty:**
```bash
gbrain routing-eval --llm 2>&1 | grep -i placeholder
# expect: "[routing-eval] --llm flag is a placeholder in this release..."
```
3. **Skillpack receipt + cumulative semantics:**
```bash
gbrain skillpack install <name>
grep "gbrain:skillpack:manifest cumulative-slugs" $OPENCLAW_WORKSPACE/AGENTS.md
# expect a receipt line listing every gbrain-installed slug
```
4. **Privacy guard wired:**
```bash
grep "check-privacy.sh" package.json
# expect a hit in scripts.test
```
5. **If anything fails,** file an issue at https://github.com/garrytan/gbrain/issues with the output of `gbrain doctor` and which step broke.
No schema migration. Existing brains work unchanged.
### Itemized changes
#### Fixed
- **`gbrain routing-eval --llm`** is no longer a silent no-op. CLI emits a stderr placeholder notice; `--json` mode preserves clean stdout JSON with the warning on stderr only (no bleed). README:294, CHANGELOG entry, and CLI help text all rewritten to match the actual behavior. Tests in `test/routing-eval-cli.test.ts`.
- **Skillpack installer** now embeds a receipt comment (`<!-- gbrain:skillpack:manifest cumulative-slugs="..." version="..." -->`) inside the managed-block fence on every install. Per-skill installs accumulate via union(prior receipt, this-call slugs); `install --all` prunes slugs no longer in the bundle (the only prune path). Unknown rows inside the fence (user hand-adds, third-party bundles, typos) survive reinstalls with a stderr `Investigate:` breadcrumb. Pre-v0.24.0 fences upgrade silently on first install. Tests in `test/skillpack-install.test.ts` cover all four paths including the regression-guard "install alpha; install beta; assert both present."
- **`gbrain skillify scaffold --force`** no longer creates duplicate resolver rows when the existing row uses non-backticked path forms. The detection regex now matches backticked, single-quoted, double-quoted, and bare forms, with anchored boundaries to prevent false-matching shared-prefix slugs (e.g., `demo` vs `demo-extended`). Tests in `test/skillify-scaffold.test.ts`.
- **5 banned OpenClaw fork-name leaks** scrubbed from public artifacts (`CHANGELOG.md`, `skills/migrations/v0.19.0.md`, `src/cli.ts`, `src/commands/sync.ts`). All originated in earlier releases when the privacy script existed but wasn't wired to CI. Replacements per `CLAUDE.md:550` (origin-story → "Garry's OpenClaw"; reader-facing → "your OpenClaw").
- **Stale `v0.17`/`v0.18` version labels** removed from 5 files (`src/core/routing-eval.ts`, `src/core/filing-audit.ts`, `src/commands/check-resolvable.ts`, `src/commands/skillify.ts`, `src/commands/skillpack.ts`). Replaced with version-agnostic phrasing or current-release references.
#### Changed
- **`package.json` `"test"` script** now prepends `scripts/check-privacy.sh` to the existing chain. Test failure if the banned fork name appears anywhere in tracked files.
- **`.github/workflows/e2e.yml`** Tier 2 job (`test/e2e/skills.test.ts`, requires `OPENAI_API_KEY` + `ANTHROPIC_API_KEY`) promoted from schedule-only to required per-PR CI. Same secrets, same install path, same workflow YAML structure — just removed the `if: github.event_name == 'schedule' or workflow_dispatch` guard.
#### Added (tests)
- **`test/routing-eval-cli.test.ts`** (4 cases) — `--llm` placeholder behavior across human + JSON modes, exit-code preservation, regression guard for the silent-no-op state.
- **`test/privacy-script-wired.test.ts`** (3 cases) — asserts `check-privacy.sh` exists and is executable, asserts `package.json` `scripts.test` references it, asserts the `check:privacy` convenience alias is present.
- **`test/skillpack-install.test.ts`** (+4 cases) — cumulative-install regression guard, full-bundle prune semantics, unknown-row preserve+warn, pre-v0.24 upgrade path. Total 30 cases for the installer.
- **`test/skillify-scaffold.test.ts`** (+4 cases) — bare/quoted/single-quoted resolver rows + shared-prefix slug isolation. Total 18 cases for scaffold.
#### Deferred
- LLM tie-break layer for `routing-eval --llm` — placeholder ships in v0.24.0, full implementation is a future release. Code already accepts the flag.
- `gbrain skillpack forget <name>` — explicit uninstall command. v0.24.0 covers the minimum (managed-block prune via `install --all`). Tracked in `TODOS.md`.
- PID-liveness check in installer lock — current behavior (mtime-based stale detection + `--force-unlock` opt-in) is conservative; PID liveness is a v0.24.x ergonomic. Tracked.
### Cross-model review credit
This release's quality is directly attributable to running `/plan-ceo-review` + `/plan-eng-review` + `/codex review` in sequence on the v0.19.0 production-readiness audit. Codex caught one critical and three high findings the in-skill review missed: cumulative-install regression (load-bearing), `--llm` public-contract drift (4-surface scrub), Tier 2 framing as unowned dependency, and 6.5 hours of guesswork named files in the flake-diagnosis plan. The cross-model agreement on every fix is the signal that turns "ship the demo path" into "ship the production path."
## [0.23.2] - 2026-04-30
**The dream cycle now stamps every page it writes. The guard checks for the stamp. No content guessing, no false positives.**
@@ -764,7 +860,7 @@ If `gbrain sync` blocks with parse failures, the breakdown tells you what to fix
- DB-layer error patterns (`DB_DUPLICATE_KEY`, `STATEMENT_TIMEOUT`) check BEFORE YAML patterns in the classifier, so Postgres errors don't get YAML-labeled.
- Frontmatter regex patterns rewritten to match canonical messages from `collectValidationErrors()` (`File is empty...`, `No closing --- delimiter found`, `Frontmatter block is empty`) instead of aspirational code-token strings (`missing.*open`) that never appeared in practice.
Closes #500.
Closes #500. Eng-review plan: `~/.claude/plans/then-codex-synchronous-toucan.md` (codex outside-voice agreed on all 7 findings).
## [0.22.8] - 2026-04-28
@@ -914,6 +1010,8 @@ Then point Claude Desktop, claude.ai/code, or any MCP client at `http://your-tun
If anything breaks: `gbrain doctor`, `~/.gbrain/upgrade-errors.jsonl` (if present), and please file an issue at https://github.com/garrytan/gbrain/issues with both.
## [0.22.6.1] - 2026-04-26
**Old brains can upgrade again.**
@@ -1359,7 +1457,7 @@ Two SearchOpts additions plumb hard-exclude through the API: `exclude_slug_prefi
### The numbers that matter
A new BrainBench category — **Cat 13b: Source Swamp Resistance** — ships in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo. The corpus is 20 pages: 10 short opinionated `originals/` pages and 10 long `wintermute/chat/` dumps that mention the same multi-word phrases at higher per-byte density. 30 hand-curated queries assert the curated page wins.
A new BrainBench category — **Cat 13b: Source Swamp Resistance** — ships in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo. The corpus is 20 pages: 10 short opinionated `originals/` pages and 10 long `openclaw/chat/` dumps that mention the same multi-word phrases at higher per-byte density. 30 hand-curated queries assert the curated page wins.
| gbrain version | Top-1 hit | Top-3 hit | Swamp@top |
|--------------------------------------|-----------|-----------|-----------|
@@ -1373,17 +1471,17 @@ The world-v1 corpus (BrainBench Cats 1+2 retrieval, 145 relational queries) is u
### What this means for you
If your brain's biggest directories are chat dumps, daily logs, or X archives, search just got dramatically better for the topic queries you actually run. If you depend on chat surfacing for date-framed questions ("what did we discuss last week"), nothing changed ... the intent classifier routes those to `detail=high` which bypasses source-boost. If you want a different boost map, set `GBRAIN_SOURCE_BOOST=originals/:1.8,wintermute/chat/:0.3` and ship.
If your brain's biggest directories are chat dumps, daily logs, or X archives, search just got dramatically better for the topic queries you actually run. If you depend on chat surfacing for date-framed questions ("what did we discuss last week"), nothing changed ... the intent classifier routes those to `detail=high` which bypasses source-boost. If you want a different boost map, set `GBRAIN_SOURCE_BOOST=originals/:1.8,openclaw/chat/:0.3` and ship.
## To take advantage of v0.22.0
`gbrain upgrade` should do this automatically. No DB migration is needed ... the change is purely a SQL ranking refactor on existing tables.
1. **No manual migration step required.** The new ranking is on by default. Defaults are tuned for a brain with the canonical `originals/`, `concepts/`, `writing/`, `meetings/`, `daily/`, `media/x/`, `wintermute/chat/` shape.
1. **No manual migration step required.** The new ranking is on by default. Defaults are tuned for a brain with the canonical `originals/`, `concepts/`, `writing/`, `meetings/`, `daily/`, `media/x/`, `openclaw/chat/` shape.
2. **Tune for your brain (optional):**
```bash
# Stronger originals boost, harder chat dampening
export GBRAIN_SOURCE_BOOST="originals/:1.8,wintermute/chat/:0.3"
export GBRAIN_SOURCE_BOOST="originals/:1.8,openclaw/chat/:0.3"
# Add a directory to the hard-exclude list
export GBRAIN_SEARCH_EXCLUDE="scratch/,private/"
```
@@ -1404,7 +1502,7 @@ If your brain's biggest directories are chat dumps, daily logs, or X archives, s
#### Source-aware retrieval
- New module `src/core/search/source-boost.ts` ships the default boost map (`originals/` 1.5, `concepts/` 1.3, `writing/` 1.4, `people/companies/deals/` 1.2, `daily/` 0.8, `media/x/` 0.7, `wintermute/chat/` 0.5) and the four default hard-exclude prefixes (`test/`, `archive/`, `attachments/`, `.raw/`). Both knobs override via env (`GBRAIN_SOURCE_BOOST`, `GBRAIN_SEARCH_EXCLUDE`) or per-call SearchOpts.
- New module `src/core/search/source-boost.ts` ships the default boost map (`originals/` 1.5, `concepts/` 1.3, `writing/` 1.4, `people/companies/deals/` 1.2, `daily/` 0.8, `media/x/` 0.7, `openclaw/chat/` 0.5) and the four default hard-exclude prefixes (`test/`, `archive/`, `attachments/`, `.raw/`). Both knobs override via env (`GBRAIN_SOURCE_BOOST`, `GBRAIN_SEARCH_EXCLUDE`) or per-call SearchOpts.
- New module `src/core/search/sql-ranking.ts` is a pair of pure SQL-fragment builders shared between Postgres and PGLite engines. `buildSourceFactorCase` emits a longest-prefix-match CASE expression and returns literal `'1.0'` when `detail === 'high'` so temporal queries bypass source-boost. `buildHardExcludeClause` emits `NOT (col LIKE 'p1%' OR col LIKE 'p2%')` ... OR-chain wrapped in NOT, never `NOT LIKE ALL/ANY` (those don't express set-exclusion). LIKE meta-character escape covers `%`, `_`, AND `\` (backslash matters because it's Postgres LIKE's default escape char). Single-quote doubling renders SQL-injection-style inputs inert.
- `src/core/postgres-engine.ts` and `src/core/pglite-engine.ts` ... three methods wired: `searchKeyword` (chunk-grain CTE → DISTINCT ON page dedup, multiplies ts_rank by source-factor), `searchKeywordChunks` (the chunk-grain anchor primitive used by Cathedral II two-pass retrieval, also gets source-boost so the anchor pool is dampened on chat dirs), and `searchVector` (becomes a two-stage CTE: pure-distance HNSW inner ORDER BY, source-boost re-rank in outer SELECT, innerLimit scales with offset to preserve pagination).
- `src/core/types.ts` ... SearchOpts gains two fields: `exclude_slug_prefixes?: string[]` (additive over defaults + env) and `include_slug_prefixes?: string[]` (subtractive opt-back-in).
@@ -1585,7 +1683,7 @@ If you build with gbrain + OpenClaw + Claude Code: add your repo as a source (`g
### Itemized changes
**Layer 0 — Wintermute's baseline (cherry-picked, author scrubbed).** Tree-sitter code chunker for 6 languages (TS/TSX/JS/Python/Ruby/Go), `gbrain repos add/list/remove`, strategy-aware sync, `PageType 'code'`, `importCodeFile`, per-file sync progress via the v0.15.2 reporter. Preserved exactly, committed under Garry's author identity.
**Layer 0 — Garry's OpenClaw baseline (cherry-picked, author scrubbed).** Tree-sitter code chunker for 6 languages (TS/TSX/JS/Python/Ruby/Go), `gbrain repos add/list/remove`, strategy-aware sync, `PageType 'code'`, `importCodeFile`, per-file sync progress via the v0.15.2 reporter. Preserved exactly, committed under Garry's author identity.
**Layer 1 — A6 structured errors + version bump.** New `src/core/errors.ts` exports `StructuredAgentError` + `buildError` + `serializeError`. Matches the v0.17.0 `CycleReport.PhaseResult.error` shape so agent-consumable errors stay consistent across every gbrain surface. `globToRegex` bug fix: `src/**/*.ts` now matches `src/foo.ts` (zero intermediate dirs). `GBRAIN_HOME` env var for test isolation. `package.json``0.19.0`.
@@ -1593,7 +1691,7 @@ If you build with gbrain + OpenClaw + Claude Code: add your repo as a source (`g
**Layer 3 — schema migrations v25 + v26.** `pages.page_kind TEXT CHECK (page_kind IN ('markdown','code'))` on v25, using Postgres's `NOT VALID` + `VALIDATE CONSTRAINT` split so tables with millions of pages don't hold a write lock during the ALTER. `content_chunks` adds `language`, `symbol_name`, `symbol_type`, `start_line`, `end_line` on v26, plus partial indexes keyed on non-null values so code-chunk lookups stay cheap on mixed markdown+code brains.
**Layer 4 — delete Wintermute's multi-repo, wire v0.18.0 sources.** The `repos` abstraction in Wintermute's baseline turned out to be redundant with v0.18.0's `sources` subsystem (per-source `last_commit`, `federated` search config, RLS-friendly, DB-native). v0.19.0 keeps `gbrain repos` as a deprecated alias that routes into `runSources`. `sync --all` iterates the `sources` table instead of a local config array. Codex's P0 #2 (per-repo sync bookmarks) and P0 #3 (slug collision) both resolved by the existing schema.
**Layer 4 — delete the OpenClaw baseline's multi-repo, wire v0.18.0 sources.** The `repos` abstraction in Garry's OpenClaw baseline turned out to be redundant with v0.18.0's `sources` subsystem (per-source `last_commit`, `federated` search config, RLS-friendly, DB-native). v0.19.0 keeps `gbrain repos` as a deprecated alias that routes into `runSources`. `sync --all` iterates the `sources` table instead of a local config array. Codex's P0 #2 (per-repo sync bookmarks) and P0 #3 (slug collision) both resolved by the existing schema.
**Layer 5 — Chonkie chunker parity (E2a).** 6 languages → 29. Embedded asset paths for every grammar in `tree-sitter-wasms`. Accurate tokenizer via `@dqbd/tiktoken` `cl100k_base` (lazy-init). Small-sibling merging with the Chonkie `bisect_left` pattern tuned to 15% of chunk target, so tiny siblings (imports, single-line consts) collapse while substantive classes/functions stay independent. `CHUNKER_VERSION=3` folded into `importCodeFile`'s `content_hash` so chunker-shape changes across releases force clean re-chunks without `sync --force`.
@@ -2040,7 +2138,7 @@ No schema migration. Existing brains work unchanged.
- **`gbrain skillpack list`** — prints the curated bundle (25 skills) shipped with gbrain.
- **`gbrain skillpack install <name>` / `--all`** — copies bundled skills into the target workspace. Automatically pulls shared convention files so nothing references a missing dep. Per-file diff protection, `--overwrite-local` escape hatch, `.gbrain-skillpack.lock` against concurrent installers, atomic managed-block update to AGENTS.md / RESOLVER.md.
- **`gbrain skillpack diff <name>`** — per-file diff preview before install.
- **`gbrain routing-eval`** — dedicated CI verb that runs routing fixtures (`skills/<name>/routing-eval.jsonl`) and surfaces intent-to-skill mismatches, ambiguous routing, and false positives. Default structural layer runs alongside `check-resolvable`; `--llm` opts into an LLM tie-break layer.
- **`gbrain routing-eval`** — dedicated CI verb that runs routing fixtures (`skills/<name>/routing-eval.jsonl`) and surfaces intent-to-skill mismatches, ambiguous routing, and false positives. Ships the structural layer (same logic `check-resolvable` runs). The `--llm` flag is accepted as a placeholder for a future LLM tie-break layer; in this release it emits a stderr notice and runs structural only.
- **`gbrain check-resolvable --strict`** — opt-in CI mode that promotes warnings to failures.
- **`skills/_brain-filing-rules.json`** — machine-readable canonical filing rules (JSON sidecar to the prose `_brain-filing-rules.md`).
- **`writes_pages: true` + `writes_to: [...]`** — new skill frontmatter fields consumed by the filing audit. Distinct from `mutating:` so cron schedulers and report writers aren't dragged into filing checks.
+2 -2
View File
@@ -57,9 +57,9 @@ strict behavior when unset.
- `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.
- `src/commands/skillify.ts` + `src/core/skillify/{generator,templates}.ts` (v0.19) — `gbrain skillify scaffold <name>` creates all stubs for a new skill in one command: SKILL.md, script, tests, routing-eval.jsonl, resolver entry, filing-rules pointer. `gbrain skillify check <script>` runs the 10-step checklist (LLM evals, routing evals, check-resolvable gate, filing audit) against a candidate skill before it lands.
- `src/commands/skillify-check.ts` (v0.19) — `gbrain skillpack-check` agent-readable health report. Exit 0/1/2 for CI pipeline gating; JSON for debugging. Wraps `check-resolvable --json`, `doctor --json`, and migration ledger into one payload so agents can decide whether a human action is required.
- `src/commands/skillpack.ts` + `src/core/skillpack/{bundle,installer}.ts` (v0.19) — `gbrain skillpack install` drops gbrain's curated 25-skill bundle into a host workspace, managed-block style. Never clobbers local edits; tracks a skill manifest so subsequent `install --update` diffs cleanly. Bundle builder (`skillpack/bundle.ts`) packages the set from `skills/` into a versioned payload.
- `src/commands/skillpack.ts` + `src/core/skillpack/{bundle,installer}.ts` (v0.19) — `gbrain skillpack install` drops gbrain's curated 25-skill bundle into a host workspace, managed-block style. Never clobbers local edits; tracks a skill manifest so subsequent `install --update` diffs cleanly. Bundle builder (`skillpack/bundle.ts`) packages the set from `skills/` into a versioned payload. **v0.24.0:** managed block embeds a `<!-- gbrain:skillpack:manifest cumulative-slugs="..." version="..." -->` receipt inside the fence. Per-skill installs accumulate via `union(prior_receipt, this_call)`; `install --all` is the only path that prunes (drops slugs no longer in the bundle). Rows inside the fence whose slug is in neither the new cumulative set nor the bundle survive as user-added with a stderr `[skillpack] unknown row in managed block: "<slug>" — Investigate: ...` warning. Pre-v0.24 fences upgrade silently on first install (extracted slugs become the prior cumulative set).
- `src/core/skill-manifest.ts` (v0.19) — parser for `skill-manifest.json` records. Used by skillpack installer to detect drift between the shipped bundle and the user's local edits, so updates merge instead of overwriting.
- `src/commands/routing-eval.ts` + `src/core/routing-eval.ts` (v0.19) — `gbrain routing-eval` catches user phrasings that route to the wrong skill. Reads `skills/<name>/routing-eval.jsonl` fixtures (`{intent, expected_skill, ambiguous_with?}`). Structural layer runs in `check-resolvable` by default (zero API cost); `--llm` opts into a Haiku tie-break layer for CI. False positives surface before users hit them.
- `src/commands/routing-eval.ts` + `src/core/routing-eval.ts` (v0.19) — `gbrain routing-eval` catches user phrasings that route to the wrong skill. Reads `skills/<name>/routing-eval.jsonl` fixtures (`{intent, expected_skill, ambiguous_with?}`). Structural layer runs in `check-resolvable` by default (zero API cost). The `--llm` flag is accepted as a placeholder for a future LLM tie-break layer; in v0.24.0 it emits a stderr notice and runs structural only. False positives surface before users hit them.
- `src/core/filing-audit.ts` + `skills/_brain-filing-rules.json` (v0.19) — Check 6 of `check-resolvable`. Parses new `writes_pages:` / `writes_to:` frontmatter on skills and audits their filing claims against the filing-rules JSON. Warning-only in v0.19, upgrades to error in v0.20.
- `src/core/dry-fix.ts``gbrain doctor --fix` engine. `autoFixDryViolations(fixes, {dryRun})` rewrites inlined rules to `> **Convention:** see [path](path).` callouts via three shape-aware expanders (bullet / blockquote / paragraph). Five guards: working-tree-dirty (`getWorkingTreeStatus()` returns 3-state `'clean' | 'dirty' | 'not_a_repo'`), no-git-backup, inside-code-fence, already-delegated (40-line proximity, consistent with detector), ambiguous-multi-match, block-is-callout. `execFileSync` array args (no shell — no injection surface). EOF newline preserved.
- `src/core/backoff.ts` — Adaptive load-aware throttling: CPU/memory checks, exponential backoff, active hours multiplier
+11
View File
@@ -52,6 +52,10 @@ docs/ Architecture docs
## Running tests
```bash
# Recommended: full CI guard chain + tests (matches what CI runs)
bun run test # privacy + jsonb + progress + wasm + typecheck + bun test
# Just the test runner (skips CI guards)
bun test # all tests (unit + E2E skipped without DB)
bun test test/markdown.test.ts # specific unit test
@@ -63,6 +67,13 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5434/gbrain_test bun run t
DATABASE_URL=postgresql://... bun run test:e2e
```
Use `bun run test` before pushing. The guard chain catches: banned fork-name leaks
(`scripts/check-privacy.sh`), `JSON.stringify(x)::jsonb` interpolation patterns
(`scripts/check-jsonb-pattern.sh`), `\r` progress bleed to stdout
(`scripts/check-progress-to-stdout.sh`), trailing-newline drift across tracked
files (`scripts/check-trailing-newline.sh`), and silent fallback to recursive
chunking in the compiled binary (`scripts/check-wasm-embedded.sh`).
### Local CI gate (recommended before pushing, v0.23.1+)
```bash
+9 -3
View File
@@ -316,9 +316,11 @@ is what you spend time on. Everything else is boilerplate the CLI writes for you
Drop a `routing-eval.jsonl` fixture next to any skill. Each line is `{intent, expected_skill,
ambiguous_with?}`. `gbrain check-resolvable` runs the structural layer by default; `gbrain
routing-eval --llm` runs an LLM tie-break layer for CI. False positives (wrong skill matched),
missed routes (no skill matched), and tautological fixtures (intent copies trigger verbatim)
all surface as specific advisories with the exact file:line to fix.
routing-eval` runs the same structural layer as a dedicated CI verb. The `--llm` flag is
accepted as a placeholder for a future LLM tie-break layer; in this release it emits a stderr
notice and runs structural only. False positives (wrong skill matched), missed routes (no
skill matched), and tautological fixtures (intent copies trigger verbatim) all surface as
specific advisories with the exact file:line to fix.
### Works on your OpenClaw, not just gbrain's repo
@@ -355,6 +357,10 @@ gbrain skillpack diff brain-ops # compare bundle vs your local co
Re-running is safe. The managed-block markers in your AGENTS.md let `skillpack install`
accumulate rows across separate single-skill installs instead of overwriting each other.
A receipt comment inside the fence (`<!-- gbrain:skillpack:manifest cumulative-slugs="..." -->`)
tracks what gbrain has installed across runs. `install --all` is the only path that prunes;
per-skill install never deletes what it didn't install. If you hand-add a row inside the fence,
gbrain preserves it on reinstall and emits a stderr notice telling your agent to investigate.
**Skillify is the piece that makes the skills tree survive six months of compounding work.**
Read [`skills/skillify/SKILL.md`](skills/skillify/SKILL.md) for the full 10-item checklist
+12
View File
@@ -543,6 +543,18 @@ keeping both skills' triggers intact for chaining.
**Depends on / blocked by:** Nothing — UNION-on-read path keeps unresolved edges surfaced even without this.
## P3 — Dev experience: test suite parallelism on fast multi-core machines
**Context:** `bun test` on M-series Macs spawns ~1 worker per core. `test/dream.test.ts` (5 describe blocks, 11 tests) and `test/orphans.test.ts` create a fresh PGLite engine in `beforeEach` that runs ~20 schema migrations per test. Under parallel load, WASM-instance contention causes ~18 `beforeEach` timeouts at 59s.
**Evidence:** CI (ubuntu-latest, fewer cores) is green on every PR. Running the suspect files in isolation (`bun test test/dream.test.ts test/orphans.test.ts`) is also green. Reproduces only on fast multi-core local machines running the full 136-file parallel suite.
**Fix:** move engine creation from `beforeEach` to `beforeAll` per describe block; add a data-reset helper (delete-all-rows-in-relevant-tables) between tests. ~80 LOC change across two test files.
**Priority:** P3 because production CI is unaffected. Hits local dev iteration speed on fast Macs.
**Found:** 2026-04-24 during v0.19.0 production-readiness review.
## Completed
### ~~Checks 5 + 6 for check-resolvable~~
+1 -1
View File
@@ -1 +1 @@
0.23.2
0.24.0
+1 -1
View File
@@ -73,7 +73,7 @@ hook resumes blocking malformed pages.
## For downstream agent forks
If your fork (Wintermute, Hermes, OpenClaw) wraps gbrain in a host repo
If your OpenClaw wraps gbrain in a host repo
that's not the brain repo itself, you may want a separate hook strategy:
- **Brain repo IS the host repo** (gbrain skills + brain pages in one repo):
+11 -5
View File
@@ -143,9 +143,9 @@ strict behavior when unset.
- `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.
- `src/commands/skillify.ts` + `src/core/skillify/{generator,templates}.ts` (v0.19) — `gbrain skillify scaffold <name>` creates all stubs for a new skill in one command: SKILL.md, script, tests, routing-eval.jsonl, resolver entry, filing-rules pointer. `gbrain skillify check <script>` runs the 10-step checklist (LLM evals, routing evals, check-resolvable gate, filing audit) against a candidate skill before it lands.
- `src/commands/skillify-check.ts` (v0.19) — `gbrain skillpack-check` agent-readable health report. Exit 0/1/2 for CI pipeline gating; JSON for debugging. Wraps `check-resolvable --json`, `doctor --json`, and migration ledger into one payload so agents can decide whether a human action is required.
- `src/commands/skillpack.ts` + `src/core/skillpack/{bundle,installer}.ts` (v0.19) — `gbrain skillpack install` drops gbrain's curated 25-skill bundle into a host workspace, managed-block style. Never clobbers local edits; tracks a skill manifest so subsequent `install --update` diffs cleanly. Bundle builder (`skillpack/bundle.ts`) packages the set from `skills/` into a versioned payload.
- `src/commands/skillpack.ts` + `src/core/skillpack/{bundle,installer}.ts` (v0.19) — `gbrain skillpack install` drops gbrain's curated 25-skill bundle into a host workspace, managed-block style. Never clobbers local edits; tracks a skill manifest so subsequent `install --update` diffs cleanly. Bundle builder (`skillpack/bundle.ts`) packages the set from `skills/` into a versioned payload. **v0.24.0:** managed block embeds a `<!-- gbrain:skillpack:manifest cumulative-slugs="..." version="..." -->` receipt inside the fence. Per-skill installs accumulate via `union(prior_receipt, this_call)`; `install --all` is the only path that prunes (drops slugs no longer in the bundle). Rows inside the fence whose slug is in neither the new cumulative set nor the bundle survive as user-added with a stderr `[skillpack] unknown row in managed block: "<slug>" — Investigate: ...` warning. Pre-v0.24 fences upgrade silently on first install (extracted slugs become the prior cumulative set).
- `src/core/skill-manifest.ts` (v0.19) — parser for `skill-manifest.json` records. Used by skillpack installer to detect drift between the shipped bundle and the user's local edits, so updates merge instead of overwriting.
- `src/commands/routing-eval.ts` + `src/core/routing-eval.ts` (v0.19) — `gbrain routing-eval` catches user phrasings that route to the wrong skill. Reads `skills/<name>/routing-eval.jsonl` fixtures (`{intent, expected_skill, ambiguous_with?}`). Structural layer runs in `check-resolvable` by default (zero API cost); `--llm` opts into a Haiku tie-break layer for CI. False positives surface before users hit them.
- `src/commands/routing-eval.ts` + `src/core/routing-eval.ts` (v0.19) — `gbrain routing-eval` catches user phrasings that route to the wrong skill. Reads `skills/<name>/routing-eval.jsonl` fixtures (`{intent, expected_skill, ambiguous_with?}`). Structural layer runs in `check-resolvable` by default (zero API cost). The `--llm` flag is accepted as a placeholder for a future LLM tie-break layer; in v0.24.0 it emits a stderr notice and runs structural only. False positives surface before users hit them.
- `src/core/filing-audit.ts` + `skills/_brain-filing-rules.json` (v0.19) — Check 6 of `check-resolvable`. Parses new `writes_pages:` / `writes_to:` frontmatter on skills and audits their filing claims against the filing-rules JSON. Warning-only in v0.19, upgrades to error in v0.20.
- `src/core/dry-fix.ts` — `gbrain doctor --fix` engine. `autoFixDryViolations(fixes, {dryRun})` rewrites inlined rules to `> **Convention:** see [path](path).` callouts via three shape-aware expanders (bullet / blockquote / paragraph). Five guards: working-tree-dirty (`getWorkingTreeStatus()` returns 3-state `'clean' | 'dirty' | 'not_a_repo'`), no-git-backup, inside-code-fence, already-delegated (40-line proximity, consistent with detector), ambiguous-multi-match, block-is-callout. `execFileSync` array args (no shell — no injection surface). EOF newline preserved.
- `src/core/backoff.ts` — Adaptive load-aware throttling: CPU/memory checks, exponential backoff, active hours multiplier
@@ -1670,9 +1670,11 @@ is what you spend time on. Everything else is boilerplate the CLI writes for you
Drop a `routing-eval.jsonl` fixture next to any skill. Each line is `{intent, expected_skill,
ambiguous_with?}`. `gbrain check-resolvable` runs the structural layer by default; `gbrain
routing-eval --llm` runs an LLM tie-break layer for CI. False positives (wrong skill matched),
missed routes (no skill matched), and tautological fixtures (intent copies trigger verbatim)
all surface as specific advisories with the exact file:line to fix.
routing-eval` runs the same structural layer as a dedicated CI verb. The `--llm` flag is
accepted as a placeholder for a future LLM tie-break layer; in this release it emits a stderr
notice and runs structural only. False positives (wrong skill matched), missed routes (no
skill matched), and tautological fixtures (intent copies trigger verbatim) all surface as
specific advisories with the exact file:line to fix.
### Works on your OpenClaw, not just gbrain's repo
@@ -1709,6 +1711,10 @@ gbrain skillpack diff brain-ops # compare bundle vs your local co
Re-running is safe. The managed-block markers in your AGENTS.md let `skillpack install`
accumulate rows across separate single-skill installs instead of overwriting each other.
A receipt comment inside the fence (`<!-- gbrain:skillpack:manifest cumulative-slugs="..." -->`)
tracks what gbrain has installed across runs. `install --all` is the only path that prunes;
per-skill install never deletes what it didn't install. If you hand-add a row inside the fence,
gbrain preserves it on reinstall and emits a stderr notice telling your agent to investigate.
**Skillify is the piece that makes the skills tree survive six months of compounding work.**
Read [`skills/skillify/SKILL.md`](skills/skillify/SKILL.md) for the full 10-item checklist
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "gbrain",
"version": "0.23.2",
"version": "0.24.0",
"description": "Postgres-native personal knowledge brain with hybrid RAG search",
"type": "module",
"main": "src/core/index.ts",
@@ -33,7 +33,7 @@
"build:schema": "bash scripts/build-schema.sh",
"build:llms": "bun run scripts/build-llms.ts",
"build:pglite-snapshot": "bun run scripts/build-pglite-snapshot.ts",
"test": "scripts/check-jsonb-pattern.sh && scripts/check-progress-to-stdout.sh && scripts/check-trailing-newline.sh && scripts/check-wasm-embedded.sh && bun run typecheck && bun test --timeout=60000",
"test": "scripts/check-privacy.sh && scripts/check-jsonb-pattern.sh && scripts/check-progress-to-stdout.sh && scripts/check-trailing-newline.sh && scripts/check-wasm-embedded.sh && bun run typecheck && bun test --timeout=60000",
"check:wasm": "scripts/check-wasm-embedded.sh",
"check:newlines": "scripts/check-trailing-newline.sh",
"test:e2e": "bash scripts/run-e2e.sh",
@@ -44,6 +44,7 @@
"ci:select-e2e": "bun run scripts/select-e2e.ts",
"typecheck": "tsc --noEmit",
"check:jsonb": "scripts/check-jsonb-pattern.sh",
"check:privacy": "scripts/check-privacy.sh",
"check:progress": "scripts/check-progress-to-stdout.sh",
"postinstall": "command -v gbrain >/dev/null 2>&1 && gbrain apply-migrations --yes --non-interactive || echo '[gbrain] postinstall skipped. If installed via bun install -g github:...: run `gbrain doctor` and `gbrain apply-migrations --yes` manually. See https://github.com/garrytan/gbrain/issues/218' 1>&2",
"prepublish:clawhub": "bun run build:all",
+68 -14
View File
@@ -1,21 +1,75 @@
# Brain-First Lookup Convention
Before using ANY external API (web search, enrichment services, social APIs) to
research a person, company, or topic, check the brain first.
**Read this before doing ANY entity/person/company/fact lookup.**
## The 5-Step Lookup
Sub-agents and fresh sessions inherit gbrain tools but not the knowledge of
when and how to use them. This file is that knowledge.
1. `gbrain search "name"` — keyword search for existing pages
2. `gbrain query "natural question about name"` — hybrid search for related context
3. `gbrain get <slug>` — if you know the slug, read the full page
4. Check backlinks: `gbrain get_backlinks <slug>` — who references this entity?
5. Check timeline: `gbrain get_timeline <slug>` — recent events involving this entity
## Available GBrain Tools
The brain almost always has something. External APIs fill gaps, not start from scratch.
Your tool inventory includes these (prefixed `gbrain__` in OpenClaw):
## Why This Matters
| Tool | Use for |
|------|---------|
| `gbrain__search` / `search` | Keyword search — fast, always works |
| `gbrain__query` / `query` | Hybrid search (keyword + semantic) — best quality |
| `gbrain__get_page` / `get_page` | Direct page read when you know the slug |
| `gbrain__get_links` / `get_links` | Outgoing links from a page |
| `gbrain__get_backlinks` / `get_backlinks` | Who references this entity |
| `gbrain__get_timeline` / `get_timeline` | Dated events for an entity |
| `gbrain__resolve_slugs` / `resolve_slugs` | Fuzzy slug resolution |
| `gbrain__traverse_graph` / `traverse_graph` | Walk the relationship graph |
| `gbrain__put_page` / `put_page` | Create or update a brain page |
| `gbrain__add_timeline_entry` | Add a dated event |
| `gbrain__add_link` | Add a relationship edge |
- The brain has context that external APIs don't (user's direct observations, meeting notes, personal relationships)
- External API calls cost money and time
- Brain context makes external lookups more targeted (you know what's missing)
- The user's direct statements are highest-authority data. External sources are lowest.
Tool names vary by transport (MCP uses short names, OpenClaw plugin uses
`gbrain__` prefix). Both work. Use whichever your environment provides.
## The Lookup Chain (MANDATORY ORDER)
1. **`search`** first — keyword search, fast, zero API cost
2. **`query`** if search is thin — hybrid semantic search, uses embedding API
3. **`get_page`** if you found a slug — read the full compiled truth
4. **External APIs only after steps 1-2 return nothing useful**
Never skip to external APIs without completing steps 1-2. The brain has
thousands of pages. The answer is almost always there.
## Rules
- **Score > 0.5 = use it.** Don't reach for external APIs when the brain answered.
- **User's direct statements are highest-authority data.** The brain captures
what the user said in meetings, conversations, and notes. External sources
are supplementary.
- **After any brain page write:** trigger a sync so new pages are searchable.
In OpenClaw: `gbrain__sync_brain`. From CLI: `gbrain sync --no-pull`.
- **Every brain page reference in output** should use a clickable link format
appropriate to the deployment (GitHub URL, local path, or slug).
- **Never use `memory_search` for entity lookups.** Memory tools search
session notes (MEMORY.md), not the brain knowledge graph. Use
`search` or `query` for entity lookups.
## Entity Page Conventions
Standard directory structure:
| Directory | Type | Example |
|-----------|------|---------|
| `people/` | person | `people/paul-graham.md` |
| `companies/` | company | `companies/stripe.md` |
| `deals/` | deal | `deals/stripe-series-c.md` |
| `meetings/` | meeting | `meetings/2026-04-23-weekly-sync.md` |
| `projects/` | project | `projects/gbrain.md` |
| `yc/` | yc | `yc/batch-w26.md` |
When creating new pages, include proper frontmatter with `type`, `title`,
and `tags` fields.
## When Spawning Further Sub-agents
If you spawn your own sub-agents, include this line in their task prompt:
> Read `skills/conventions/brain-first.md` before starting work.
This ensures the convention propagates through any depth of sub-agent chain.
+1 -1
View File
@@ -46,7 +46,7 @@ These run as part of `gbrain upgrade` → `gbrain apply-migrations`. No manual D
5. **Observe incremental chunking.** Edit one function in a 20-function file, re-run `sync --source <id>`. Embedding cost should be ~5% of the first sync because unchanged chunks reuse their existing embeddings.
## Migration from Wintermute's `repos` (if you used it)
## Migration from your OpenClaw's `repos` (if you used it)
v0.19.0 deletes `~/.gbrain/config.json`'s `repos` array in favor of the `sources` table. The CLI surface is preserved as a deprecated alias: `gbrain repos add` still works, but routes into `runSources` with a one-line deprecation notice on stderr. Existing scripts keep working; prefer `gbrain sources` going forward.
+1 -1
View File
@@ -575,7 +575,7 @@ async function handleCliOnly(command: string, args: string[]) {
}
case 'repos': {
// v0.19.0: `gbrain repos ...` is an alias into the v0.18.0 sources
// subsystem. The repos abstraction (Wintermute's baseline) was
// subsystem. The repos abstraction (Garry's OpenClaw baseline) was
// redundant with sources and carried per-user config state that
// couldn't participate in federation / RLS / multi-tenancy. We
// keep the alias so scripts like `gbrain repos add .` keep
+9 -9
View File
@@ -57,11 +57,11 @@ export interface Flags {
skillsDir: string | null;
}
// Check 5 (trigger_routing_eval) landed in v0.17 (W2). Check 6
// (brain_filing) landed in v0.17 (W3). Array is now empty; the
// export stays as a stable public field of the --json envelope so
// downstream consumers that check `.deferred[]` keep working.
// Future deferred checks get appended here.
// Check 5 (trigger_routing_eval) and Check 6 (brain_filing) both
// shipped as real implementations in v0.19 (W2 + W3). Array is now
// empty; the export stays as a stable public field of the --json
// envelope so downstream consumers that check `.deferred[]` keep
// working. Future deferred checks get appended here.
export const DEFERRED: DeferredCheck[] = [];
const HELP_TEXT = `gbrain check-resolvable [options]
@@ -83,13 +83,13 @@ Exit codes:
0 clean (no errors; no warnings unless --strict)
1 errors present, OR (with --strict) warnings present
Check 5 (trigger routing eval) lands in v0.17 via W2: any
Check 5 (trigger routing eval) runs via W2: any
skills/<name>/routing-eval.jsonl fixtures are evaluated and routing
gaps surface as warnings.
Check 6 (brain filing) lands in v0.17 via W3: skills with
writes_pages: true are audited against skills/_brain-filing-rules.json.
No checks are deferred as of v0.17.
Check 6 (brain filing) runs via W3: skills with writes_pages: true
are audited against skills/_brain-filing-rules.json. No checks are
currently deferred.
`;
// ---------------------------------------------------------------------------
+20 -9
View File
@@ -1,5 +1,5 @@
/**
* gbrain routing-eval — Standalone CLI verb for Check 5 (W2, v0.17).
* gbrain routing-eval — Standalone CLI verb for Check 5 (W2).
*
* Runs the structural routing eval against every `routing-eval.jsonl`
* fixture in the skills tree. Exits:
@@ -8,10 +8,10 @@
* 1 any failure
* 2 fixtures directory not found / resolver missing (setup error)
*
* Layer B (LLM tie-break) via `--llm` is reserved: the flag parses and
* surfaces in the envelope, but the harness does not yet call any model.
* The plan ships structural layer only in v0.17. The LLM layer has
* explicit sequencing in v0.18 once the structural baseline is stable.
* Layer B (LLM tie-break) via `--llm` is a placeholder: the flag parses
* and surfaces in the envelope, but the harness does not yet call any
* model. Passing `--llm` emits a stderr notice and runs the structural
* layer only. A future release will implement the tie-break layer.
*/
import { readFileSync, existsSync } from 'fs';
@@ -55,7 +55,9 @@ false-positive counts. Lints fixtures for verbatim trigger copies.
Options:
--json Machine-readable JSON envelope
--llm (reserved for v0.18) Run Layer B LLM tie-break
--llm Placeholder for Layer B LLM tie-break. Not yet
implemented. Accepted for forward-compat; emits a
stderr notice and runs the structural layer only.
--skills-dir PATH Override the auto-detected skills/ directory
--help Show this message
@@ -111,6 +113,15 @@ export async function runRoutingEvalCli(args: string[]): Promise<void> {
process.exit(0);
}
// --llm is a placeholder in this release. Emit a stderr notice so
// users (and CI logs) can see the structural-only fallback clearly,
// regardless of --json mode. Does not affect exit code or stdout.
if (flags.llm) {
console.error(
'[routing-eval] --llm flag is a placeholder in this release. Running structural layer only; a future release will implement LLM tie-break.',
);
}
const { dir, error, message } = resolveSkillsDir(flags);
if (error === 'no_skills_dir') {
const env: RoutingEvalEnvelope = {
@@ -200,9 +211,9 @@ export async function runRoutingEvalCli(args: string[]): Promise<void> {
for (const m of loaded.malformed) {
console.log(` [malformed] ${m.file}:${m.line}${m.error}`);
}
if (flags.llm) {
console.log('\nNote: --llm (Layer B LLM tie-break) is reserved for v0.18. No model calls made.');
}
// The stderr notice emitted at the top of runRoutingEvalCli
// already informed the user that --llm is a placeholder; do not
// repeat it here. Stdout in human mode stays results-only.
}
process.exit(ok ? 0 : 1);
+4 -3
View File
@@ -1,5 +1,5 @@
/**
* gbrain skillify <scaffold|check> — v0.17 W4 CLI namespace.
* gbrain skillify <scaffold|check> — W4 CLI namespace.
*
* `scaffold`: creates 5 stub files for a new skill. Mechanical only.
* `check`: 10-item audit of an existing skill. Promoted from
@@ -299,8 +299,9 @@ export async function runSkillifyScaffold(args: string[]): Promise<void> {
// ---------------------------------------------------------------------------
// `gbrain skillify check` — delegates to scripts/skillify-check.ts via same
// internal helpers. For v0.17 we shell out to the script (kept as single
// source of truth); v0.18 may inline it further.
// internal helpers. Current design shells out to the script (kept as the
// single source of truth for the check logic); a future release may inline
// it further.
// ---------------------------------------------------------------------------
async function runSkillifyCheck(args: string[]): Promise<void> {
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* gbrain skillpack <list|install|diff|check> — v0.17 W5 CLI namespace.
* gbrain skillpack <list|install|diff|check> — W5 CLI namespace.
*
* D-CX-2 pattern: unified subcommand namespace. The pre-existing
* `skillpack-check` command keeps its top-level name for backwards
+1 -1
View File
@@ -948,7 +948,7 @@ export async function runSync(engine: BrainEngine, args: string[]) {
// local_path. Sources are the canonical v0.18.0 abstraction: per-source
// last_commit, last_sync_at, config.federated flags. Per-source
// bookmarks live in the sources table (not ~/.gbrain/config.json),
// which is why this path replaced Wintermute's `multi-repo.ts` shim.
// which is why this path replaced Garry's OpenClaw `multi-repo.ts` shim.
//
// Only sources with a non-null local_path participate. A GitHub-only
// source (no checkout) has nothing for `sync` to pull. Sources with
+8 -8
View File
@@ -1,5 +1,5 @@
/**
* filing-audit.ts Check 6 of the skillify checklist (W3, v0.17).
* filing-audit.ts Check 6 of the skillify checklist (W3).
*
* For every skill that writes brain pages (`writes_pages: true`),
* verify that:
@@ -8,18 +8,18 @@
* `skills/_brain-filing-rules.json`. `sources/` is explicitly
* allowed (bulk data capture is a legitimate filing target).
*
* Important distinction (D-CX-7): `writes_pages: true` is distinct
* from the pre-existing `mutating: true` field. `mutating:true` means
* "has side effects" (any side effect cron, config, report write).
* Important distinction: `writes_pages: true` is distinct from the
* pre-existing `mutating: true` field. `mutating:true` means "has
* side effects" (any side effect cron, config, report write).
* `writes_pages:true` means "writes brain pages to a semantic
* directory." Cron/config/report-writer skills set `mutating:true`
* but NOT `writes_pages:true`, and so are correctly exempted from
* filing-audit noise.
*
* v0.17 scope: declaration-level audit only (cheap, deterministic).
* v0.18 plan: `filing-audit --pages` walks brain pages and infers
* primary subject via LLM to catch real misfilings vs declarations
* (D-CX-13).
* Current scope: declaration-level audit only (cheap, deterministic).
* A future release may add `filing-audit --pages` to walk brain pages
* and infer primary subject via LLM (catches real misfilings vs
* declarations); that is tracked as follow-up work, not in this scope.
*/
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
+4 -3
View File
@@ -15,8 +15,9 @@
* of skills this intent is allowed to also match).
*
* Layer B (LLM tie-break, optional): only runs via `gbrain routing-eval
* --llm`. Not yet implemented in v0.17 core; the CLI stubs the flag
* so call sites are ready.
* --llm`. Not yet implemented in this release; the CLI accepts the
* flag (emits a stderr notice and runs Layer A only) so call sites
* are ready. A future release will wire up the tie-break layer.
*
* Fixture linter (D-CX-6): we reject fixtures where the normalized
* `intent` is a verbatim substring of any trigger phrase attached to
@@ -316,7 +317,7 @@ export function loadRoutingFixtures(skillsDir: string): LoadResult {
// ---------------------------------------------------------------------------
export interface RunRoutingEvalOptions {
/** Reserved for Layer B (LLM tie-break). Not implemented in v0.17. */
/** Reserved for Layer B (LLM tie-break). Not implemented in this release. */
llm?: boolean;
}
+4 -4
View File
@@ -3,13 +3,13 @@
*
* Multiplies into ts_rank / vector cosine score at SQL build time so that
* curated content (originals/, concepts/, writing/) outranks bulk content
* (wintermute/chat/, daily/, media/x/) for non-temporal queries.
* (openclaw/chat/, daily/, media/x/) for non-temporal queries.
*
* Keyed by slug prefix. Longest-prefix-match wins (sorted at lookup time
* inside sql-ranking.ts). Defaults grounded in the composition of the
* canonical brain at ~/git/brain/.
*
* Override via env: GBRAIN_SOURCE_BOOST="originals/:1.8,wintermute/chat/:0.3"
* Override via env: GBRAIN_SOURCE_BOOST="originals/:1.8,openclaw/chat/:0.3"
* Hard-exclude via env: GBRAIN_SEARCH_EXCLUDE="test/,scratch/"
*/
@@ -36,7 +36,7 @@ export const DEFAULT_SOURCE_BOOSTS: Record<string, number> = {
'daily/': 0.8,
'media/x/': 0.7,
// Chat transcripts — massive, noisy, swamp keyword queries
'wintermute/chat/': 0.5,
'openclaw/chat/': 0.5,
};
/**
@@ -53,7 +53,7 @@ export const DEFAULT_HARD_EXCLUDES: string[] = [
/**
* Parse GBRAIN_SOURCE_BOOST env var.
* Format: comma-separated prefix:factor pairs.
* Example: "originals/:1.8,wintermute/chat/:0.3"
* Example: "originals/:1.8,openclaw/chat/:0.3"
*
* Malformed entries are skipped silently. Returns empty object if env is
* unset or unparseable in its entirety.
+19 -4
View File
@@ -116,9 +116,16 @@ export function planScaffold(opts: ScaffoldOptions): ScaffoldPlan {
}
/**
* Check whether the resolver already has a backtick-wrapped reference
* to `skills/<name>/SKILL.md`. Idempotency contract (D-CX-7) if
* present, we never re-append a row for this skill, even with --force.
* Check whether the resolver already references `skills/<name>/SKILL.md`
* in ANY form: backticked (`skills/foo/SKILL.md`), single-quoted
* ('skills/foo/SKILL.md'), double-quoted ("skills/foo/SKILL.md"), or
* bare (skills/foo/SKILL.md surrounded by non-word chars).
*
* Idempotency contract if any form is present, we never re-append a
* row for this skill, even with --force. This is broader than the
* original backtick-only match: users who hand-edit the resolver to
* normalize formatting (drop backticks, use quotes, etc.) should not
* cause duplicate rows on the next scaffold --force.
*/
function detectExistingResolverRow(resolverFile: string, name: string): boolean {
let content: string;
@@ -128,7 +135,15 @@ function detectExistingResolverRow(resolverFile: string, name: string): boolean
return false;
}
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`\`skills\\/${escaped}\\/SKILL\\.md\``);
// Match the path with any common delimiter on either side: backtick,
// single quote, double quote, parenthesis, whitespace, start/end of
// line. The `(?:^|...)` and `(?:$|...)` anchors ensure we don't
// false-match on something like "skills/foo-bar/SKILL.md" when
// looking for "foo".
const re = new RegExp(
`(?:^|[\`'"\\s\\(\\[])skills\\/${escaped}\\/SKILL\\.md(?:[\`'"\\s\\)\\]]|$)`,
'm',
);
return re.test(content);
}
+124 -7
View File
@@ -238,15 +238,57 @@ function releaseLock(workspace: string): void {
const MANAGED_BEGIN = '<!-- gbrain:skillpack:begin -->';
const MANAGED_END = '<!-- gbrain:skillpack:end -->';
export function buildManagedBlock(manifest: BundleManifest, slugs: string[]): string {
// Receipt comment embedded inside the fence on every write. Lets the
// next install distinguish "row gbrain installed previously" from
// "row a user hand-added inside the fence." Format is intentionally
// regex-friendly.
//
// <!-- gbrain:skillpack:manifest cumulative-slugs="a,b,c" version="0.19.0" -->
//
// Sorted, comma-separated slug list. version is the gbrain version
// that wrote this receipt.
const RECEIPT_RE =
/<!-- gbrain:skillpack:manifest cumulative-slugs="([^"]*)" version="([^"]*)" -->/;
function buildReceipt(cumulativeSlugs: string[], version: string): string {
const sorted = [...cumulativeSlugs].sort();
return `<!-- gbrain:skillpack:manifest cumulative-slugs="${sorted.join(',')}" version="${version}" -->`;
}
/**
* Parse the receipt comment from a managed block. Returns null if no
* receipt is present (pre-v0.19 fences). The slug list is split on
* comma; an empty string returns an empty list.
*/
export function parseReceipt(resolverContent: string): { cumulativeSlugs: string[]; version: string } | null {
const beginIdx = resolverContent.indexOf(MANAGED_BEGIN);
const endIdx = resolverContent.indexOf(MANAGED_END);
if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return null;
const block = resolverContent.slice(beginIdx, endIdx);
const m = RECEIPT_RE.exec(block);
if (!m) return null;
const slugs = m[1].length === 0 ? [] : m[1].split(',');
return { cumulativeSlugs: slugs, version: m[2] };
}
export function buildManagedBlock(
manifest: BundleManifest,
slugs: string[],
cumulativeSlugs?: string[],
): string {
const sorted = [...slugs].sort();
const rows = sorted.map(
slug => `| "${slug}" | \`skills/${slug}/SKILL.md\` |`,
);
// Default cumulative = the rendered slug set when caller didn't
// pass one explicitly (kept backward-compatible with older callers
// that don't yet thread the cumulative set through).
const receipt = buildReceipt(cumulativeSlugs ?? sorted, manifest.version);
return [
MANAGED_BEGIN,
'',
`<!-- Installed by gbrain ${manifest.version} — do not hand-edit between markers. -->`,
receipt,
'',
'| Trigger | Skill |',
'|---------|-------|',
@@ -339,15 +381,24 @@ export function applyInstall(
});
}
// Managed block update
// Managed block update.
//
// installedSlugs = slugs we just wrote in THIS call.
// bundleSlugs = the FULL bundle manifest's slug list (always
// populated; used for the install-all prune path).
// isInstallAll = caller passed --all (no specific skillSlug).
const installedSlugs = opts.skillSlug
? [opts.skillSlug]
: plan.manifest.skills.map(pathSlug);
const bundleSlugs = plan.manifest.skills.map(pathSlug);
const isInstallAll = !opts.skillSlug;
const managedBlock = applyManagedBlock(
plan.targetWorkspace,
plan.targetSkillsDir,
plan.manifest,
installedSlugs,
bundleSlugs,
isInstallAll,
opts.dryRun ?? false,
);
@@ -371,6 +422,8 @@ function applyManagedBlock(
skillsDir: string,
manifest: BundleManifest,
installedSlugs: string[],
bundleSlugs: string[],
isInstallAll: boolean,
dryRun: boolean,
): ManagedBlockResult {
// Prefer skills-dir resolver; fall back to workspace-root resolver.
@@ -383,11 +436,75 @@ function applyManagedBlock(
};
}
const existing = readFileSync(resolver, 'utf-8');
// Merge with any slugs already present in the managed block so
// repeated single-skill installs accumulate rather than overwrite.
const priorSlugs = extractManagedSlugs(existing);
const merged = Array.from(new Set([...priorSlugs, ...installedSlugs]));
const newBlock = buildManagedBlock(manifest, merged);
// Step 1: figure out what gbrain previously installed into this fence.
// - If receipt is present, trust it as the cumulative-slug history.
// - If receipt is absent (pre-v0.19 fence), fall back to the rows
// currently in the fence — they were ALL gbrain-written before
// the receipt feature existed, so trust them as the prior set.
const receipt = parseReceipt(existing);
const priorCumulativeSlugs =
receipt !== null
? new Set(receipt.cumulativeSlugs)
: new Set(extractManagedSlugs(existing));
// Step 2: compute the new cumulative slug set.
// - Single-skill install: union(prior, installed). Per-skill
// installs accumulate; the documented v0.18 behavior.
// - Install-all: prune slugs no longer in the bundle. Renamed
// and removed skills leave the cumulative set ONLY via this
// path. (Single-skill never prunes — it would regress
// cumulative semantics for unrelated skills.)
//
// We track `prunedSlugs` separately so the unknown-row detector
// (Step 3) doesn't re-resurrect slugs we just intentionally removed.
const newCumulative = new Set(priorCumulativeSlugs);
for (const s of installedSlugs) newCumulative.add(s);
const prunedSlugs = new Set<string>();
if (isInstallAll) {
const bundleSet = new Set(bundleSlugs);
for (const s of [...newCumulative]) {
if (!bundleSet.has(s)) {
newCumulative.delete(s);
prunedSlugs.add(s);
}
}
}
// Step 3: detect unknown rows. A row inside the fence whose slug
// is NOT in newCumulative AND NOT in bundleSlugs AND NOT in the
// intentionally-pruned set is something gbrain never wrote: a user
// hand-add, a typo, or stale debris from an unknown bundle.
// Preserve it (do not destroy data) and emit a single stderr
// warning per slug instructing the agent to investigate.
const existingRowSlugs = extractManagedSlugs(existing);
const bundleSet = new Set(bundleSlugs);
const unknownSlugs: string[] = [];
// Skip the unknown-row check on the very first v0.19 install (no
// receipt yet). All existing rows are presumed gbrain-written and
// captured into newCumulative via the fallback above; warning here
// would create false positives.
if (receipt !== null) {
for (const slug of existingRowSlugs) {
if (newCumulative.has(slug)) continue;
if (bundleSet.has(slug)) continue;
if (prunedSlugs.has(slug)) continue; // known prune, do not resurrect
unknownSlugs.push(slug);
// Re-add to newCumulative so the rebuild preserves the row.
newCumulative.add(slug);
}
}
for (const slug of unknownSlugs) {
console.error(
`[skillpack] unknown row in managed block: "${slug}" at skills/${slug}/SKILL.md — not in gbrain's installed set. Investigate: user-added skill, hand-edited fence, or typo?`,
);
}
// Step 4: write the new block. The visible row set is sorted
// newCumulative. The receipt comment carries the same set so the
// next install can do the same diff.
const cumulativeArr = [...newCumulative].sort();
const newBlock = buildManagedBlock(manifest, cumulativeArr, cumulativeArr);
const updated = updateManagedBlock(existing, newBlock);
if (updated === existing) {
return { resolverFile: resolver, applied: false, skippedReason: 'no_change' };
+4 -4
View File
@@ -52,7 +52,7 @@ const SEED_PAGES: SeedPage[] = [
embeddingDim: 14,
},
{
slug: 'wintermute/chat/2026-04-15',
slug: 'openclaw/chat/2026-04-15',
type: 'note',
title: '2026-04-15 chat',
body:
@@ -61,7 +61,7 @@ const SEED_PAGES: SeedPage[] = [
embeddingDim: 8,
},
{
slug: 'wintermute/chat/2026-04-16',
slug: 'openclaw/chat/2026-04-16',
type: 'note',
title: '2026-04-16 chat',
body:
@@ -215,8 +215,8 @@ describeBoth('Engine parity — Postgres vs PGLite', () => {
const pgliteHigh = await pgliteEngine.searchKeyword('fat code thin harness', { detail: 'high', limit: 5 });
// Chat pages must be present in detail=high results on both engines.
expect(pgHigh.some((r: SearchResult) => r.slug.startsWith('wintermute/chat/'))).toBe(true);
expect(pgliteHigh.some((r: SearchResult) => r.slug.startsWith('wintermute/chat/'))).toBe(true);
expect(pgHigh.some((r: SearchResult) => r.slug.startsWith('openclaw/chat/'))).toBe(true);
expect(pgliteHigh.some((r: SearchResult) => r.slug.startsWith('openclaw/chat/'))).toBe(true);
// The boost must be doing something — at least one engine's ordering
// should change between default and detail=high.
+6 -6
View File
@@ -2,7 +2,7 @@
* Search Swamp Resistance E2E
*
* Reproduces the v3-plan repro case: a curated article (originals/) competes
* with two chat-log pages (wintermute/chat/) on similar ts_rank. With v0.21+
* with two chat-log pages (openclaw/chat/) on similar ts_rank. With v0.21+
* source-aware ranking, the article must rank #0.
*
* Mirrors the structure of search-quality.test.ts. Uses PGLite in-memory.
@@ -47,7 +47,7 @@ beforeAll(async () => {
] satisfies ChunkInput[]);
// Chat swamp #1 — long page, mentions the phrase repeatedly.
await engine.putPage('wintermute/chat/2026-04-15', {
await engine.putPage('openclaw/chat/2026-04-15', {
type: 'note',
title: '2026-04-15 chat',
compiled_truth: '',
@@ -57,7 +57,7 @@ beforeAll(async () => {
'The fat code thin harness pattern is something we keep returning to. ' +
'fat code thin harness fat code thin harness fat code thin harness.',
});
await engine.upsertChunks('wintermute/chat/2026-04-15', [
await engine.upsertChunks('openclaw/chat/2026-04-15', [
{
chunk_index: 0,
chunk_text:
@@ -71,7 +71,7 @@ beforeAll(async () => {
] satisfies ChunkInput[]);
// Chat swamp #2 — same shape.
await engine.putPage('wintermute/chat/2026-04-16', {
await engine.putPage('openclaw/chat/2026-04-16', {
type: 'note',
title: '2026-04-16 chat',
compiled_truth: '',
@@ -79,7 +79,7 @@ beforeAll(async () => {
'fat code thin harness once more. fat code thin harness fat code thin harness. ' +
'still talking about fat code thin harness. fat code thin harness.',
});
await engine.upsertChunks('wintermute/chat/2026-04-16', [
await engine.upsertChunks('openclaw/chat/2026-04-16', [
{
chunk_index: 0,
chunk_text:
@@ -112,7 +112,7 @@ describe('searchKeyword swamp resistance', () => {
expect(results.length).toBeGreaterThan(0);
// Top result should be a chat page (more keyword density per chunk).
const topSlugs = results.slice(0, 2).map(r => r.slug);
const anyChat = topSlugs.some(s => s.startsWith('wintermute/chat/'));
const anyChat = topSlugs.some(s => s.startsWith('openclaw/chat/'));
expect(anyChat).toBe(true);
});
});
+44
View File
@@ -0,0 +1,44 @@
/**
* Regression guard: scripts/check-privacy.sh must be wired into the
* `bun run test` chain.
*
* CLAUDE.md:550 bans the private OpenClaw fork name from public
* artifacts. scripts/check-privacy.sh is the enforcement mechanism.
* If someone refactors the test script and drops the privacy check,
* this test fails loudly.
*
* The assertion is a substring match against package.json's
* scripts.test field. Deliberately simple: the goal is to detect
* accidental unwiring, not to validate the shell chain's semantics.
*/
import { describe, it, expect } from 'bun:test';
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
const REPO_ROOT = resolve(import.meta.dir, '..');
const PACKAGE_JSON = resolve(REPO_ROOT, 'package.json');
const PRIVACY_SCRIPT = resolve(REPO_ROOT, 'scripts/check-privacy.sh');
describe('check-privacy.sh CI wiring', () => {
it('scripts/check-privacy.sh exists and is executable', () => {
expect(existsSync(PRIVACY_SCRIPT)).toBe(true);
const stat = require('fs').statSync(PRIVACY_SCRIPT);
// Mode has user-exec bit set.
// eslint-disable-next-line no-bitwise
expect((stat.mode & 0o100) !== 0).toBe(true);
});
it('package.json "test" script includes check-privacy.sh', () => {
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf-8'));
expect(typeof pkg.scripts?.test).toBe('string');
expect(pkg.scripts.test).toContain('check-privacy.sh');
});
it('package.json exposes a "check:privacy" convenience alias', () => {
// Not load-bearing for CI, but prevents the script from disappearing
// from the scripts map during a refactor.
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf-8'));
expect(pkg.scripts?.['check:privacy']).toContain('check-privacy.sh');
});
});
+138
View File
@@ -0,0 +1,138 @@
/**
* Tests for `gbrain routing-eval` CLI surface specifically --llm
* placeholder behavior.
*
* v0.19 ships the structural layer only. The --llm flag is accepted
* as a placeholder for a future LLM tie-break layer. This test file
* locks in the contract:
*
* 1. Passing --llm emits a stderr notice ("placeholder" / "structural
* layer only"). Regardless of --json.
* 2. Passing --llm does NOT alter exit code (0 on clean, 1 on issues,
* same as without --llm).
* 3. Passing --llm --json emits valid structural JSON on stdout with
* the warning on stderr only (no stderr-to-stdout bleed).
*/
import { describe, it, expect, afterEach } from 'bun:test';
import { spawnSync } from 'child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join, resolve } from 'path';
const CLI = resolve(import.meta.dir, '..', 'src', 'cli.ts');
const REPO_ROOT = resolve(import.meta.dir, '..');
function makeFixture(created: string[]): string {
const root = mkdtempSync(join(tmpdir(), 'routing-eval-cli-'));
created.push(root);
const skillsDir = join(root, 'skills');
mkdirSync(skillsDir, { recursive: true });
// Minimal resolver: one skill with a trigger phrase.
const resolver = [
'# Resolver',
'',
'| Trigger | Skill |',
'|---------|-------|',
'| "build the foo" | `skills/foo-builder/SKILL.md` |',
'',
].join('\n');
writeFileSync(join(skillsDir, 'RESOLVER.md'), resolver);
// One skill + one routing fixture that maps to it.
const skillDir = join(skillsDir, 'foo-builder');
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(skillDir, 'SKILL.md'),
'---\nname: foo-builder\n---\n\nBuilds foos.\n',
);
writeFileSync(
join(skillDir, 'routing-eval.jsonl'),
JSON.stringify({ intent: 'build the foo now please', expected_skill: 'foo-builder' }) + '\n',
);
// Manifest referencing the skill.
writeFileSync(
join(skillsDir, 'manifest.json'),
JSON.stringify({ skills: [{ name: 'foo-builder', path: 'foo-builder/SKILL.md' }] }, null, 2),
);
return skillsDir;
}
const WARNING_NEEDLE = 'placeholder';
describe('gbrain routing-eval CLI — --llm placeholder behavior', () => {
const created: string[] = [];
afterEach(() => {
for (const d of created) try { rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ }
created.length = 0;
});
it('--llm emits a stderr notice and exits 0 on clean fixtures', () => {
const skillsDir = makeFixture(created);
const proc = spawnSync('bun', [CLI, 'routing-eval', '--skills-dir', skillsDir, '--llm'], {
cwd: REPO_ROOT,
encoding: 'utf-8',
});
expect(proc.status).toBe(0);
expect(proc.stderr).toContain(WARNING_NEEDLE);
// Human-mode stdout still shows the structural results header.
expect(proc.stdout).toContain('routing-eval');
});
it('--llm --json emits warning on stderr AND valid structural JSON on stdout (no bleed)', () => {
const skillsDir = makeFixture(created);
const proc = spawnSync('bun', [CLI, 'routing-eval', '--skills-dir', skillsDir, '--llm', '--json'], {
cwd: REPO_ROOT,
encoding: 'utf-8',
});
expect(proc.status).toBe(0);
expect(proc.stderr).toContain(WARNING_NEEDLE);
// stdout must be clean JSON — no warning text bleed.
expect(proc.stdout).not.toContain(WARNING_NEEDLE);
const envelope = JSON.parse(proc.stdout); // throws if bleed corrupted it
expect(envelope.ok).toBe(true);
expect(envelope.skillsDir).toBe(skillsDir);
expect(envelope.report).not.toBeNull();
});
it('WITHOUT --llm, no placeholder warning on stderr (regression guard)', () => {
const skillsDir = makeFixture(created);
const proc = spawnSync('bun', [CLI, 'routing-eval', '--skills-dir', skillsDir], {
cwd: REPO_ROOT,
encoding: 'utf-8',
});
expect(proc.status).toBe(0);
expect(proc.stderr).not.toContain(WARNING_NEEDLE);
});
it('--llm does NOT alter exit code when fixtures have issues (still 1, not 2)', () => {
const created2: string[] = [];
const root = mkdtempSync(join(tmpdir(), 'routing-eval-cli-fail-'));
created2.push(root);
created.push(root);
const skillsDir = join(root, 'skills');
mkdirSync(skillsDir, { recursive: true });
// Resolver with no row pointing at the expected skill → miss.
writeFileSync(join(skillsDir, 'RESOLVER.md'), '# Resolver\n\n| Trigger | Skill |\n|---|---|\n');
const skillDir = join(skillsDir, 'bar-skill');
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: bar-skill\n---\n');
writeFileSync(
join(skillDir, 'routing-eval.jsonl'),
JSON.stringify({ intent: 'do bar now', expected_skill: 'bar-skill' }) + '\n',
);
writeFileSync(
join(skillsDir, 'manifest.json'),
JSON.stringify({ skills: [{ name: 'bar-skill', path: 'bar-skill/SKILL.md' }] }, null, 2),
);
const proc = spawnSync('bun', [CLI, 'routing-eval', '--skills-dir', skillsDir, '--llm'], {
cwd: REPO_ROOT,
encoding: 'utf-8',
});
expect(proc.status).toBe(1);
expect(proc.stderr).toContain(WARNING_NEEDLE);
});
});
+77
View File
@@ -210,6 +210,83 @@ describe('planScaffold', () => {
expect(plan.resolverAppend).toBeNull();
});
it('detects bare-path resolver row (no backticks) → no duplicate append', () => {
// User hand-edited the resolver to drop backticks. The original
// backtick-only matcher missed this; broadened matcher catches it.
const { root, skillsDir } = scratchRepo();
const resolverPath = join(skillsDir, 'RESOLVER.md');
const before = readFileSync(resolverPath, 'utf-8');
writeFileSync(
resolverPath,
before +
'\n## Uncategorized\n\n| Trigger | Skill |\n|---------|-------|\n' +
'| "do thing" | skills/demo/SKILL.md |\n',
);
const plan = planScaffold({
skillsDir,
repoRoot: root,
vars: { name: 'demo', description: 'd', triggers: ['do thing'], writesTo: [], writesPages: false, mutating: false },
});
expect(plan.resolverAppend).toBeNull();
});
it('detects double-quoted resolver row → no duplicate append', () => {
const { root, skillsDir } = scratchRepo();
const resolverPath = join(skillsDir, 'RESOLVER.md');
const before = readFileSync(resolverPath, 'utf-8');
writeFileSync(
resolverPath,
before +
'\n## Uncategorized\n\n| Trigger | Skill |\n|---------|-------|\n' +
'| "do thing" | "skills/demo/SKILL.md" |\n',
);
const plan = planScaffold({
skillsDir,
repoRoot: root,
vars: { name: 'demo', description: 'd', triggers: ['do thing'], writesTo: [], writesPages: false, mutating: false },
});
expect(plan.resolverAppend).toBeNull();
});
it('detects single-quoted resolver row → no duplicate append', () => {
const { root, skillsDir } = scratchRepo();
const resolverPath = join(skillsDir, 'RESOLVER.md');
const before = readFileSync(resolverPath, 'utf-8');
writeFileSync(
resolverPath,
before +
"\n## Uncategorized\n\n| Trigger | Skill |\n|---------|-------|\n" +
"| \"do thing\" | 'skills/demo/SKILL.md' |\n",
);
const plan = planScaffold({
skillsDir,
repoRoot: root,
vars: { name: 'demo', description: 'd', triggers: ['do thing'], writesTo: [], writesPages: false, mutating: false },
});
expect(plan.resolverAppend).toBeNull();
});
it('does NOT false-match a longer skill name with a shared prefix', () => {
// If "demo" is the target but resolver only references
// "demo-extended", we MUST treat that as no existing row (different
// skill). Broadened matcher uses anchored boundaries to prevent this.
const { root, skillsDir } = scratchRepo();
const resolverPath = join(skillsDir, 'RESOLVER.md');
const before = readFileSync(resolverPath, 'utf-8');
writeFileSync(
resolverPath,
before +
'\n## Uncategorized\n\n| Trigger | Skill |\n|---------|-------|\n' +
'| "do extended" | `skills/demo-extended/SKILL.md` |\n',
);
const plan = planScaffold({
skillsDir,
repoRoot: root,
vars: { name: 'demo', description: 'd', triggers: ['do thing'], writesTo: [], writesPages: false, mutating: false },
});
expect(plan.resolverAppend).not.toBeNull();
});
it('handles --triggers omitted by seeding TBD placeholder', () => {
const { root, skillsDir } = scratchRepo();
const plan = planScaffold({
+177
View File
@@ -29,6 +29,7 @@ import {
buildManagedBlock,
diffSkill,
extractManagedSlugs,
parseReceipt,
planInstall,
updateManagedBlock,
InstallError,
@@ -436,6 +437,182 @@ describe('planInstall + applyInstall', () => {
});
});
describe('managed-block receipt + cumulative semantics (v0.19)', () => {
/**
* Regression guard for cumulative-install semantics. The PR's CEO
* review originally proposed a "rebuild from current manifest" that
* would have deleted alpha's row when a user later ran `install beta`
* alone. Codex caught it. This test fails fast if anyone tries that
* design again.
*/
it('install alpha, then install beta (separately) → both rows survive AND receipt lists both', () => {
const { gbrainRoot } = scratchGbrain();
const { workspace, skillsDir } = scratchTarget();
const alphaOpts = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: 'alpha' };
applyInstall(planInstall(alphaOpts), alphaOpts);
const betaOpts = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: 'beta' };
applyInstall(planInstall(betaOpts), betaOpts);
const resolver = readFileSync(join(skillsDir, 'RESOLVER.md'), 'utf-8');
expect(resolver).toContain('`skills/alpha/SKILL.md`');
expect(resolver).toContain('`skills/beta/SKILL.md`');
const receipt = parseReceipt(resolver);
expect(receipt).not.toBeNull();
expect(receipt!.cumulativeSlugs.sort()).toEqual(['alpha', 'beta']);
});
/**
* Full-bundle install IS the prune surface. Removing a slug from the
* bundle and running install --all silently drops the row.
*/
it('install --all then remove a slug from bundle and re-install --all → removed slug pruned silently', () => {
const { gbrainRoot } = scratchGbrain();
const { workspace, skillsDir } = scratchTarget();
const allOpts = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: null };
applyInstall(planInstall(allOpts), allOpts);
let resolver = readFileSync(join(skillsDir, 'RESOLVER.md'), 'utf-8');
expect(resolver).toContain('`skills/alpha/SKILL.md`');
expect(resolver).toContain('`skills/beta/SKILL.md`');
// Simulate "alpha removed from bundle" by rewriting the manifest.
const pluginManifest = JSON.parse(
readFileSync(join(gbrainRoot, 'openclaw.plugin.json'), 'utf-8'),
);
pluginManifest.skills = ['skills/beta'];
writeFileSync(
join(gbrainRoot, 'openclaw.plugin.json'),
JSON.stringify(pluginManifest, null, 2),
);
// Capture stderr to verify NO unknown-row warning fires for the
// pruned slug (it's a known removal via prior receipt + bundle diff).
const stderrLines: string[] = [];
const origErr = console.error;
console.error = (...args: unknown[]) => {
stderrLines.push(args.map(String).join(' '));
};
try {
const allOpts2 = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: null };
applyInstall(planInstall(allOpts2), allOpts2);
} finally {
console.error = origErr;
}
resolver = readFileSync(join(skillsDir, 'RESOLVER.md'), 'utf-8');
expect(resolver).not.toContain('`skills/alpha/SKILL.md`');
expect(resolver).toContain('`skills/beta/SKILL.md`');
const receipt = parseReceipt(resolver);
expect(receipt!.cumulativeSlugs).toEqual(['beta']);
// Known prune ⇒ silent.
expect(stderrLines.some(l => l.includes('alpha'))).toBe(false);
});
/**
* User hand-adds a row inside the fence. Reinstall must not destroy
* it. Stderr emits the investigate warning so the operating agent
* notices.
*/
it('user hand-adds an unknown row → preserved on reinstall AND stderr warning fires', () => {
const { gbrainRoot } = scratchGbrain();
const { workspace, skillsDir } = scratchTarget();
const allOpts = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: null };
applyInstall(planInstall(allOpts), allOpts);
// Inject a hand-added row inside the fence (between begin/end).
// We splice right before the fence end marker so the row is
// unambiguously within gbrain's managed block.
const path = join(skillsDir, 'RESOLVER.md');
const orig = readFileSync(path, 'utf-8');
const endMarker = '<!-- gbrain:skillpack:end -->';
const endIdx = orig.indexOf(endMarker);
expect(endIdx).toBeGreaterThan(-1);
const splice =
orig.slice(0, endIdx) +
'| "custom" | `skills/custom-skill/SKILL.md` |\n\n' +
orig.slice(endIdx);
writeFileSync(path, splice);
// Capture stderr.
const stderrLines: string[] = [];
const origErr = console.error;
console.error = (...args: unknown[]) => {
stderrLines.push(args.map(String).join(' '));
};
try {
const allOpts2 = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: null };
applyInstall(planInstall(allOpts2), allOpts2);
} finally {
console.error = origErr;
}
const resolver = readFileSync(path, 'utf-8');
// Hand-added row preserved.
expect(resolver).toContain('`skills/custom-skill/SKILL.md`');
// Stderr told the agent to investigate.
const warning = stderrLines.find(l => l.includes('custom-skill'));
expect(warning).toBeDefined();
expect(warning!).toContain('Investigate');
});
/**
* Pre-v0.19 fence (no receipt comment): the first install on it
* must not destroy data and must not fire warnings (rows were
* gbrain-written before the receipt feature existed). Receipt is
* present after the rebuild.
*/
it('pre-v0.19 fence (no receipt) → clean rebuild, receipt now present, no warnings', () => {
const { gbrainRoot } = scratchGbrain();
const { workspace, skillsDir } = scratchTarget();
// Hand-write a v0.18-style fence with rows but NO receipt comment.
const path = join(skillsDir, 'RESOLVER.md');
writeFileSync(
path,
[
'# Target RESOLVER',
'',
'<!-- gbrain:skillpack:begin -->',
'',
'<!-- Installed by gbrain 0.18.2 — do not hand-edit between markers. -->',
'',
'| Trigger | Skill |',
'|---------|-------|',
'| "alpha" | `skills/alpha/SKILL.md` |',
'',
'<!-- gbrain:skillpack:end -->',
'',
].join('\n'),
);
expect(parseReceipt(readFileSync(path, 'utf-8'))).toBeNull();
const stderrLines: string[] = [];
const origErr = console.error;
console.error = (...args: unknown[]) => {
stderrLines.push(args.map(String).join(' '));
};
try {
const opts = { gbrainRoot, targetWorkspace: workspace, targetSkillsDir: skillsDir, skillSlug: 'beta' };
applyInstall(planInstall(opts), opts);
} finally {
console.error = origErr;
}
const resolver = readFileSync(path, 'utf-8');
expect(resolver).toContain('`skills/alpha/SKILL.md`');
expect(resolver).toContain('`skills/beta/SKILL.md`');
const receipt = parseReceipt(resolver);
expect(receipt).not.toBeNull();
expect(receipt!.cumulativeSlugs.sort()).toEqual(['alpha', 'beta']);
// No warnings on this first upgrade.
expect(stderrLines.length).toBe(0);
});
});
describe('diffSkill', () => {
it('reports missing files', () => {
const { gbrainRoot } = scratchGbrain();
+2 -2
View File
@@ -162,8 +162,8 @@ describe('buildHardExcludeClause', () => {
describe('parseSourceBoostEnv', () => {
test('parses comma-separated prefix:factor pairs', () => {
expect(parseSourceBoostEnv('originals/:1.8,wintermute/chat/:0.3'))
.toEqual({ 'originals/': 1.8, 'wintermute/chat/': 0.3 });
expect(parseSourceBoostEnv('originals/:1.8,openclaw/chat/:0.3'))
.toEqual({ 'originals/': 1.8, 'openclaw/chat/': 0.3 });
});
test('returns empty object for undefined or empty', () => {