mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 11:22:34 +00:00
* v0.37.1.0 feat: skill_brain_first doctor check + auto-fix + declarative opt-out Cathedral wave superseding PR #1206. Doctor now scans every SKILL.md for external-lookup tools (web_search / web_fetch / exa / perplexity / happenstance / crustdata / captain_api / firecrawl) and warns when the skill has no brain- first compliance signal. gbrain doctor --fix auto-inserts the canonical > **Convention:** see [conventions/brain-first.md](...) callout via the dry-fix.ts MISSING_RULE_PATTERNS extension (sharing safety gates with the existing REPLACE patterns). Motivated by the 2026-05-19 tweet-shield incident: cross-modal eval flagged Garry's Palantir tweet as risky because no model knew he built it, but the brain already had "designed the entire Finance product UI" and "150+ PSDs from April-December 2006." Static check catches authorship; v0.37+ runtime gate (filed in TODOS.md) closes the dispatch side. Key design decisions locked via /plan-eng-review + codex outside-voice review: - A1: frontmatter ships only brain_first: exempt (no required/n/a enum) - A2: snapshot+diff audit at ~/.gbrain/audit/skill-brain-first-YYYY-Www.jsonl with transition-only writes (stable brains = 0 lines/run) - A3: scaffold template pre-inserts callout; skillify check fails (exit 1) on external + no callout + no exempt - A4: position-relative gate is BODY-ONLY (frontmatter tools: [web_search] declaration doesn't false-flag the skill) - Q1: single pure analyzeSkillBrainFirst() helper consumed by 3 surfaces - CMT1: no upgrade migration — doctor surfaces hint, --fix applies via dry-fix safety gates (user stays in loop) - CMT2: dropped tools+writes_pages auto-exemption (was hiding mixed-class skills like idea-ingest/meeting-ingestion/data-research) Trio: VERSION + package.json + CHANGELOG aligned at 0.37.1.0. 56 unit cases + 12 E2E cases pass. 170 related existing tests pass unchanged. Self-dogfood: gbrain doctor against this repo's skills/ reports skill_brain_first: ok across 43 skills (compliant or exempt). functional-area-resolver and strategic-reading skills gained brain_first: exempt to validate the declarative opt-out in production code (both name perplexity in dispatcher prose without calling it). Co-Authored-By: garrytan-agents <noreply@github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: update CLAUDE.md for v0.37.1.0 skill_brain_first wave Added Key Files entries for the four new modules: - src/core/skill-frontmatter.ts (shared parser) - src/core/skill-brain-first.ts (analyzer + FORMERLY_HARDCODED_EXEMPT) - src/core/skill-fix-gates.ts (extracted safety primitives) - src/core/audit-skill-brain-first.ts (snapshot+diff JSONL) Extended existing entries: - src/core/filing-audit.ts: rewired to shared parser - src/core/dry-fix.ts: MISSING_RULE_PATTERNS INSERT pattern type - src/commands/doctor.ts: skill_brain_first check + tweet-shield framing - src/commands/skillify-check.ts: required item 12 + scaffold pre-insert Added test inventory entries: - test/skill-brain-first.test.ts (56 unit cases) - test/e2e/skill-brain-first.test.ts (12 E2E cases) Regenerated llms-full.txt via bun run build:llms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): skill_brain_first guard uses doctor --fast to skip engine connect CI run #76881161092 failed because scripts/check-skill-brain-first.sh invoked plain `gbrain doctor --json`, which routes through connectEngine(). With no ~/.gbrain/config.json present (CI's case — runner is bun-only, no brain init), connectEngine() exits 1 with "No brain configured." and emits zero stdout. The python parser sees an empty file and returns parse_error, failing the verify gate. Fix: pass --fast to doctor. --fast routes through runDoctor(null, ...) which runs the filesystem-only check set (resolver_health, skill_conformance, skill_brain_first) and emits the standard single-line JSON envelope the parser expects. skill_brain_first is filesystem-only by design (scans SKILL.md, no DB touch), so --fast is the correct knob, not a workaround. Verified by reproducing the CI failure mode locally with GBRAIN_HOME=/tmp/empty-... — gate now passes both with and without a configured brain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rebump v0.37.1.0 → v0.37.3.0 (queue collision with #1214) PR #1214 (brainstorm + lsd) claimed v0.37.1.0 concurrently with #1215. Skipping 0.37.2.0 leaves a buffer for #1214's adjacent slot. Trio (VERSION + package.json + CHANGELOG header + inline "To take advantage of v0.37.3.0" block) aligned at 0.37.3.0. No behavior changes — version metadata only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: garrytan-agents <noreply@github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
4.2 KiB
TypeScript
100 lines
4.2 KiB
TypeScript
/**
|
|
* skill-fix-gates.ts — Shared safety primitives consumed by every auto-fix
|
|
* pattern in `dry-fix.ts` (F8 from /plan-eng-review).
|
|
*
|
|
* Pre-v0.36.x the dry-fix module had REPLACE-in-place semantics only
|
|
* (`CROSS_CUTTING_PATTERNS`). v0.36.x adds INSERT-missing-rule semantics
|
|
* for the brain-first auto-add (`MISSING_RULE_PATTERNS`). Both share the
|
|
* exact same safety gates:
|
|
*
|
|
* - **working-tree check** — refuse writes when the file has uncommitted
|
|
* changes (the git-is-backup contract).
|
|
* - **not-a-repo check** — refuse writes when the file isn't under git
|
|
* (writing would destroy the only copy with no rollback).
|
|
* - **inside-code-fence check** — don't mangle example prose inside
|
|
* ``` fences.
|
|
*
|
|
* Extracting these as a sibling module is cleaner than duplicating across
|
|
* REPLACE and INSERT pattern handlers in `dry-fix.ts`. Tests import from
|
|
* here directly; `dry-fix.ts` re-exports for back-compat with callers that
|
|
* imported these functions from there pre-v0.36.x.
|
|
*
|
|
* The D6 install-path safety gate (refuse `--fix` when skills dir came
|
|
* from the install-path fallback) lives in `doctor.ts` because it's
|
|
* specific to the doctor `--fix` flow and consults the
|
|
* `autoDetectSkillsDirReadOnly` `detected.source` field that doesn't
|
|
* appear in dry-fix's own argument surface.
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { dirname } from 'node:path';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Code-fence guard
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* True when the byte offset sits inside a fenced code block (``` ... ```).
|
|
* Counts triple-backtick fences at line starts before `offset`. Odd count
|
|
* = inside a fence; even count = outside.
|
|
*
|
|
* Used to skip pattern matches that fall inside example prose so the auto-
|
|
* fix doesn't mangle code samples in skill documentation.
|
|
*/
|
|
export function isInsideCodeFence(content: string, offset: number): boolean {
|
|
const before = content.slice(0, offset);
|
|
const fenceRe = /^```/gm;
|
|
const fenceCount = (before.match(fenceRe) || []).length;
|
|
return fenceCount % 2 === 1;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Working-tree status (git check)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type WorkingTreeStatus = 'clean' | 'dirty' | 'not_a_repo';
|
|
|
|
/**
|
|
* Check the git state of a skill file.
|
|
*
|
|
* Three outcomes are deliberately distinct:
|
|
* - `clean` — file is tracked and has no uncommitted changes.
|
|
* Safe to write; `git diff` after the write surfaces the change.
|
|
* - `dirty` — file has uncommitted changes. Refuse to write — auto-fix
|
|
* would mix its changes with the user's mid-edit work, and
|
|
* `git stash` / `git checkout -- <file>` wouldn't cleanly
|
|
* separate them.
|
|
* - `not_a_repo` — file isn't under git at all. Refuse to write — there
|
|
* is NO rollback path. The auto-fix contract is "git is
|
|
* the backup," and writing here breaks the contract.
|
|
*
|
|
* `execFileSync` with array args bypasses the shell entirely so paths
|
|
* with odd characters from a manifest can't inject commands. We change
|
|
* to the file's parent dir so `git status --porcelain -- <path>`
|
|
* resolves correctly even when the calling process's cwd is elsewhere.
|
|
*/
|
|
export function getWorkingTreeStatus(skillPath: string): WorkingTreeStatus {
|
|
try {
|
|
const out = execFileSync('git', ['status', '--porcelain', '--', skillPath], {
|
|
encoding: 'utf-8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
cwd: dirname(skillPath),
|
|
});
|
|
return out.trim().length > 0 ? 'dirty' : 'clean';
|
|
} catch {
|
|
// git exits 128 when not inside a repo; treat any non-zero the same.
|
|
return 'not_a_repo';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Legacy wrapper. Callers that need to distinguish `not_a_repo` from
|
|
* `clean` should use `getWorkingTreeStatus()` directly. The two-state
|
|
* boolean here is preserved for the existing dry-fix call site that
|
|
* was previously content with a coarse "dirty vs everything else"
|
|
* check.
|
|
*/
|
|
export function isWorkingTreeDirty(skillPath: string): boolean {
|
|
return getWorkingTreeStatus(skillPath) === 'dirty';
|
|
}
|