mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* 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>
441 lines
14 KiB
TypeScript
441 lines
14 KiB
TypeScript
/**
|
|
* 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
|
|
* compat but is also reachable as `gbrain skillpack check` here.
|
|
*/
|
|
|
|
import { existsSync, readFileSync } from 'fs';
|
|
import { isAbsolute, resolve as resolvePath, join } from 'path';
|
|
|
|
import {
|
|
bundledSkillSlugs,
|
|
findGbrainRoot,
|
|
loadBundleManifest,
|
|
BundleError,
|
|
} from '../core/skillpack/bundle.ts';
|
|
import {
|
|
planInstall,
|
|
applyInstall,
|
|
diffSkill,
|
|
InstallError,
|
|
} from '../core/skillpack/installer.ts';
|
|
import { autoDetectSkillsDir } from '../core/repo-root.ts';
|
|
|
|
const HELP_TOP = `gbrain skillpack <subcommand> [options]
|
|
|
|
Subcommands:
|
|
list Print every skill bundled in openclaw.plugin.json.
|
|
install <name> Copy one skill (or --all) into a target workspace.
|
|
Data-loss protected: per-file diff, --overwrite-local
|
|
escape hatch, lockfile + atomic AGENTS.md update.
|
|
diff <name> Show per-file diff status between the bundle and
|
|
the target workspace for one skill.
|
|
check Run the skillpack health report (same as the
|
|
top-level \`gbrain skillpack-check\`).
|
|
|
|
Run \`gbrain skillpack <subcommand> --help\` for per-subcommand options.
|
|
`;
|
|
|
|
export async function runSkillpack(args: string[]): Promise<void> {
|
|
const sub = args[0];
|
|
const rest = args.slice(1);
|
|
if (!sub || sub === '--help' || sub === '-h') {
|
|
console.log(HELP_TOP);
|
|
process.exit(0);
|
|
}
|
|
if (sub === 'list') {
|
|
await runList(rest);
|
|
return;
|
|
}
|
|
if (sub === 'install') {
|
|
await runInstall(rest);
|
|
return;
|
|
}
|
|
if (sub === 'diff') {
|
|
await runDiff(rest);
|
|
return;
|
|
}
|
|
if (sub === 'check') {
|
|
const { runSkillpackCheck } = await import('./skillpack-check.ts');
|
|
await runSkillpackCheck(rest);
|
|
return;
|
|
}
|
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
console.error(HELP_TOP);
|
|
process.exit(2);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// list
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const HELP_LIST = `gbrain skillpack list [--json]
|
|
|
|
Print every skill bundled in openclaw.plugin.json, one per line.
|
|
Exit 0 always (unless the manifest is missing/malformed).
|
|
`;
|
|
|
|
async function runList(args: string[]): Promise<void> {
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
console.log(HELP_LIST);
|
|
process.exit(0);
|
|
}
|
|
const json = args.includes('--json');
|
|
const gbrainRoot = findGbrainRoot();
|
|
if (!gbrainRoot) {
|
|
console.error(
|
|
'Error: could not find gbrain repo root. Run this from inside a gbrain checkout, or pass --gbrain-root (not yet implemented).',
|
|
);
|
|
process.exit(2);
|
|
}
|
|
let manifest;
|
|
try {
|
|
manifest = loadBundleManifest(gbrainRoot);
|
|
} catch (err) {
|
|
console.error(`skillpack list: ${(err as Error).message}`);
|
|
process.exit(2);
|
|
}
|
|
const slugs = bundledSkillSlugs(manifest);
|
|
if (json) {
|
|
const entries = slugs.map(slug => {
|
|
const skillMd = join(gbrainRoot, 'skills', slug, 'SKILL.md');
|
|
let description: string | null = null;
|
|
if (existsSync(skillMd)) {
|
|
const body = readFileSync(skillMd, 'utf-8');
|
|
const fm = body.match(/^---\n([\s\S]*?)\n---/);
|
|
if (fm) {
|
|
const descMatch = fm[1].match(/^description:\s*["']?([^\n"']+)/m);
|
|
if (descMatch) description = descMatch[1].trim();
|
|
}
|
|
}
|
|
return { name: slug, description };
|
|
});
|
|
console.log(
|
|
JSON.stringify(
|
|
{ name: manifest.name, version: manifest.version, skills: entries },
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} else {
|
|
console.log(`${manifest.name} ${manifest.version} bundle — ${slugs.length} skills:`);
|
|
for (const slug of slugs) {
|
|
console.log(` ${slug}`);
|
|
}
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// install
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface InstallFlags {
|
|
help: boolean;
|
|
json: boolean;
|
|
dryRun: boolean;
|
|
force: boolean;
|
|
overwriteLocal: boolean;
|
|
forceUnlock: boolean;
|
|
all: boolean;
|
|
skillName: string | null;
|
|
skillsDir: string | null;
|
|
workspace: string | null;
|
|
}
|
|
|
|
const HELP_INSTALL = `gbrain skillpack install <name> | --all [options]
|
|
|
|
Copy bundled skills into a target OpenClaw workspace. The target is
|
|
auto-detected (\$OPENCLAW_WORKSPACE, then ~/.openclaw/workspace, then
|
|
--skills-dir). Shared convention files are installed alongside
|
|
(dependency closure per codex D-CX-10).
|
|
|
|
Arguments:
|
|
<name> Install a single skill by slug.
|
|
--all Install every skill in openclaw.plugin.json#skills.
|
|
|
|
Options:
|
|
--dry-run Preview file operations; no writes.
|
|
--overwrite-local For per-file diff: overwrite target files that
|
|
differ from the bundle. Default: skip locally-
|
|
modified files for data-loss protection.
|
|
--force-unlock Acquire the skillpack lockfile even if a stale
|
|
peer lock exists.
|
|
--skills-dir PATH Override target skills directory.
|
|
--workspace PATH Override target workspace (parent of skills/).
|
|
--json Machine-readable envelope.
|
|
--help Show this message.
|
|
|
|
Exit codes:
|
|
0 success
|
|
1 some files skipped due to local-modification protection
|
|
2 setup error (no workspace, no bundle, lock held)
|
|
`;
|
|
|
|
function parseInstallFlags(argv: string[]): InstallFlags {
|
|
const f: InstallFlags = {
|
|
help: false,
|
|
json: false,
|
|
dryRun: false,
|
|
force: false,
|
|
overwriteLocal: false,
|
|
forceUnlock: false,
|
|
all: false,
|
|
skillName: null,
|
|
skillsDir: null,
|
|
workspace: null,
|
|
};
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--help' || a === '-h') f.help = true;
|
|
else if (a === '--json') f.json = true;
|
|
else if (a === '--dry-run') f.dryRun = true;
|
|
else if (a === '--force') f.force = true;
|
|
else if (a === '--overwrite-local') f.overwriteLocal = true;
|
|
else if (a === '--force-unlock') f.forceUnlock = true;
|
|
else if (a === '--all') f.all = true;
|
|
else if (a === '--skills-dir') {
|
|
f.skillsDir = argv[i + 1] ?? null;
|
|
i++;
|
|
} else if (a?.startsWith('--skills-dir=')) {
|
|
f.skillsDir = a.slice('--skills-dir='.length) || null;
|
|
} else if (a === '--workspace') {
|
|
f.workspace = argv[i + 1] ?? null;
|
|
i++;
|
|
} else if (a?.startsWith('--workspace=')) {
|
|
f.workspace = a.slice('--workspace='.length) || null;
|
|
} else if (a && !a.startsWith('--') && !f.skillName) {
|
|
f.skillName = a;
|
|
}
|
|
}
|
|
return f;
|
|
}
|
|
|
|
function resolveAbs(p: string): string {
|
|
return isAbsolute(p) ? p : resolvePath(process.cwd(), p);
|
|
}
|
|
|
|
async function runInstall(args: string[]): Promise<void> {
|
|
const flags = parseInstallFlags(args);
|
|
if (flags.help) {
|
|
console.log(HELP_INSTALL);
|
|
process.exit(0);
|
|
}
|
|
if (!flags.all && !flags.skillName) {
|
|
console.error('Error: pass a skill name or --all.\n');
|
|
console.error(HELP_INSTALL);
|
|
process.exit(2);
|
|
}
|
|
const gbrainRoot = findGbrainRoot();
|
|
if (!gbrainRoot) {
|
|
console.error('Error: could not find gbrain repo root.');
|
|
process.exit(2);
|
|
}
|
|
|
|
// Resolve target: workspace (parent of skills/) is required for the
|
|
// managed block + lockfile. skillsDir defaults to workspace/skills.
|
|
let targetWorkspace: string | null = flags.workspace
|
|
? resolveAbs(flags.workspace)
|
|
: null;
|
|
let targetSkillsDir: string | null = flags.skillsDir
|
|
? resolveAbs(flags.skillsDir)
|
|
: null;
|
|
|
|
if (!targetSkillsDir) {
|
|
const detected = autoDetectSkillsDir();
|
|
if (detected.dir) {
|
|
targetSkillsDir = detected.dir;
|
|
if (!targetWorkspace) {
|
|
// workspace is parent of skills/
|
|
targetWorkspace = resolvePath(targetSkillsDir, '..');
|
|
}
|
|
}
|
|
}
|
|
if (!targetSkillsDir) {
|
|
console.error(
|
|
'Error: could not find a target skills directory. Set $OPENCLAW_WORKSPACE or pass --skills-dir / --workspace.',
|
|
);
|
|
process.exit(2);
|
|
}
|
|
if (!targetWorkspace) {
|
|
targetWorkspace = resolvePath(targetSkillsDir, '..');
|
|
}
|
|
|
|
try {
|
|
const plan = planInstall({
|
|
gbrainRoot,
|
|
targetWorkspace,
|
|
targetSkillsDir,
|
|
skillSlug: flags.all ? null : flags.skillName!,
|
|
overwriteLocal: flags.overwriteLocal,
|
|
dryRun: flags.dryRun,
|
|
forceUnlock: flags.forceUnlock,
|
|
});
|
|
const result = applyInstall(plan, {
|
|
gbrainRoot,
|
|
targetWorkspace,
|
|
targetSkillsDir,
|
|
skillSlug: flags.all ? null : flags.skillName!,
|
|
overwriteLocal: flags.overwriteLocal,
|
|
dryRun: flags.dryRun,
|
|
forceUnlock: flags.forceUnlock,
|
|
});
|
|
|
|
if (flags.json) {
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: result.summary.skippedLocallyModified === 0,
|
|
dryRun: result.dryRun,
|
|
gbrainRoot,
|
|
targetWorkspace,
|
|
targetSkillsDir,
|
|
summary: result.summary,
|
|
managedBlock: result.managedBlock,
|
|
files: result.files.map(f => ({
|
|
source: f.source,
|
|
target: f.target,
|
|
outcome: f.outcome,
|
|
sharedDep: f.sharedDep,
|
|
})),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} else {
|
|
const label = flags.dryRun ? 'skillpack install --dry-run' : 'skillpack install';
|
|
console.log(
|
|
`${label}: ${result.summary.wroteNew} new, ${result.summary.wroteOverwrite} overwrites, ${result.summary.skippedIdentical} unchanged, ${result.summary.skippedLocallyModified} skipped (local edits)`,
|
|
);
|
|
for (const f of result.files) {
|
|
if (f.outcome === 'skipped_identical') continue;
|
|
const tag = f.outcome.padEnd(25);
|
|
const dep = f.sharedDep ? ' [shared]' : '';
|
|
console.log(` ${tag} ${f.target}${dep}`);
|
|
}
|
|
if (result.managedBlock.applied) {
|
|
console.log(` managed-block ${result.managedBlock.resolverFile}`);
|
|
} else if (result.managedBlock.skippedReason === 'resolver_not_found') {
|
|
console.log(
|
|
` warn: no RESOLVER.md / AGENTS.md in ${targetWorkspace} or ${targetSkillsDir} — managed block not written. Create one and re-run.`,
|
|
);
|
|
}
|
|
if (result.summary.skippedLocallyModified > 0) {
|
|
console.log(
|
|
`\nNote: ${result.summary.skippedLocallyModified} file(s) differ from the bundle and were skipped. Pass --overwrite-local to replace them (loses local edits) or run \`gbrain skillpack diff <name>\` to inspect.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const exitCode = result.summary.skippedLocallyModified > 0 ? 1 : 0;
|
|
process.exit(exitCode);
|
|
} catch (err) {
|
|
if (err instanceof InstallError || err instanceof BundleError) {
|
|
if (flags.json) {
|
|
console.log(
|
|
JSON.stringify(
|
|
{ ok: false, error: (err as Error & { code?: string }).code, message: err.message },
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} else {
|
|
console.error(`skillpack install: ${err.message}`);
|
|
}
|
|
process.exit(err instanceof InstallError && err.code === 'lock_held' ? 2 : 2);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// diff
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const HELP_DIFF = `gbrain skillpack diff <name> [options]
|
|
|
|
Show per-file diff status between the bundle and the target workspace
|
|
for one skill. Read-only; no writes.
|
|
|
|
Options:
|
|
--skills-dir PATH Override target skills directory.
|
|
--json Machine-readable envelope.
|
|
--help Show this message.
|
|
|
|
Exit codes:
|
|
0 every file matches the bundle
|
|
1 at least one file differs (or is missing)
|
|
`;
|
|
|
|
async function runDiff(args: string[]): Promise<void> {
|
|
const help = args.includes('--help') || args.includes('-h');
|
|
if (help) {
|
|
console.log(HELP_DIFF);
|
|
process.exit(0);
|
|
}
|
|
const json = args.includes('--json');
|
|
let targetSkillsDir: string | null = null;
|
|
let skillName: string | null = null;
|
|
for (let i = 0; i < args.length; i++) {
|
|
const a = args[i];
|
|
if (a === '--skills-dir') {
|
|
targetSkillsDir = args[i + 1] ? resolveAbs(args[i + 1]) : null;
|
|
i++;
|
|
} else if (a?.startsWith('--skills-dir=')) {
|
|
targetSkillsDir = resolveAbs(a.slice('--skills-dir='.length));
|
|
} else if (a && !a.startsWith('--') && !skillName) {
|
|
skillName = a;
|
|
}
|
|
}
|
|
if (!skillName) {
|
|
console.error('Error: pass a skill name.\n');
|
|
console.error(HELP_DIFF);
|
|
process.exit(2);
|
|
}
|
|
const gbrainRoot = findGbrainRoot();
|
|
if (!gbrainRoot) {
|
|
console.error('Error: could not find gbrain repo root.');
|
|
process.exit(2);
|
|
}
|
|
if (!targetSkillsDir) {
|
|
const detected = autoDetectSkillsDir();
|
|
if (detected.dir) targetSkillsDir = detected.dir;
|
|
}
|
|
if (!targetSkillsDir) {
|
|
console.error('Error: pass --skills-dir or set $OPENCLAW_WORKSPACE.');
|
|
process.exit(2);
|
|
}
|
|
|
|
try {
|
|
const diffs = diffSkill(gbrainRoot, skillName, targetSkillsDir);
|
|
const clean = diffs.every(d => d.identical && d.existing);
|
|
if (json) {
|
|
console.log(JSON.stringify({ ok: clean, skillName, diffs }, null, 2));
|
|
} else {
|
|
console.log(`skillpack diff ${skillName} → ${targetSkillsDir}`);
|
|
for (const d of diffs) {
|
|
let tag: string;
|
|
if (!d.existing) tag = 'missing ';
|
|
else if (d.identical) tag = 'identical';
|
|
else tag = 'differs ';
|
|
console.log(` ${tag} ${d.target} (src ${d.sourceBytes}B / tgt ${d.targetBytes}B)`);
|
|
}
|
|
console.log(clean ? '\n✓ all files match the bundle.' : '\n(run `gbrain skillpack install <name> --overwrite-local` to replace local-modified files.)');
|
|
}
|
|
process.exit(clean ? 0 : 1);
|
|
} catch (err) {
|
|
if (err instanceof BundleError) {
|
|
if (json) {
|
|
console.log(JSON.stringify({ ok: false, error: err.code, message: err.message }, null, 2));
|
|
} else {
|
|
console.error(`skillpack diff: ${err.message}`);
|
|
}
|
|
process.exit(2);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|