mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(skillpack): extract copyArtifacts shared helper (T1)
Pure file-copy primitive for scaffold (gbrain→host) and harvest (host→gbrain).
Atomic-refusal contract: symlink-reject + canonical-path containment validate
every item before any write. Used by both directions of the v0.33 loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): scaffold subcommand + SKILL.md frontmatter sources (T2)
New scaffold.ts replaces the managed-block installer. One-time additive copy
into the user's repo via copyArtifacts; refuses to overwrite existing files
(user owns them). Partial-state policy: copies missing paired sources even
when the skill dir already exists.
bundle.ts extended with loadSkillSources + enumerateScaffoldEntries — paired
source files declared in each SKILL.md's frontmatter sources: array, not in
openclaw.plugin.json. Single source of truth, co-located with the skill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): reference command + apply-clean-hunks (T4 + T15)
reference is the read-only diff lens with an agent-readable framing line. Pure-JS
unified-diff producer + parser + applier (no patch(1) dependency). Two-way merge
with documented limitation: without scaffold-time base tracking, applied hunks
align everything to gbrain. The agent dry-runs reference first, then decides.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): migrate-fence + scrub-legacy-fence-rows (T5 + T16)
migrate-fence is the one-shot transition from the pre-v0.36 managed-block model.
Strips begin/end markers and the cumulative-slugs receipt comment; preserves
fence rows verbatim as user-owned routing during the transition to frontmatter
discovery. Receipt-then-row fallback (F-CDX-8) covers stale/missing receipts.
scrub-legacy-fence-rows is the opt-in cleanup after migrate-fence. Two-condition
gate: removes a row only when skills/<slug>/ exists AND that skill's frontmatter
declares non-empty triggers (proof frontmatter discovery covers it).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): harvest + privacy linter (T6 + T7)
The inverse loop: lift a proven skill from a host repo (~/git/wintermute, etc.)
back into gbrain so other clients can scaffold it. --from <host-repo-root> is
symmetric with scaffold's --workspace.
Security: symlink rejection + canonical-path containment (mirrors validateUploadPath).
Privacy: default-on linter scans harvested files against ~/.gbrain/harvest-private-patterns.txt
plus built-in defaults (Wintermute, email, Slack channel patterns). Any match
rolls back the copy and exits non-zero. --no-lint bypasses for the editorial
workflow after a manual scrub.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(repo-root): cwd_walk_up tier for non-OpenClaw hosts (T9 + D3)
autoDetectSkillsDir now walks up from cwd looking for any skills/ directory,
ahead of the implicit ~/.openclaw/workspace fallback. cd ~/git/wintermute &&
gbrain skillpack scaffold ... finds wintermute automatically without requiring
a RESOLVER.md/AGENTS.md to exist yet.
R5 regression preserved: $OPENCLAW_WORKSPACE still wins when explicitly set.
+5 test cases in test/repo-root.test.ts pin the new tier order and the R5 guard.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): rewrite CLI dispatch, drop install + uninstall (T3 + T10)
skillpack.ts dispatcher rewritten for the v0.36 contract: scaffold, reference
(+ --apply-clean-hunks), migrate-fence, scrub-legacy-fence-rows, harvest, plus
the existing list / diff / check.
install and uninstall are gone — both exit non-zero with a hint pointing at
scaffold / migrate-fence. Clean break, no deprecated alias.
skillpack-check gains --strict for CI gating. When invoked as the subcommand
`gbrain skillpack check`, default is informational (exit 0 even with drift);
--strict opts back into the cron-friendly exit-1-on-issues behavior. Top-level
gbrain skillpack-check preserves its existing exit semantics for backwards compat.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skills): skillpack-harvest editorial workflow + resolver wiring (T8)
The companion editorial skill for the gbrain skillpack harvest CLI. Walks the
genericization checklist (scrub fork names, generalize triggers, lift fork-
specific conventions to references) before the CLI runs. Routing-eval fixtures
use paraphrased intents to avoid the intent_copies_trigger lint.
Wires the new slug into openclaw.plugin.json#skills, skills/manifest.json, and
skills/RESOLVER.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(skillpack): 9-case real-subprocess E2E flow (T11)
Spawns gbrain as a subprocess against tempdir workspaces. Covers: scaffold
first-run + re-run no-op, reference diff + --apply-clean-hunks, migrate-fence,
scrub-legacy-fence-rows, harvest privacy-lint catch + --no-lint bypass, and
the install removed-error path. No DATABASE_URL needed — skillpack is
filesystem-only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: docs + VERSION + CHANGELOG for v0.36.0.0 (T13 + T14)
Skillpacks as scaffolding, not amber.
v0.36 retires the managed-block install model. Six new subcommands replace
install + uninstall: scaffold, reference (with --apply-clean-hunks), migrate-fence,
scrub-legacy-fence-rows, harvest, plus the existing list / diff / check
(check gains --strict for CI gating). Routing comes from each skill's
frontmatter triggers — gbrain does not touch your RESOLVER.md or AGENTS.md.
Companion editorial skill skillpack-harvest drives the genericization
checklist; default-on privacy linter catches Wintermute / email / Slack
references before they leak into gbrain core.
New docs guide at docs/guides/skillpacks-as-scaffolding.md walks the model
and the migration path for pre-v0.36 installs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(ci): privacy checks — allow-list harvest-lint tests, scrub user-facing fork-name references
CI's check-privacy.sh and check-test-real-names.sh both flagged the literal
fork name across the v0.36 skillpack diff. Two failure modes, two fixes:
1. **Meta-rule-enforcement files** added to both allow-lists. The harvest
privacy linter's whole job is to catch the banned literal leaking into
gbrain; its source has the regex pattern, its tests verify the linter
fires by feeding it the banned string, and the skill markdown documents
the substitution policy. Same exception status as check-privacy.sh and
check-proposal-pii.sh themselves. Files allow-listed:
- src/core/skillpack/harvest-lint.ts
- test/skillpack-harvest-lint.test.ts
- test/skillpack-harvest.test.ts
- test/e2e/skillpack-flow.test.ts
- skills/skillpack-harvest/SKILL.md
2. **User-facing references** swapped for canonical phrasing per CLAUDE.md's
responsible-disclosure rule. README + new docs guide + 4 src docstrings
+ 1 test now say 'your OpenClaw' / 'host agent repo' / 'agentRepo' var
name. Behavior unchanged — only documentation strings touched.
Verify gate (the script CI runs) passes locally: EXIT=0.
Tests still pass: 60/60 across the affected files.
llms-full.txt regenerated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(test): update check-resolvable-cli expectation for cwd_walk_up tier
Sister fix to the test/repo-root.test.ts update in commit a31418e3. The new
v0.33 cwd_walk_up tier fires before repo_root when running from inside the
gbrain repo — same skills/ dir matched, different source label. Behavior
unchanged; the legacy repo_root tier is now functionally subsumed (kept in
the type union for back-compat).
CI shard 3 failure: test/check-resolvable-cli.test.ts:171.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(test): pin clock in sync_freshness boundary tests (CI flake)
The 24h and 72h exact-boundary tests scheduled last_sync_at relative to
Date.now() at construction time, then let the check call Date.now() again
internally. CI scheduler jitter between the two reads pushed ageMs past
the strict > thresholds by microseconds, dropping the 72h-boundary case
into the fail branch instead of warn.
Fix: add an optional `opts.now` test seam to checkSyncFreshness. The two
boundary tests now capture t0 once and pass it both to the timestamp
constructor and to the check, making ageMs deterministically equal to
the boundary. The non-boundary tests (4d, 30h, 2h, etc.) don't need
pinning — they're comfortably away from the > comparison.
CI shard 1 flake: test/doctor.test.ts:479. Locally 48/48 doctor tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): agent-onboarding readme + next-action hints on every CLI surface (DX review)
DX audit of the v0.36 scaffold model surfaced one structural gap and four
output gaps. When scaffolded files land on a downstream agent's disk, the
agent had no agent-facing manifest telling it what to do — no routing
contract, no upgrade flow, no two-way merge warning at the right surface.
Fixes:
1. **New shared dep: skills/_AGENT_README.md.** Lands on every scaffold +
migrate-fence alongside the existing _brain-filing-rules.md and
_output-rules.md. Short, agent-readable contract: walk *.SKILL.md
frontmatter triggers: for routing, gbrain is reference not law on
upgrade, no managed-block fence anymore, two-way merge has known
limitations. Single source of truth for the agent operating contract.
2. **scaffold stdout** prints a next-action hint pointing at the readme
(with absolute path) and the reference --all upgrade-sweep command.
3. **reference stdout** adds per-category decision policy:
- missing → scaffold again
- differs → was edit intentional? keep it. Accidental? patch by hand or
apply-clean-hunks after reading the two-way warning.
4. **reference --apply-clean-hunks** prints the two-way merge WARNING
BEFORE the apply (to stderr, survives stdout redirect). Spells out
that gbrain has no scaffold-time base and local edits in differing
sections WILL be aligned to gbrain. Skipped in --json mode for
machine consumers. On conflicts, prints how to inspect and patch.
5. **migrate-fence stdout** tells the agent its routing model just
changed (fence gone, walk frontmatter now) and points at
scrub-legacy-fence-rows as the eventual cleanup. References the new
_AGENT_README for fresh-install agents.
Smoke verified end-to-end: 16 files land (was 15, +1 for _AGENT_README),
hint prints with absolute path, readme lands on disk. Tests + verify gate
pass clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skillpack): upgrade-time reference sweep + reference --since version filter (DX deferred items)
Closes the last two DX gaps from the v0.36 audit:
1. **Post-upgrade reference sweep.** New `postUpgradeReferenceSweep`
helper called at the end of `gbrain post-upgrade`. After migrations
apply, auto-runs `reference --all` against the detected host
workspace and prints a one-line-per-skill summary of drift. Five
gates: GBRAIN_SKIP_REFERENCE_SWEEP env-var bypass, no detected
workspace (silent), workspace IS gbrain repo (dev-mode silent),
zero drift (silent), and pure-missing skills the host never
scaffolded are filtered out as noise. All errors swallowed —
never blocks post-upgrade. Helper accepts test-seam opts
(gbrainRoot, targetWorkspace) for unit testability.
2. **`reference --all --since <version>`.** Filters the sweep to
skills whose source actually changed in gbrain between
<version> and HEAD, using a new `changedSlugsSinceVersion`
helper in bundle.ts. Pure-JS git wrapper (spawnSync), no deps.
Accepts bare '0.X.Y.Z' or 'v0.X.Y.Z' or commit SHA. Falls back
loudly to full sweep when git can't resolve the ref (tarball
install, missing tag).
Test coverage added — total +32 new test cases:
UNIT (15 cases):
- test/skillpack-changed-since-version.test.ts (9 cases): git-aware
filter against a fixture git repo. Covers null on non-repo,
null on bad tag, empty array on no changes, single + multi-slug
drift (deduped + sorted), bare + v-prefix version forms, non-
skills/ path filtering, SHA-prefix ref form.
- test/upgrade-reference-sweep.test.ts (6 cases): gate logic.
Covers env-var bypass, zero drift, empty-host suppression,
drift-detected output shape, dev-mode workspace==gbrain guard,
error-swallowing contract.
E2E (8 new cases in test/e2e/skillpack-flow.test.ts):
- 10: scaffold lands skills/_AGENT_README.md
- 11: scaffold stdout prints the Next: hint
- 12: scaffold re-run (skipped-existing) suppresses the hint
- 13: reference stdout prints per-category decision policy
- 14: --apply-clean-hunks WARNING on stderr, not stdout
- 15: --apply-clean-hunks --json suppresses the WARNING (bug fix
surfaced here: code originally printed unconditionally, now
gated on !json)
- 16: migrate-fence stdout points at the new routing model
- 17: --since with a bad tag falls back to full sweep with warn
Local sweep: 579/579 pass across 18 affected test files, verify
gate EXIT=0, llms regenerated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(README): zero-base rewrite — 921 → 422 lines, refreshed catalog, MECE structure
The README had drifted into a changelog dumping ground. Four 'New in vX.Y'
paragraphs competed for the lead, 16 version tags scattered through
headings, the production-numbers hook (17,888 pages, 4,383 people) was
six months stale, and skills were described in three places (Skills section,
Commands section, inline marketing prose).
Zero-based rewrite:
**Refreshed catalog** (surveyed live brain + live agent fork, broad strokes
per CLAUDE.md privacy rules):
- ~100K total brain items (was 17,888 in the old README — 6x stale)
- ~16K people (was 4,383)
- ~5K companies (was 723)
- ~8K concepts, ~4K originals, ~3.5K daily notes
- ~31K media (30K tweets, 179 books, papers/films/games/interviews)
- 108 cron jobs running (was 21)
- 273 skills in the live agent fork (35 bundled + 238 user-built)
**Structure** — MECE, single source of truth per concept:
1. Hook + at-a-glance table (refreshed numbers)
2. Install (3 paths, terse)
3. What it does (5 capability areas — replaces 12 scattered sections)
4. Skills (categorized one-liners — 35 lines, was ~200)
5. How it works (one coherent flow — replaces 4 overlapping sections:
Architecture, Knowledge Model, Knowledge Graph, Search, Why It Works)
6. Commands (terse cheatsheet — every command, one line each)
7. Docs (link map — points to docs/ for the heavy stuff)
8. Origin / Contributing / License
**Cut entirely** (moved or deleted):
- 4 'New in vX.Y' leads (→ CHANGELOG.md is the changelog)
- 16 (vX.Y) version tags in section headings
- Minions stats subsection (subsumed into hook + 'durable background work')
- Voice section (was 12 lines of brand prose)
- Engine Architecture detail (→ docs/architecture/)
- File Storage section (→ docs/guides/storage-tiering.md)
- Per-skill marketing prose (one-liner per skill in the table)
The README is no longer the changelog. Future releases append to
CHANGELOG.md; the README only changes when a structural capability does.
llms-full.txt regenerated. Privacy check + verify gate pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(README): fix line-start '+' rendering bug + lead with eval evidence
Two fixes in one:
1. **Markdown bug fix.** The OAuth 2.1 paragraph had `+ PKCE,` on a line
start (column 1), which GitHub-flavored markdown interprets as a list
marker — the line break before it broke the paragraph and rendered as
an orphan first line followed by a bullet. Rewrote the OAuth 2.1
capabilities as inline-comma-separated, escaped the `+` semantics.
Swept the whole file for the same bug class — no other instances.
2. **Maximum-sell mode for evals.** Surveyed every published benchmark
in both this repo and ~/git/gbrain-evals. Strongest evidence pulled
to the top:
- **97.60% R@5 on the public LongMemEval _s (500 questions).** No LLM
in the retrieval loop. $0.50 per 1000 queries. Beats MemPalace raw
by a point on the same dataset, beats every academic dense
retriever (Stella, Contriever, BM25). Mastra/Supermemory measure
a different metric (QA accuracy with LLM judge) — flagged honestly.
- **+31.4 points P@5 from the self-wiring knowledge graph** on
BrainBench v0.20.0 (240-page rich-prose corpus, 145 relational
gold queries). Separable, measured, load-bearing. Zero retrieval
regression across seven releases (v0.16 → v0.20).
New '## Benchmarks' section after Install:
- Public benchmark table with cross-system comparison
- In-house BrainBench scorecard with per-adapter Δ vs gbrain
- Source-swamp resistance result (93.3% top-1 vs 80% grep-only)
- Skill/prompt compression: 25KB → 13KB AGENTS.md, +13-17pp accuracy
across Opus 4.7 / Sonnet 4.6 / Haiku 4.5
- 'Run your own evals' subsection with copy-pasteable commands for
every eval surface (longmemeval, cross-modal, eval capture/replay,
BrainBench)
Tightened the lead's cost-comparison claim to what's defensible per
the underlying eval doc (MemPal LLM-rerank $0.001/q vs gbrain
$0.0005/q; dropped the overstated '6x' I'd written initially).
Privacy + verify gate + build-llms test all pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(README): integrate the eval story into the lead, move jargon into 'Receipts on the evals'
Previous lead dumped metric acronyms (R@5, P@5, P@5 deltas, MemPalace,
Stella, Contriever, BM25) before the reader knew what gbrain does. A
'somewhat technical' reader hits the wall of jargon and bounces.
Rewritten:
**Lead (jargon-free, 3 paragraphs)** — describes the value in plain
English, with two anchor numbers:
- 'right answer in top 5 results 97.6% of the time' (not 'R@5 97.60%')
- 'roughly 4x more relevant than plain vector RAG' (not '+31.4 pts P@5')
- 'better than every comparable system that doesn't pay for a language-
model call on every retrieval' (the load-bearing honest framing,
without naming the competitors mid-hook)
- ends with '[Receipts on the evals →]' linking down
**'## Benchmarks' renamed '## Receipts on the evals'** with a glossary
at the top defining R@5, P@5, and 'no LLM in the loop' in one line each.
Then the full tables: LongMemEval cross-system (with the metric-mismatch
flag for Mastra/Supermemory), in-house BrainBench scorecard, source-swamp
resistance, and prompt compression. The competitor names + metrics stay
here where readers who want the receipts can find them, with the
glossary so the acronyms don't tax cold readers.
Net: lead reads as 'here's what it does and the proof' instead of 'here
are the benchmark numbers, figure out what they mean.' Comparison facts
unchanged.
Privacy + verify gate + build-llms test all pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(README): name LongMemEval explicitly + first-person voice in lead
Two specific edits from user feedback:
1. 'the standard public benchmark for AI memory systems' → 'LongMemEval'
(linked to the HuggingFace dataset). The benchmark has a name; use it.
2. 'Built by the President and CEO of Y Combinator to run his own AI
agents' (passive third-person) → 'I'm the President and CEO of Y
Combinator, and I use this 16 hours a day' (active first-person).
Carried the voice change through the rest of the README — the
downstream 'Garry's personal agent' line and the Origin section's
'Garry Tan needed... he'd ever drafted... so he built one' all flip
to first person ('my personal agent', 'I needed', 'I'd ever drafted',
'so I built one'). The README is now consistently first-person from
the author's voice instead of a hagiographic third-person framing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(README): add Multi-player and company brains section
Three deployment patterns documented:
1. Single GBrain server + thin MCP clients (recommended). Tailscale
private networking, OAuth scope, source-scoped clients, exhaustive
what-clients-can/cannot-do lists.
2. Local PGLite + GStack for per-worktree code search.
3. Federated repos (advanced) — multiple servers indexing the same
brain repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(README): tighten install path + add tech-orientation block + visceral query example
Self-eval as a cold reader surfaced four gaps blocking a 10/10 first read:
1. Lead never says WHAT it is technically — CLI? service? cloud? local?
Added a "What it is, technically" block right after the hook: open-source
MIT, Bun CLI + MCP server, local-first, data stays on disk, MCP-native.
2. Install path optimized for committed users not evaluators. The old
"recommended" path (deploy OpenClaw on Render, 8GB RAM) blocked anyone
trying gbrain for the first time. Reordered into 3 paths by commitment:
60-second standalone CLI first, MCP for Claude Code / Cursor second,
full agentic install third.
3. No example output showing what success looks like. Added a real sample
`gbrain query` invocation with the hybrid-search result format so a
reader can feel the experience before they install.
4. Privacy / data-locality unaddressed in lead. Now stated up front:
embedding calls only hit external APIs if you configure them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
332 lines
14 KiB
TypeScript
332 lines
14 KiB
TypeScript
import { describe, it, expect, afterEach } from 'bun:test';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import {
|
|
autoDetectSkillsDir,
|
|
autoDetectSkillsDirReadOnly,
|
|
findRepoRoot,
|
|
AUTO_DETECT_HINT,
|
|
AUTO_DETECT_HINT_READ_ONLY,
|
|
} from '../src/core/repo-root.ts';
|
|
|
|
describe('findRepoRoot', () => {
|
|
const created: string[] = [];
|
|
afterEach(() => {
|
|
while (created.length) {
|
|
const p = created.pop()!;
|
|
try { rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
}
|
|
});
|
|
|
|
function scratch(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), 'repo-root-'));
|
|
created.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function seedRepo(dir: string): void {
|
|
mkdirSync(join(dir, 'skills'), { recursive: true });
|
|
writeFileSync(join(dir, 'skills', 'RESOLVER.md'), '# RESOLVER\n');
|
|
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
writeFileSync(join(dir, 'src', 'cli.ts'), '// gbrain marker\n');
|
|
}
|
|
|
|
function seedSkillsDir(dir: string): void {
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, 'RESOLVER.md'), '# RESOLVER\n');
|
|
}
|
|
|
|
it('finds skills/RESOLVER.md in the passed startDir on first iteration', () => {
|
|
const root = scratch();
|
|
seedRepo(root);
|
|
expect(findRepoRoot(root)).toBe(root);
|
|
});
|
|
|
|
it('walks up N directories to find the repo root', () => {
|
|
const root = scratch();
|
|
seedRepo(root);
|
|
const nested = join(root, 'a', 'b', 'c');
|
|
mkdirSync(nested, { recursive: true });
|
|
expect(findRepoRoot(nested)).toBe(root);
|
|
});
|
|
|
|
it('returns null when no skills/RESOLVER.md exists up to filesystem root', () => {
|
|
const empty = scratch();
|
|
// Deliberately no seedRepo — empty dir; walk terminates at filesystem root.
|
|
expect(findRepoRoot(empty)).toBeNull();
|
|
});
|
|
|
|
it('default arg uses process.cwd() (behavioral parity with prior doctor-private impl)', () => {
|
|
// The default arg must match calling with an explicit process.cwd().
|
|
// Don't assert on the path contents — it varies between local checkouts
|
|
// and CI runners. What matters is parity: no-arg === cwd-arg.
|
|
expect(findRepoRoot()).toBe(findRepoRoot(process.cwd()));
|
|
});
|
|
|
|
it('auto-detect: falls back to $OPENCLAW_WORKSPACE/skills when repo root is absent', () => {
|
|
const cwd = scratch();
|
|
const workspace = scratch();
|
|
seedSkillsDir(join(workspace, 'skills'));
|
|
const found = autoDetectSkillsDir(cwd, { OPENCLAW_WORKSPACE: workspace });
|
|
expect(found.dir).toBe(join(workspace, 'skills'));
|
|
expect(found.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
it('auto-detect: falls back to ~/.openclaw/workspace/skills when env var is not set', () => {
|
|
const cwd = scratch();
|
|
const home = scratch();
|
|
seedSkillsDir(join(home, '.openclaw', 'workspace', 'skills'));
|
|
const found = autoDetectSkillsDir(cwd, { HOME: home });
|
|
expect(found.dir).toBe(join(home, '.openclaw', 'workspace', 'skills'));
|
|
expect(found.source).toBe('openclaw_workspace_home');
|
|
});
|
|
|
|
it('auto-detect: cwd has ./skills directory → cwd_walk_up tier (v0.33)', () => {
|
|
// Behavior unchanged from pre-v0.33 except for the reported source
|
|
// — cwd_walk_up fires first now that the broader tier exists.
|
|
// cwd_skills (resolver-gated) is functionally subsumed.
|
|
const cwd = scratch();
|
|
seedSkillsDir(join(cwd, 'skills'));
|
|
const found = autoDetectSkillsDir(cwd, {});
|
|
expect(found.dir).toBe(join(cwd, 'skills'));
|
|
expect(found.source).toBe('cwd_walk_up');
|
|
});
|
|
|
|
it('D-CX-4: $OPENCLAW_WORKSPACE wins over repo-root walk when explicitly set', () => {
|
|
// Prior priority (shadow bug): walking up from cwd found gbrain's
|
|
// repo root first and silently ignored the env var. Post-D-CX-4:
|
|
// explicit env wins. Unset env → repo-root walk still wins
|
|
// (tested below).
|
|
const root = scratch();
|
|
seedRepo(root);
|
|
const workspace = scratch();
|
|
seedSkillsDir(join(workspace, 'skills'));
|
|
const nested = join(root, 'a', 'b');
|
|
mkdirSync(nested, { recursive: true });
|
|
const found = autoDetectSkillsDir(nested, { OPENCLAW_WORKSPACE: workspace });
|
|
expect(found.dir).toBe(join(workspace, 'skills'));
|
|
expect(found.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
it('D-CX-4 + v0.33: walk-up from nested finds the repo skills/ via cwd_walk_up', () => {
|
|
// Pre-v0.33: this returned source='repo_root' via findRepoRoot's
|
|
// isGbrainRepoRoot gate. v0.33 added a broader cwd_walk_up tier
|
|
// ahead of repo_root — same dir matched, different source name.
|
|
// Behavior preserved; source label changed.
|
|
const root = scratch();
|
|
seedRepo(root);
|
|
const nested = join(root, 'a', 'b');
|
|
mkdirSync(nested, { recursive: true });
|
|
const found = autoDetectSkillsDir(nested, {});
|
|
expect(found.dir).toBe(join(root, 'skills'));
|
|
expect(found.source).toBe('cwd_walk_up');
|
|
});
|
|
|
|
it('W1: AGENTS.md at skills dir is accepted (OpenClaw skills-subdir variant)', () => {
|
|
const workspace = scratch();
|
|
const skillsDir = join(workspace, 'skills');
|
|
mkdirSync(skillsDir, { recursive: true });
|
|
// seed AGENTS.md (no RESOLVER.md)
|
|
require('fs').writeFileSync(
|
|
join(skillsDir, 'AGENTS.md'),
|
|
'# AGENTS\n\n| Trigger | Skill |\n|---|---|\n',
|
|
);
|
|
const found = autoDetectSkillsDir(scratch(), { OPENCLAW_WORKSPACE: workspace });
|
|
expect(found.dir).toBe(skillsDir);
|
|
expect(found.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
it('W1: AGENTS.md at workspace root (OpenClaw-native layout)', () => {
|
|
// The reference OpenClaw deployment places AGENTS.md at
|
|
// workspace/AGENTS.md, with skills in workspace/skills/. Auto-detect
|
|
// must find the skills dir and flag this as the workspace-root variant.
|
|
const workspace = scratch();
|
|
const skillsDir = join(workspace, 'skills');
|
|
mkdirSync(skillsDir, { recursive: true });
|
|
require('fs').writeFileSync(
|
|
join(workspace, 'AGENTS.md'),
|
|
'# AGENTS\n\n| Trigger | Skill |\n|---|---|\n',
|
|
);
|
|
const found = autoDetectSkillsDir(scratch(), { OPENCLAW_WORKSPACE: workspace });
|
|
expect(found.dir).toBe(skillsDir);
|
|
expect(found.source).toBe('openclaw_workspace_env_root');
|
|
});
|
|
|
|
it('W1: both RESOLVER.md and AGENTS.md present — RESOLVER.md wins', () => {
|
|
// Policy: when both exist at the same location, gbrain-native wins.
|
|
const workspace = scratch();
|
|
const skillsDir = join(workspace, 'skills');
|
|
mkdirSync(skillsDir, { recursive: true });
|
|
require('fs').writeFileSync(
|
|
join(skillsDir, 'RESOLVER.md'),
|
|
'# RESOLVER\n\n| Trigger | Skill |\n|---|---|\n',
|
|
);
|
|
require('fs').writeFileSync(
|
|
join(skillsDir, 'AGENTS.md'),
|
|
'# AGENTS\n\n| Trigger | Skill |\n|---|---|\n',
|
|
);
|
|
const found = autoDetectSkillsDir(scratch(), { OPENCLAW_WORKSPACE: workspace });
|
|
expect(found.dir).toBe(skillsDir);
|
|
// Source is still `env` — the distinction is which file was found,
|
|
// and RESOLVER.md takes precedence inside resolveWorkspaceSkillsDir.
|
|
expect(found.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// v0.31.7 #128 adaptation: tier-0 GBRAIN_SKILLS_DIR + read-only
|
|
// install-path fallback. Locked in eng-review D3 + D5.
|
|
// -----------------------------------------------------------------------
|
|
|
|
it('v0.31.7 D3-1: tier-0 $GBRAIN_SKILLS_DIR valid path returns env_explicit source', () => {
|
|
const cwd = scratch();
|
|
const explicit = scratch();
|
|
seedSkillsDir(explicit);
|
|
const found = autoDetectSkillsDir(cwd, { GBRAIN_SKILLS_DIR: explicit });
|
|
expect(found.dir).toBe(explicit);
|
|
expect(found.source).toBe('env_explicit');
|
|
});
|
|
|
|
it('v0.31.7 D3-2: tier-0 invalid path falls through to lower tiers', () => {
|
|
// GBRAIN_SKILLS_DIR points to a directory with no resolver file. Must
|
|
// not crash; must continue to the next tier (OPENCLAW_WORKSPACE here).
|
|
const cwd = scratch();
|
|
const invalid = scratch(); // no RESOLVER.md / AGENTS.md inside
|
|
const workspace = scratch();
|
|
seedSkillsDir(join(workspace, 'skills'));
|
|
const found = autoDetectSkillsDir(cwd, {
|
|
GBRAIN_SKILLS_DIR: invalid,
|
|
OPENCLAW_WORKSPACE: workspace,
|
|
});
|
|
expect(found.dir).toBe(join(workspace, 'skills'));
|
|
expect(found.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
it('v0.31.7 D3-3: tier-0 wins over $OPENCLAW_WORKSPACE precedence', () => {
|
|
// Both env vars set. Tier-0 (explicit operator override) MUST win.
|
|
const cwd = scratch();
|
|
const explicit = scratch();
|
|
seedSkillsDir(explicit);
|
|
const workspace = scratch();
|
|
seedSkillsDir(join(workspace, 'skills'));
|
|
const found = autoDetectSkillsDir(cwd, {
|
|
GBRAIN_SKILLS_DIR: explicit,
|
|
OPENCLAW_WORKSPACE: workspace,
|
|
});
|
|
expect(found.dir).toBe(explicit);
|
|
expect(found.source).toBe('env_explicit');
|
|
});
|
|
|
|
it('v0.31.7 D3-4: autoDetectSkillsDirReadOnly install-path walk finds bundled skills', () => {
|
|
// When primary detection returns null (no env, no openclaw, no repo
|
|
// root walk-up, no ./skills), the read-only variant walks up from
|
|
// import.meta.url (the gbrain module's install path) and finds the
|
|
// bundled skills/. This test runs from a tempdir cwd with no env vars
|
|
// set; the only path that can succeed is the install-path fallback.
|
|
const cwd = scratch();
|
|
const found = autoDetectSkillsDirReadOnly(cwd, {});
|
|
// We're inside the gbrain repo when running the test, so the install
|
|
// path resolves to the repo's skills dir.
|
|
expect(found.dir).not.toBeNull();
|
|
expect(found.source).toBe('install_path');
|
|
expect(found.dir).toMatch(/\/skills$/);
|
|
});
|
|
|
|
it('v0.31.7 D3-5: autoDetectSkillsDirReadOnly returns same primary detection on success', () => {
|
|
// When primary detection succeeds, the read-only variant must return
|
|
// the SAME result — no behavior drift, install-path is fallback only.
|
|
const cwd = scratch();
|
|
const workspace = scratch();
|
|
seedSkillsDir(join(workspace, 'skills'));
|
|
const env = { OPENCLAW_WORKSPACE: workspace };
|
|
const primary = autoDetectSkillsDir(cwd, env);
|
|
const readOnly = autoDetectSkillsDirReadOnly(cwd, env);
|
|
expect(readOnly.dir).toBe(primary.dir);
|
|
expect(readOnly.source).toBe(primary.source);
|
|
// Specifically NOT install_path — primary succeeded so fallback never fires.
|
|
expect(readOnly.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
it('v0.31.7 D3-6: AUTO_DETECT_HINT documents tier-0 GBRAIN_SKILLS_DIR', () => {
|
|
// Hint string is what users see when auto-detect fails — must list the
|
|
// tier-0 explicit override so they know to set it.
|
|
expect(AUTO_DETECT_HINT).toContain('$GBRAIN_SKILLS_DIR');
|
|
expect(AUTO_DETECT_HINT).toContain('explicit operator override');
|
|
});
|
|
|
|
it('v0.31.7 D3-7: AUTO_DETECT_HINT_READ_ONLY adds install-path tier', () => {
|
|
// Read-only hint must mention the install-path fallback so doctor's
|
|
// user can understand what gbrain found and where.
|
|
expect(AUTO_DETECT_HINT_READ_ONLY).toContain('install path');
|
|
expect(AUTO_DETECT_HINT_READ_ONLY).toContain('read-only');
|
|
// And must include everything from the base hint.
|
|
expect(AUTO_DETECT_HINT_READ_ONLY).toContain('$GBRAIN_SKILLS_DIR');
|
|
expect(AUTO_DETECT_HINT_READ_ONLY).toContain('$OPENCLAW_WORKSPACE');
|
|
});
|
|
|
|
// ─── v0.33 cwd_walk_up tier (D3) ──────────────────────────────
|
|
// Non-OpenClaw hosts (any agent repo with a bare skills/ dir) own
|
|
// their skills/ directly. The new tier resolves these without
|
|
// requiring a resolver file or gbrain-shape `src/cli.ts`.
|
|
|
|
it('v0.33 cwd_walk_up: cwd has skills/ but no resolver, no src/cli.ts (bare-skills-dir shape)', () => {
|
|
const agentRepo = scratch();
|
|
mkdirSync(join(agentRepo, 'skills'));
|
|
// Intentionally: no RESOLVER.md / AGENTS.md, no src/cli.ts.
|
|
const found = autoDetectSkillsDir(agentRepo, {});
|
|
expect(found.dir).toBe(join(agentRepo, 'skills'));
|
|
expect(found.source).toBe('cwd_walk_up');
|
|
});
|
|
|
|
it('v0.33 cwd_walk_up: walks up from nested cwd to first ancestor with skills/', () => {
|
|
const agentRepo = scratch();
|
|
mkdirSync(join(agentRepo, 'skills'));
|
|
const nested = join(agentRepo, 'src', 'commands');
|
|
mkdirSync(nested, { recursive: true });
|
|
const found = autoDetectSkillsDir(nested, {});
|
|
expect(found.dir).toBe(join(agentRepo, 'skills'));
|
|
expect(found.source).toBe('cwd_walk_up');
|
|
});
|
|
|
|
it('R5 regression: $OPENCLAW_WORKSPACE still wins when both env and cwd-skills exist', () => {
|
|
// Pre-v0.33 R5: explicit env beats implicit cwd. The new cwd_walk_up
|
|
// tier MUST NOT regress this — explicit operator intent always wins.
|
|
const agentRepo = scratch();
|
|
mkdirSync(join(agentRepo, 'skills'));
|
|
const openclawWs = scratch();
|
|
mkdirSync(join(openclawWs, 'skills'));
|
|
writeFileSync(join(openclawWs, 'skills', 'AGENTS.md'), '# agents\n');
|
|
|
|
const found = autoDetectSkillsDir(agentRepo, { OPENCLAW_WORKSPACE: openclawWs });
|
|
expect(found.dir).toBe(join(openclawWs, 'skills'));
|
|
expect(found.source).toBe('openclaw_workspace_env');
|
|
});
|
|
|
|
it('v0.33 cwd_walk_up: no skills/ anywhere up to root → returns null', () => {
|
|
const empty = scratch();
|
|
const nested = join(empty, 'deep', 'nested', 'dir');
|
|
mkdirSync(nested, { recursive: true });
|
|
const found = autoDetectSkillsDir(nested, {});
|
|
expect(found.dir).toBeNull();
|
|
expect(found.source).toBeNull();
|
|
});
|
|
|
|
it('v0.31.7 D5 regression guard: autoDetectSkillsDir does NOT install-path-fallback', () => {
|
|
// CRITICAL: this test pins the read-path/write-path split. The shared
|
|
// autoDetectSkillsDir is used by write-path callers (skillpack install,
|
|
// skillify scaffold, post-install-advisory). If a future edit adds the
|
|
// install-path fallback to the SHARED function, those write-path
|
|
// callers running from `~` would silently retarget the bundled gbrain
|
|
// repo's skills/ instead of the user's actual workspace — a quiet
|
|
// data-flow regression. This test asserts the shared function returns
|
|
// null (not an install_path source) when no env or repo-root signal
|
|
// is present, even though the install-path fallback would succeed.
|
|
const cwd = scratch(); // empty tempdir; no resolver anywhere up
|
|
const found = autoDetectSkillsDir(cwd, {});
|
|
// Either null (no skills dir found) or repo_root if test runs inside
|
|
// the gbrain repo. In either case, NEVER 'install_path' on the shared
|
|
// function — that variant is reserved for autoDetectSkillsDirReadOnly.
|
|
expect(found.source).not.toBe('install_path');
|
|
});
|
|
});
|