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>
193 lines
6.4 KiB
TypeScript
193 lines
6.4 KiB
TypeScript
/**
|
|
* skillify/generator.ts — pure file-tree generator for `gbrain skillify scaffold`.
|
|
*
|
|
* Takes a scaffold spec + target skillsDir and returns the list of
|
|
* files that would be written (dry-run) or writes them (apply).
|
|
*
|
|
* Idempotency contract (D-CX-7): `--force` regenerates STUB files
|
|
* but NEVER re-appends resolver rows if a row for this skill path
|
|
* already exists. The resolver row append is idempotent by content.
|
|
*/
|
|
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
import { dirname, join } from 'path';
|
|
|
|
import {
|
|
resolverRow,
|
|
routingEvalTemplate,
|
|
scriptTemplate,
|
|
skillMdTemplate,
|
|
testTemplate,
|
|
type ScaffoldVars,
|
|
} from './templates.ts';
|
|
import { findResolverFile, RESOLVER_FILENAMES_LABEL } from '../resolver-filenames.ts';
|
|
|
|
export interface ScaffoldPlan {
|
|
files: Array<{ path: string; kind: 'new' | 'overwrite' | 'append'; content: string }>;
|
|
resolverFile: string | null;
|
|
resolverAppend: string | null; // null when row already present (idempotent)
|
|
}
|
|
|
|
export interface ScaffoldOptions {
|
|
/** Absolute path to the target `skills/` dir. */
|
|
skillsDir: string;
|
|
/** Scaffold variables (name, description, triggers, etc.). */
|
|
vars: ScaffoldVars;
|
|
/**
|
|
* Repo root for the `test/` and `scripts/` directories. Falls back
|
|
* to `dirname(skillsDir)` when unset. Tests pass explicit values.
|
|
*/
|
|
repoRoot?: string;
|
|
/** When true, overwrite existing skill files. Per-file (D-CX-7). */
|
|
force?: boolean;
|
|
}
|
|
|
|
const SKILL_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
|
export class SkillifyScaffoldError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public code:
|
|
| 'invalid_name'
|
|
| 'exists'
|
|
| 'no_resolver'
|
|
| 'write_failed',
|
|
) {
|
|
super(message);
|
|
this.name = 'SkillifyScaffoldError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build the list of files + the resolver-append string without doing
|
|
* any I/O that writes. Callers can preview via dry-run, then pass the
|
|
* same inputs to `applyScaffold`.
|
|
*/
|
|
export function planScaffold(opts: ScaffoldOptions): ScaffoldPlan {
|
|
const { vars, skillsDir } = opts;
|
|
if (!SKILL_NAME_PATTERN.test(vars.name)) {
|
|
throw new SkillifyScaffoldError(
|
|
`'${vars.name}' is not a valid skill name. Must be lowercase-kebab-case (examples: webhook-verify, context-now).`,
|
|
'invalid_name',
|
|
);
|
|
}
|
|
|
|
const repoRoot = opts.repoRoot ?? dirname(skillsDir);
|
|
const skillDir = join(skillsDir, vars.name);
|
|
const skillMdPath = join(skillDir, 'SKILL.md');
|
|
const scriptPath = join(skillDir, 'scripts', `${vars.name}.mjs`);
|
|
const routingEvalPath = join(skillDir, 'routing-eval.jsonl');
|
|
const testPath = join(repoRoot, 'test', `${vars.name}.test.ts`);
|
|
|
|
const files: ScaffoldPlan['files'] = [];
|
|
|
|
const want = (path: string, content: string) => {
|
|
if (existsSync(path)) {
|
|
if (!opts.force) {
|
|
throw new SkillifyScaffoldError(
|
|
`'${path}' already exists. Pass --force to regenerate stubs (destructive to any local edits), or edit the file directly.`,
|
|
'exists',
|
|
);
|
|
}
|
|
files.push({ path, kind: 'overwrite', content });
|
|
} else {
|
|
files.push({ path, kind: 'new', content });
|
|
}
|
|
};
|
|
|
|
want(skillMdPath, skillMdTemplate(vars));
|
|
want(scriptPath, scriptTemplate(vars));
|
|
want(routingEvalPath, routingEvalTemplate(vars));
|
|
want(testPath, testTemplate(vars));
|
|
|
|
// Resolver row — append to whichever file exists; `null` both fields
|
|
// if no resolver exists (caller handles setup error).
|
|
const resolverFile =
|
|
findResolverFile(skillsDir) ?? findResolverFile(dirname(skillsDir));
|
|
let resolverAppend: string | null = null;
|
|
if (resolverFile) {
|
|
const existingRow = detectExistingResolverRow(resolverFile, vars.name);
|
|
if (!existingRow) {
|
|
resolverAppend = buildResolverAppend(resolverFile, vars);
|
|
}
|
|
}
|
|
|
|
return { files, resolverFile, resolverAppend };
|
|
}
|
|
|
|
/**
|
|
* Check whether the resolver already references `skills/<name>/SKILL.md`
|
|
* in ANY form: backticked (`skills/foo/SKILL.md`), single-quoted
|
|
* ('skills/foo/SKILL.md'), double-quoted ("skills/foo/SKILL.md"), or
|
|
* bare (skills/foo/SKILL.md surrounded by non-word chars).
|
|
*
|
|
* Idempotency contract — if any form is present, we never re-append a
|
|
* row for this skill, even with --force. This is broader than the
|
|
* original backtick-only match: users who hand-edit the resolver to
|
|
* normalize formatting (drop backticks, use quotes, etc.) should not
|
|
* cause duplicate rows on the next scaffold --force.
|
|
*/
|
|
function detectExistingResolverRow(resolverFile: string, name: string): boolean {
|
|
let content: string;
|
|
try {
|
|
content = readFileSync(resolverFile, 'utf-8');
|
|
} catch {
|
|
return false;
|
|
}
|
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
// Match the path with any common delimiter on either side: backtick,
|
|
// single quote, double quote, parenthesis, whitespace, start/end of
|
|
// line. The `(?:^|...)` and `(?:$|...)` anchors ensure we don't
|
|
// false-match on something like "skills/foo-bar/SKILL.md" when
|
|
// looking for "foo".
|
|
const re = new RegExp(
|
|
`(?:^|[\`'"\\s\\(\\[])skills\\/${escaped}\\/SKILL\\.md(?:[\`'"\\s\\)\\]]|$)`,
|
|
'm',
|
|
);
|
|
return re.test(content);
|
|
}
|
|
|
|
function buildResolverAppend(resolverFile: string, vars: ScaffoldVars): string {
|
|
// Append under a `## Uncategorized` section. If the section already
|
|
// exists, just add the row; otherwise create the section.
|
|
let content: string;
|
|
try {
|
|
content = readFileSync(resolverFile, 'utf-8');
|
|
} catch {
|
|
content = '';
|
|
}
|
|
|
|
const row = resolverRow(vars);
|
|
const hasUncategorized = /^## Uncategorized\s*$/m.test(content);
|
|
if (hasUncategorized) {
|
|
return '\n' + row + '\n';
|
|
}
|
|
const needsLeadingNewline = content.endsWith('\n') ? '' : '\n';
|
|
return (
|
|
needsLeadingNewline +
|
|
'\n## Uncategorized\n\n| Trigger | Skill |\n|---------|-------|\n' +
|
|
row +
|
|
'\n'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Apply a previously-computed ScaffoldPlan. I/O only — no planning.
|
|
* Callers that want dry-run behavior should skip this call entirely
|
|
* and just render the plan.
|
|
*/
|
|
export function applyScaffold(plan: ScaffoldPlan): void {
|
|
for (const f of plan.files) {
|
|
mkdirSync(dirname(f.path), { recursive: true });
|
|
writeFileSync(f.path, f.content);
|
|
}
|
|
if (plan.resolverFile && plan.resolverAppend !== null) {
|
|
const current = existsSync(plan.resolverFile)
|
|
? readFileSync(plan.resolverFile, 'utf-8')
|
|
: '';
|
|
writeFileSync(plan.resolverFile, current + plan.resolverAppend);
|
|
}
|
|
}
|
|
|
|
export { SKILL_NAME_PATTERN };
|