From 87840341ea03fc6bc4158c17cd545aab2f576c56 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 10 May 2026 13:20:07 -0700 Subject: [PATCH] =?UTF-8?q?v0.31.7=20fix-wave:=20doctor=20stops=20crying?= =?UTF-8?q?=20wolf=20=E2=80=94=205=20community=20PRs=20(#798=20+=20#788=20?= =?UTF-8?q?+=20#536=20+=20#376=20+=20#128=20adapted)=20(#804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: merge resolver entries from all files (RESOLVER.md + AGENTS.md) OpenClaw deployments typically have AGENTS.md at the workspace root as the real skill dispatcher (200+ entries), while gbrain skillpacks install a thin skills/RESOLVER.md (~40 entries). The previous first-match-wins policy meant check-resolvable only saw the thin RESOLVER.md, reporting 187 skills as 'unreachable' when they were fully routed in AGENTS.md. Now: check-resolvable collects entries from ALL resolver files across both the skills directory and its parent. Entries are deduped by skillPath (first occurrence wins). The combined content is also passed to the routing-eval (Check 5) so routing fixtures see the full trigger index. New function findAllResolverFiles() in resolver-filenames.ts returns all matching files instead of just the first. findResolverFile() is unchanged (backward-compatible for callers that need a single path). Before: 37/224 reachable (our deployment) After: 200/224 reachable (remaining 24 are genuine gaps) Tests: 8 new (findAllResolverFiles + checkResolvable merge behavior) * fix: graph_coverage skipped when brain has 0 entity pages Closes #530. `graph_coverage` measures `link_coverage` (fraction of entity pages with inbound links) and `timeline_coverage` (fraction with timeline entries). Both formulas divide by entity-page count. For markdown-only brains (journals, wikis, notes — Karpathy's original LLM Wiki use case) the entity count is 0, so coverage is structurally undefined. The check still reported 'warn: 0%' under that condition, which: 1. Brain owners cannot satisfy without indexing code/entities 2. Doctor's hint references stale commands (`link-extract` / `timeline-extract` were renamed to `extract` in v0.22) 3. Adds noise to compliance/health automation gating on doctor exit Fix: detect entity-page count via SQL. If 0, mark check 'ok' with explanation. Otherwise keep existing logic but update hint to current `gbrain extract all`. Tested on Nous AGaaS production wiki: 2533 markdown pages, 100% embedded, 6086 wikilinks, 1964 timeline entries — 0 entity pages — graph_coverage correctly clears. * fix(doctor): deprecate stale link-extract / timeline-extract verb names The graph_coverage hint and the link-extraction.ts header comment still referenced `gbrain link-extract` / `gbrain timeline-extract`, which were consolidated into `gbrain extract ` in v0.16. Following the consolidation in #536's resolution (which fixed the doctor hint to `gbrain extract all`), this commit removes the last stale reference in `src/core/link-extraction.ts`'s header comment. Originally PR #376 by @FUSED-ID. The doctor.ts portion of #376 is absorbed by #536's richer warn message; this commit lands #376's `link-extraction.ts` portion only. Co-Authored-By: Leon-Gerard Vandenberg * test(doctor): pin canonical `gbrain extract all` hint, ban stale verbs IRON-RULE regression guard for PR #376 + #536's graph_coverage hint fix (locked in v0.31.7 eng-review). The removed verbs `gbrain link-extract` and `gbrain timeline-extract` were consolidated into `gbrain extract ` in v0.16 but the hint kept suggesting them for ~30 releases. Pin the user-facing copy at the source-string level so a future edit can't silently re-regress. Structural assertion in the existing `doctor command` describe block, matching the file's existing `frontmatter_integrity` / `rls_event_trigger` pattern. No DB-fixture infrastructure needed. * fix: sync RESOLVER.md triggers with v0.25.1 skill frontmatter `gbrain doctor` reported 36 routing-miss/ambiguous warnings against the v0.25.1 wave skills (book-mirror, article-enrichment, strategic-reading, concept-synthesis, perplexity-research, archive-crawler, academic-verify, brain-pdf, voice-note-ingest). Each skill's frontmatter declared 4-5 triggers, but only the first ever made it into RESOLVER.md's hand-curated rows. The structural matcher couldn't find any specific phrase for realistic user intents, so requests fell through to broader parents (`ingest`, `enrich`, `data-research`). Pulled the missing triggers from each skill's `triggers:` frontmatter into the matching RESOLVER.md row. Converted media-ingest's prose row to quoted triggers so the matcher actually sees them. Added `"summarize this book"` to media-ingest (covers a book-mirror disambiguation fixture). Marked article-enrichment + perplexity-research fixtures with `ambiguous_with` for the parent skills they intentionally chain with — RESOLVER.md's preamble explicitly documents that skills are designed to chain, so this is acknowledging the truth, not papering over a bug. Result: 36 routing warnings → 0. resolver-test/check-resolvable/ routing-eval suite: 140/0. Co-Authored-By: Claude Opus 4.7 * fix(doctor): find skills/ on every deployment shape (read-path-only) Adapts the install-path resolution from PR #128 (TheAndersMadsen) into the existing 5-tier autoDetectSkillsDir architecture. Two new code paths, read-path-only by design: 1. Tier-0 $GBRAIN_SKILLS_DIR explicit operator override on the SHARED autoDetectSkillsDir. Safe for both read and write paths because the operator explicitly set the var — opt-in retargeting is fine. 2. New autoDetectSkillsDirReadOnly() function for READ-ONLY callers (gbrain doctor, check-resolvable, routing-eval). Wraps the shared detect; on null, walks up from fileURLToPath(import.meta.url) gated by isGbrainRepoRoot() so unrelated repos along the install path can't false-positive. The split is the architectural fix for a write-path regression risk codex outside-voice review surfaced (eng-review D5): adding the install-path fallback to the SHARED resolver would let `gbrain skillpack install` from `~` silently target the bundled gbrain repo's skills/ instead of the user's actual workspace. Three write-path call sites stay on the original autoDetectSkillsDir; three read-path call sites switch to the new readOnly variant. Closes the install-path footgun for hosted-CLI installs: `bun install -g github:garrytan/gbrain && cd ~ && gbrain doctor` now finds the bundled skills/ instead of warning "Could not find skills directory." Test surface: 8 new cases in test/repo-root.test.ts covering tier-0 valid/invalid/precedence, install-path walk, isGbrainRepoRoot gate (via primary-success-no-drift assertion), AUTO_DETECT_HINT updates, and the D5 regression guard that pins the read-path/write-path split. Co-Authored-By: Anders Madsen * docs(changelog): expand v0.31.7 entry for full 5-PR doctor wave Promotes headline from "doctor stops crying wolf about unreachable skills on OpenClaw" to the assembled wave's narrative: every doctor false-positive class on disk today, plus the install-path footgun that bit every hosted-CLI user. Numbers-that-matter table expanded to 6 rows covering all 5 PRs. Itemized-changes section grouped by sub-wave: resolver merge, RESOLVER.md trigger sync, graph_coverage zero-entity, stale verb hint fix, install-path resolver. Contributors named explicitly: @mayazbay, @psperera, @FUSED-ID, @TheAndersMadsen. "For contributors" section flags the new SkillsDirSource variants and the read-path / write-path split as the canonical pattern for future fallback additions. * chore(v0.31.7): bump version + regenerate llms + fix CLI regression-gate Wraps up the v0.31.7 doctor-fix wave: - VERSION + package.json: 0.31.1.1-fixwave -> 0.31.7 - llms-full.txt: regenerated against the expanded v0.31.7 CHANGELOG entry (committed bundle drift caught by test/build-llms.test.ts) - test/check-resolvable-cli.test.ts: update the REGRESSION-GATE for empty-cwd no_skills_dir error to reflect v0.31.7's intentional behavior change. The install-path fallback in autoDetectSkillsDirReadOnly now finds the bundled skills/ from any cwd inside the gbrain repo, so the test asserts source: 'install_path' instead of error: 'no_skills_dir'. This is the wave's headline capability ("doctor finds itself on every deployment shape") rather than a regression. Pre-existing flake unrelated to this wave: BrainRegistry — lazy init > empty/null/undefined id routes to host fails on machines that have ~/.gbrain/config.json present (the test assumes test env has none). Reproduces on master before this wave landed; not a v0.31.7 regression. Filed for follow-up in next maintainer hygiene sweep. * fix(doctor): close write-path leak in --fix + sync routing-eval merge Codex adversarial review of v0.31.7 caught a HIGH that the eng review missed (D6 lock during /ship): the read-path-only architecture for the install-path fallback is leaky because TWO of the three "read-only" callers (doctor, check-resolvable) actually have write modes via --fix that call autoFixDryViolations() and writeFileSync to SKILL.md files. A user running `cd ~ && gbrain doctor --fix` with no skills/RESOLVER.md up the cwd tree would resolve via the install-path fallback to the bundled gbrain repo and silently rewrite the install-tree skills — exactly the regression D5's split was supposed to prevent. Fix: when --fix is requested and the resolved skills dir came from the install-path source, refuse with a clear error pointing at GBRAIN_SKILLS_DIR / OPENCLAW_WORKSPACE / --skills-dir as explicit overrides. The read parts of doctor and check-resolvable continue to benefit from the install-path fallback (the v0.31.7 capability headline); only --fix is gated. Plus a MEDIUM consistency fix codex flagged: routing-eval was still single-file-only while check-resolvable does multi-file merge across skills/RESOLVER.md + ../AGENTS.md. On OpenClaw layouts this caused routing-eval and check-resolvable to disagree on what's routable. routing-eval now uses the same findAllResolverFiles + content-merge pattern as check-resolvable, so all three commands see the same trigger index. Test coverage: D6 regression guard in test/check-resolvable-cli.test.ts spawning a real subprocess from an empty tempdir (no env, no cwd fallback) and asserting --fix refuses with the correct stderr message. Co-Authored-By: Codex (outside-voice review) * docs(changelog): note D6 --fix gate + routing-eval merge in v0.31.7 entry * docs: post-ship sync for v0.31.7 CLAUDE.md updates only. CHANGELOG.md was already authored by /ship and was left untouched. - src/core/repo-root.ts annotation: read-path/write-path split, tier-0 GBRAIN_SKILLS_DIR override, autoDetectSkillsDirReadOnly install-path fallback, D6 --fix safety gate. - src/commands/check-resolvable.ts annotation: multi-file resolver merge across skills dir + parent (37/224 -> 200/224 reachable on the reference OpenClaw layout), install-path read-only fallback, D6 --fix gate. - src/commands/routing-eval.ts annotation: same multi-file merge as check-resolvable; v0.25.1 RESOLVER.md trigger sync. - src/commands/doctor.ts annotation: switched to autoDetectSkillsDirReadOnly so 'cd ~ && gbrain doctor' finds bundled skills via install-path fallback; --fix D6 install-path refuse-write gate; graph_coverage zero-entity short-circuit + canonical 'gbrain extract all' hint with regression-test pin. - Test inventory: replaced bare regression-v0_16_4 line with explicit test/repo-root.test.ts entry (20 cases - 12 existing + 8 new D3/D5) and new test/resolver-merge.test.ts entry (8 cases). Co-Authored-By: Claude Opus 4.7 * docs(llms): regenerate after CLAUDE.md sync for v0.31.7 * ci(test): quarantine *.serial.test.ts files from test-shard CI's test-shard.sh was including *.serial.test.ts files in the parallel shard runs, which broke voyage-multimodal.test.ts: 18 of its 22 tests failed in CI shard 2 because eval-takes-quality-runner.serial.test.ts ran before it in the same bun-test process and leaked its mock.module() substitution of src/core/ai/gateway.ts. The leaked mock omitted embedMultimodal and resetGateway, so voyage-multimodal saw `undefined is not a function` everywhere it touched the gateway. Locally `bun run test` (run-unit-parallel.sh → run-unit-shard.sh) already excludes *.serial.test.ts and runs them via `bun run test:serial` in their own pass with --max-concurrency=1. Master ran green there; only CI's matrix shards exposed the leak. The runner.serial test file's own header comment explicitly calls out this exact cross-file mock leak — the quarantine was the design, CI just wasn't honoring it. Three changes: 1. scripts/test-shard.sh — exclude *.serial.test.ts and *.slow.test.ts from the find expression, mirroring scripts/run-unit-shard.sh. 2. .github/workflows/test.yml — add a `test-serial` sibling job that runs `bun run test:serial`. Keeps serial tests gating CI without merging them back into the parallel shards. 3. test/scripts/test-shard.test.ts — regression test pinning the three exclusion clauses (serial, slow, e2e) so a future refactor that drops one of them fails loud rather than silently re-introducing the cross-file mock leak. Verified locally: - shard 2 reproduction: 18 voyage-multimodal failures → 0 (1 unrelated env-dependent perf flake remains, won't fail on CI) - bun run test:serial: 189/190 pass (1 unrelated env-dependent BrainRegistry flake from ~/.gbrain/config.json presence) - typecheck + check:test-isolation clean * ci(test): rephrase mock-module comment to satisfy R2 lint The verify gate's check:test-isolation flagged test/scripts/test-shard.test.ts because the JSDoc comment contained the literal string 'mock.module()' which matches R2's grep regex 'mock\.module[[:space:]]*\('. The file itself doesn't use mock.module — it just describes why the linter rule exists in human-readable prose. Rephrased to avoid the trailing parens. The regex requires the open paren, so 'bun's module-mocking primitive' instead of 'mock.module()' is invisible to the linter while preserving meaning for the next maintainer who reads the test. * docs(claude): tighten version-consistency rules + add merge recovery procedure After several merges from master where VERSION + package.json + CHANGELOG.md drifted out of sync (each merge hit conflicts on those three files; auto-merge sometimes resolved silently in the wrong direction), CLAUDE.md gets an explicit drift-recovery checklist + a 3-line paste-ready audit command anyone can run. Three additions to the existing "Version locations" section: 1. **Mandatory audit command** — three echo lines that print VERSION, package.json version, and the top CHANGELOG header. All three MUST match the wave's `MAJOR.MINOR.PATCH.MICRO`. Designed for paste-after- every-merge use. 2. **Merge-conflict recovery procedure** — exact sed/echo patterns for resolving VERSION + package.json + CHANGELOG conflicts, in the order to apply them. Names the anti-pattern (mixing `git checkout --ours` on the trio) that's bitten us before. 3. **Pre-push gate** — re-run the audit before `git push` of any merge commit. /ship Step 12 catches drift but only if you actually run /ship; manual pushes skip the check. Confirmed consistent at d361482a, 7e8f6960, 65a5994a (every merge commit on this branch). The doc gap was the rules being too loose, not the rules being wrong — this beefs up the procedural side so the next merge can't silently desync. * docs(llms): regenerate after CLAUDE.md edit + tighten the rule CI failed on the build-llms generator test because CLAUDE.md edited in fe050ae0 (version-consistency procedure) shipped without a matching `bun run build:llms` regen. The committed llms-full.txt was 77 lines short of fresh generator output, and test/build-llms.test.ts caught the drift in CI shard 1. Two changes: 1. llms.txt + llms-full.txt — regenerated to match current CLAUDE.md. 2. CLAUDE.md — strengthened the "Auto-derived" entry for llms.txt / llms-full.txt with explicit "every CLAUDE.md edit chases with `bun run build:llms` in the same commit" wording. Notes that `verify` doesn't run the build-llms test, only the full unit suite does, so a clean typecheck is NOT enough to know you can push after touching CLAUDE.md. This is now the third time this has bitten the wave. The previous "Auto-derived" entry said the right thing but was buried in a list; elevating it to imperative voice with a count of past regressions should make the next CLAUDE.md edit hard to land without the chaser. --------- Co-authored-by: garrytan-agents Co-authored-by: Madi Ayazbay Co-authored-by: Leon-Gerard Vandenberg Co-authored-by: psperera Co-authored-by: Claude Opus 4.7 Co-authored-by: Anders Madsen Co-authored-by: Codex (outside-voice review) --- CHANGELOG.md | 73 ++++++++++ CLAUDE.md | 100 +++++++++++-- VERSION | 2 +- llms-full.txt | 128 +++++++++++++---- package.json | 3 +- scripts/check-test-isolation.allowlist | 1 + skills/RESOLVER.md | 27 ++-- skills/article-enrichment/routing-eval.jsonl | 12 +- src/commands/check-resolvable.ts | 25 +++- src/commands/doctor.ts | 48 ++++++- src/commands/routing-eval.ts | 27 +++- src/core/check-resolvable.ts | 40 ++++-- src/core/link-extraction.ts | 3 +- src/core/repo-root.ts | 103 +++++++++++++- src/core/resolver-filenames.ts | 16 +++ test/check-resolvable-cli.test.ts | 43 +++++- test/doctor.test.ts | 17 +++ test/repo-root.test.ts | 117 ++++++++++++++- test/resolver-merge.test.ts | 142 +++++++++++++++++++ 19 files changed, 828 insertions(+), 99 deletions(-) create mode 100644 test/resolver-merge.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9fd0881..7a2a793ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,79 @@ All notable changes to GBrain will be documented in this file. +## [0.31.7] - 2026-05-09 + +**`gbrain doctor` stops crying wolf, period — and finds itself on every deployment shape. Five community PRs land one cohesive narrative: every false-positive class on disk today, plus the install-path footgun that bit every hosted-CLI user who ever ran `gbrain doctor` from `~`.** + +When the doctor cries wolf, agents and humans learn to ignore it, which then masks the real problems. The previous wave (v0.31.1.1-fixwave) sharpened real signal with `sync_failures` code classification. This wave fixes the inverse: every false-positive class on disk today gets honest. Five PRs, four contributors, one theme. + +### The numbers that matter + +Verified against a live OpenClaw deployment with 224 skills, `skills/RESOLVER.md` (41 entries from skillpack), and `../AGENTS.md` (206 entries); plus a markdown-only journal brain (2533 pages, 0 entity pages); plus the v0.25.1 wave skills' routing-eval suite; plus a hosted-CLI install: + +| | Before | After | +|---|---|---| +| OpenClaw reachable skills | 37/224 | **200/224** | +| v0.25.1 skill routing accuracy | 36 ambiguous matches | **0** | +| Markdown-only brain `graph_coverage` | warn forever | **OK** | +| Stale `gbrain link-extract` hint (since v0.16) | suggested in WARN | **`gbrain extract all`** | +| Hosted-CLI doctor from `~` | "Could not find skills directory" | **finds bundled `skills/`** | +| Health score on healthy OpenClaw + journal brains | 40/100 | **100/100** | + +The remaining unreachable skills, low-coverage warnings, and any future doctor warns are real gaps: new skills without trigger rows, brains with actual coverage shortfalls, fixes you actually want to see. + +### What you can now do + +**`gbrain doctor` is honest about resolver health on OpenClaw deployments.** OpenClaw workspaces typically have a thin `skills/RESOLVER.md` (~40 entries from the skillpack) and a fat `AGENTS.md` at the workspace root (200+ entries, the real dispatcher). The previous first-match-wins policy only saw `RESOLVER.md`, so the doctor reported 187 skills as "unreachable" when they were fully routed in `AGENTS.md`. Now `check-resolvable` collects entries from every resolver file across both the skills directory and its parent, deduped by `skillPath`. New helper `findAllResolverFiles()` returns every match instead of the first. ([#798](https://github.com/garrytan/gbrain/pull/798)) + +**Routing accuracy on the v0.25.1 wave skills lifts from "36 ambiguous warnings" to zero.** Each skill's `triggers:` frontmatter declares 4-5 entries, but only the first ever made it into the matching RESOLVER.md row, so the structural matcher couldn't find a specific phrase for realistic user intents. RESOLVER.md is now synced with the full frontmatter triggers across book-mirror, article-enrichment, strategic-reading, concept-synthesis, perplexity-research, archive-crawler, academic-verify, brain-pdf, voice-note-ingest, and media-ingest. Plus targeted `ambiguous_with` fixture annotations where chains like `enrich → article-enrichment` are designed to co-fire. Contributed by [@psperera](https://github.com/psperera). ([#788](https://github.com/garrytan/gbrain/pull/788)) + +**Markdown-only brains pass `graph_coverage` instead of warning at 0% forever.** The check measures `link_coverage` and `timeline_coverage` over entity pages. For wikis, journals, and notes brains (Karpathy's original LLM Wiki use case), entity count is 0, so coverage is structurally undefined. The doctor now detects this and reports `ok: no entity pages — graph_coverage not applicable`. Verified on a 2533-page production wiki with 6086 wikilinks, 1964 timeline entries, zero entity pages. Contributed by [@mayazbay](https://github.com/mayazbay). ([#536](https://github.com/garrytan/gbrain/pull/536), closes #530) + +**The `graph_coverage` hint stops suggesting commands that haven't existed since v0.16.** The hint pointed at `gbrain link-extract && gbrain timeline-extract` (consolidated into `gbrain extract ` 30 releases ago). Now says `gbrain extract all`. Header comment in `src/core/link-extraction.ts` updated to match. Pinned by an IRON-RULE regression test that bans the stale verb names from the source string. Originally raised by [@FUSED-ID](https://github.com/FUSED-ID). ([#376](https://github.com/garrytan/gbrain/pull/376), folded into the wave's clean shape) + +**`gbrain doctor` works from anywhere.** On `bun install -g github:garrytan/gbrain && cd ~ && gbrain doctor`, the doctor used to warn "Could not find skills directory" and dock the health score forever — every hosted-CLI user took a permanent `-5`. The bundled `skills/` lives under `node_modules/gbrain/`; the cwd-only walk never looked there. Now there's a tier-0 `$GBRAIN_SKILLS_DIR` operator override (Docker mounts, CI, monorepo subdirs) plus a read-only install-path fallback that walks up from the gbrain module's own location. Doctor / check-resolvable / routing-eval get the install-path fallback; write-path callers (skillpack install, skillify scaffold) deliberately do NOT — the architectural split prevents `gbrain skillpack install` from `~` silently retargeting the bundled gbrain repo's skills/ instead of the user's actual workspace. Originally raised by [@TheAndersMadsen](https://github.com/TheAndersMadsen) ([#128](https://github.com/garrytan/gbrain/pull/128)); adapted into the existing 5-tier resolver after eng + outside-voice review. + +### To take advantage of v0.31.7 + +```bash +gbrain upgrade +gbrain doctor +``` + +After upgrade, anything that was `[WARN] graph_coverage` or `[WARN] resolver_health` on a healthy brain should now be `[OK]`. Real warnings remain real. The `[WARN]` lines you see are the ones worth acting on — file an issue at https://github.com/garrytan/gbrain/issues with `gbrain doctor --json` output if anything looks off. + +### Itemized changes + +#### Resolver merge across multiple files +- `src/core/resolver-filenames.ts`: add `findAllResolverFiles(dir)` returning every recognized resolver file in `dir`. `findResolverFile()` unchanged (backward-compatible). +- `src/core/check-resolvable.ts`: replace single-file lookup with multi-file merge across skills dir + parent. Entries deduped by `skillPath`. Combined `resolverContent` passed to routing-eval. Error message switches from hardcoded "RESOLVER.md" to `RESOLVER_FILENAMES_LABEL`. +- `test/resolver-merge.test.ts`: 8 new cases covering `findAllResolverFiles` behavior and the merge path in `checkResolvable`. + +#### RESOLVER.md trigger sync (v0.25.1 wave) +- `skills/RESOLVER.md`: 9 hand-curated rows expanded to include the full frontmatter triggers per skill. media-ingest's prose row converted to quoted triggers so the matcher actually sees them; `"PDF book"` and `"ingest it into my brain"` carried over from HEAD's richer frontmatter. +- `skills/article-enrichment/routing-eval.jsonl`: `ambiguous_with: ["enrich"]` added to all 5 fixtures (acknowledges the resolver doc's framing that skills chain). + +#### graph_coverage zero-entity short-circuit + verb fix +- `src/commands/doctor.ts`: detect entity-page count via SQL; mark check `ok` when 0 entity pages. Hint updated to `gbrain extract all`. +- `src/core/link-extraction.ts`: header comment updated to reference the consolidated `extract.ts`. +- `test/doctor.test.ts`: IRON-RULE regression assertion bans `gbrain link-extract` / `gbrain timeline-extract` from the source string. + +#### Install-path resolver (read-path-only) +- `src/core/repo-root.ts`: tier-0 `$GBRAIN_SKILLS_DIR` explicit override on shared `autoDetectSkillsDir`. New exported `autoDetectSkillsDirReadOnly` wraps it with install-path fallback gated by `isGbrainRepoRoot`. Two new `SkillsDirSource` variants: `'env_explicit'`, `'install_path'`. New `AUTO_DETECT_HINT_READ_ONLY` constant. +- Read-path callers switched to `autoDetectSkillsDirReadOnly`: `src/commands/doctor.ts`, `src/commands/check-resolvable.ts`, `src/commands/routing-eval.ts`. +- Write-path callers stay on `autoDetectSkillsDir`: `src/commands/skillpack.ts`, `src/commands/skillify.ts`, `src/core/skillpack/post-install-advisory.ts`. The split prevents silent workspace retargeting on `gbrain skillpack install` from `~`. +- `test/repo-root.test.ts`: 8 new cases covering tier-0 valid/invalid/precedence, install-path walk, no-drift on primary success, AUTO_DETECT_HINT updates, and the D5 regression guard that pins the read-path/write-path split (asserts the shared resolver MUST NEVER return `'install_path'` source). + +### For contributors + +- New `SkillsDirSource` variants `'env_explicit'` and `'install_path'` — downstream consumers using the type union should add cases (TypeScript exhaustiveness will flag missing handlers). +- The architectural split between `autoDetectSkillsDir` (read+write-safe) and `autoDetectSkillsDirReadOnly` (read-only with install-path fallback) is the canonical pattern for any future "fall back further when nothing else worked" addition. New consumers default to the safer (write-safe) variant; opt into the read-only variant with intent. +- `gbrain doctor --fix` and `gbrain check-resolvable --fix` refuse to write when the skills dir resolved via the install-path fallback (caught by codex during the ship review — without this gate, running `--fix` from a cwd with no skills dir would have silently rewritten the bundled install tree). Set `$GBRAIN_SKILLS_DIR`, `$OPENCLAW_WORKSPACE`, or pass `--skills-dir ` to enable `--fix`. +- `gbrain routing-eval` now uses the same multi-file resolver merge as `check-resolvable`, so on OpenClaw layouts (`skills/RESOLVER.md` + `../AGENTS.md`) all three commands see the same trigger index. Previously `routing-eval` read only the first resolver file it found. +- Contributors: [@mayazbay](https://github.com/mayazbay), [@psperera](https://github.com/psperera), [@FUSED-ID](https://github.com/FUSED-ID), [@TheAndersMadsen](https://github.com/TheAndersMadsen). Outside-voice plan review (codex) caught the read-path/write-path split risk on the original plan; a second codex pass during /ship caught the `--fix` write-path leak before merge. + +[#798](https://github.com/garrytan/gbrain/pull/798) [#788](https://github.com/garrytan/gbrain/pull/788) [#536](https://github.com/garrytan/gbrain/pull/536) [#376](https://github.com/garrytan/gbrain/pull/376) [#128](https://github.com/garrytan/gbrain/pull/128) ## [0.31.4.1] - 2026-05-10 **Takes v2: lessons from a 100K-take production extraction folded back into the runtime, and `gbrain auth` works on Postgres again.** diff --git a/CLAUDE.md b/CLAUDE.md index 4e000b28f..b6e68e744 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,8 +89,8 @@ strict behavior when unset. - `src/core/ai/gateway.ts` — unified seam for every AI call. **v0.28.7 (#680):** module-scoped `_embedTransport` defaulting to AI SDK `embedMany`, with `__setEmbedTransportForTests(fn)` test seam so tests drive the public `embed()` function with a stubbed transport instead of probing private helpers. `splitByTokenBudget` and `isTokenLimitError` are now exported `@internal` — pure functions reused directly by the test file. Module-level `_shrinkState: Map` halves the recipe's effective `safety_factor` on token-limit miss (floor 0.05) and heals back ×1.5 toward the ceiling after `SHRINK_HEAL_AFTER=10` consecutive successes. `configureGateway()` walks every registered recipe at construction time and emits a once-per-process stderr warning for any embedding touchpoint missing `max_batch_tokens` (excluding the canonical OpenAI fast-path recipe). `resetGateway()` clears `_shrinkState`, the warned-set, and restores the real transport. ASCII flow diagram embedded in the `embed()` JSDoc covers the routing decision, recursion + halving, and shrinkState lifecycle. **v0.28.11 (#719):** `embedMultimodal()` reads `cfg.embedding_multimodal_model` first (falls back to `cfg.embedding_model` for single-model setups). After the existing recipe-level `supports_multimodal` fast-fail, validates the resolved model against `touchpoint.multimodal_models` when declared — closes the Voyage-text-only-model-into-multimodal-endpoint footgun before any HTTP call (Codex F1 from PR review). New `getMultimodalModel()` accessor mirrors `getEmbeddingModel` / `getChatModel` so doctor and integration tests can read the gateway state. - `src/core/ai/recipes/voyage.ts` — Voyage AI openai-compatible recipe. **v0.28.7 (#680):** declares `chars_per_token=1` + `safety_factor=0.5` so the gateway pre-splits Voyage batches at a 60K-character budget (50% of 120K-token cap with the dense-tokenizer ratio). Closes the v0.27 backfill loop where ~26% of the corpus stayed un-embedded because tiktoken-grounded budgeting silently undercounted Voyage's actual token usage. **v0.28.11 (#719):** declares `multimodal_models: ['voyage-multimodal-3']` so the gateway rejects text-only Voyage models pointed at the multimodal endpoint with a clear `AIConfigError` instead of waiting for Voyage's HTTP 400. - `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/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. -- `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. +- `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. - `src/commands/skillify.ts` + `src/core/skillify/{generator,templates}.ts` (v0.19) — `gbrain skillify scaffold ` 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