mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
d838d4792b3807e2bb468b39a1ff6a43dc47ab21
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e3f704229b |
v0.20.2 feat: gbrain jobs supervisor — self-healing worker process manager (#364)
* feat: add `gbrain jobs supervisor` — self-healing worker process manager Adds a first-class supervisor command that: - Spawns `gbrain jobs work` as a child process - Restarts on crash with exponential backoff (1s→60s cap) - Resets crash counter after 5min of stable operation - PID file locking prevents duplicate supervisors - Periodic health checks (stalled jobs, completion gaps) - Graceful shutdown (SIGTERM→35s→SIGKILL) Usage: gbrain jobs supervisor --concurrency 4 Replaces ad-hoc nohup patterns in bootstrap scripts. The autopilot command's internal supervisor can be migrated to use this in a follow-up. Tests: 7 pass (backoff calc, PID management, crash tracking) * supervisor: atomic PID lock, queue-scoped health, env safety, unified exit Lane A of PR #364 review fixes (20-item multi-lane plan). Addresses the codex-tier + CEO + Eng findings on src/core/minions/supervisor.ts: Safety + correctness: - Atomic O_CREAT|O_EXCL PID lock via openSync('wx') with stale-file liveness check. Prevents two supervisors racing on the same PID file. (codex #1) - Health check now queries status='active' AND lock_until < now() matching queue.ts:848's authoritative stalled definition. The prior `status = 'stalled'` predicate returned zero rows forever because 'stalled' is not a persisted value in the schema. (codex #2) - All health queries scoped to WHERE queue = $1 via opts.queue binding. Multi-queue installs no longer see cross-queue false positives. (codex #3) - Class default allowShellJobs flipped true→false AND explicit `delete env.GBRAIN_ALLOW_SHELL_JOBS` when false, so child workers don't silently inherit the var from the parent shell. (eng #8, codex #9) - Unified shutdown(reason, exitCode) — max-crashes now routes through the same drain path as SIGTERM. Single source of truth for lifecycle cleanup; prerequisite for trustworthy audit events (Lane C). (eng #1) - Default PID path moves from /tmp to ~/.gbrain/supervisor.pid with mkdirSync recursive + GBRAIN_SUPERVISOR_PID_FILE env override. Matches the rest of the product's ~/.gbrain/ convention; fresh installs no longer hit ENOENT. (CEO #2 + codex #6) Refinements: - crashCount = 1 after 5-min stable-run reset (was 0, produced calculateBackoffMs(-1) = 500ms by accident). Now reads as 'first crash of a new cycle' with a clean 1s backoff. (Nit 1) - Top-of-file POSTGRES-ONLY docstring documenting why the supervisor can't run against PGLite. (Nit 2) - inBackoff flag suppresses 'worker not alive' warn during the expected null-child window (crash → sleep → next spawn). (eng #2) - Tracked listener refs for SIGTERM/SIGINT removed in shutdown() so integration tests spinning up/tearing down multiple supervisors on one process don't leak handlers. (eng #3) - Single FILTER query replaces two SELECT counts — one round-trip instead of two, three metrics in one pass. (eng #10) - child.on('error') listener emits worker_spawn_failed event for ENOENT/EACCES; exit handler still increments crashCount as usual so max-crashes bounds permanent misconfigurations. (codex #7) - healthInFlight boolean guard with try/finally prevents overlapping health checks from stacking on a hung DB. (codex #8) Documented exit codes (ExitCodes const): 0 CLEAN, 1 MAX_CRASHES, 2 LOCK_HELD, 3 PID_UNWRITABLE Agent can branch on exit=2 ('another supervisor, I'm fine') vs exit=1 ('escalate to human'). Event emitter surface: - started / worker_spawned / worker_exited / worker_spawn_failed - backoff / health_warn / health_error / max_crashes_exceeded - shutting_down / stopped Plumbed through emit() with an onEvent callback hook for Lane C's audit writer. json:false is the default; Lane C's --json mode flips it and writes JSONL to stderr. CLI changes (src/commands/jobs.ts): - `gbrain jobs supervisor` gains --allow-shell-jobs (explicit opt-in mirroring the env-var gate), --cli-path (override auto-resolution for exotic setups), and --json (JSONL lifecycle events on stderr). - Expanded --help body with description, 3 examples, and exit-code table. (DX Fix A per review) - Three-tier PID path resolution: --pid-file > GBRAIN_SUPERVISOR_PID_FILE > ~/.gbrain/supervisor.pid (via exported DEFAULT_PID_FILE). - Removed the catch-fallback to process.argv[1] — resolveGbrainCliPath() throws its own actionable install-hint error, which is what dev users need instead of a cryptic spawn failure on a .ts path. (codex #5) Tests: existing 7 supervisor.test.ts cases continue to pass. Integration tests (crash-restart, max-crashes, SIGTERM-during-backoff, env-inheritance regression) land in Lane E. Out of scope for this lane (tracked in follow-up lanes): - Audit file writer at ~/.gbrain/audit/supervisor-YYYY-Www.jsonl (Lane C) - Documentation pass (Lane B) - supervisor start/status/stop subcommands (Lane C) - gbrain doctor supervisor check (Lane D) - /ship release hygiene (Lane F) - autopilot.ts migration to MinionSupervisor (deferred to follow-up PR per codex — requires non-blocking start() API redesign, not ~30 lines) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: supervisor as canonical worker deployment pattern Lane B of PR #364 review fixes. Reframes docs/guides/minions-deployment.md around `gbrain jobs supervisor` as the default answer (blocker 7), deletes the 68-line legacy bash watchdog (F10), and updates README + deployment snippets to match. docs/guides/minions-deployment.md: - New 'Worker supervision' section at the top with the canonical 3-command agent pattern (start --detach / status --json / stop) and a documented exit-code table (0 clean, 1 max-crashes, 2 lock-held, 3 PID-unwritable). - 'Which supervisor when?' decision table: container = supervisor as PID 1, Linux VM = systemd-over-supervisor, dev laptop = bare terminal. - New 'Agent usage' section for OpenClaw / Hermes / Cursor / Codex — the 3-turn discover-start-maintain workflow that replaces shell archaeology with machine-parseable JSON events + an audit file at ~/.gbrain/audit/supervisor-YYYY-Www.jsonl. - Demoted the 'Option 1: watchdog cron' path entirely; replaced with a straightforward upgrade migration block (stop script, remove cron line, start supervisor, verify via doctor). - Preconditions now check Postgres connectivity directly (supervisor is Postgres-only; the CLI rejects PGLite with a clear error). Snippets: - systemd.service: ExecStart now invokes `gbrain jobs supervisor` instead of raw `gbrain jobs work`. Two-layer supervision (systemd → supervisor → worker) buys automatic restart on reboot plus fast crash recovery. ReadWritePaths expanded to cover $HOME/.gbrain (supervisor PID + audit). - Procfile + fly.toml.partial: same change — platform restarts the container on host events, supervisor restarts the worker on crashes. - minion-watchdog.sh: deleted (git history retains it for anyone in an exotic deployment). Supervisor subsumes every capability it had plus atomic PID locking, structured audit events, queue-scoped health checks, and graceful drain on SIGTERM. README.md: - Added a paragraph under the Minions section pointing `gbrain jobs supervisor` as canonical, noting the --detach / status / stop surface and the audit file path, with a link to the full deployment guide. Kept `gbrain jobs work` documented for direct raw invocation but flagged 'prefer supervisor' for any long-running use. The supervisor `--help` body itself (3 examples + exit-code table in src/commands/jobs.ts) landed with Lane A — this lane finishes the discoverability story by making the supervisor findable via doc grep, README landing, and deployment-guide landing paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * supervisor: daemon-manager subcommands + JSONL audit writer Lane C of PR #364 review fixes. Adds the daemon-manager CLI surface so agents can drive `gbrain jobs supervisor` in 3 turns instead of 10, and the audit writer that makes lifecycle events inspectable across process restarts. (Blocker 8, closes DX Fix A/B/C.) New: src/core/minions/handlers/supervisor-audit.ts - writeSupervisorEvent(emission, supervisorPid) appends JSONL to `${GBRAIN_AUDIT_DIR:-~/.gbrain/audit}/supervisor-YYYY-Www.jsonl`. ISO-week rotation via a `computeSupervisorAuditFilename()` helper that mirrors `shell-audit.ts` exactly (year-boundary ISO week math, Thursday anchor, etc). - readSupervisorEvents({sinceMs}) returns parsed events from the current week's file, oldest-first, for Lane D's doctor check. Malformed lines are skipped silently (disk-full truncation is already best-effort at write time). - Reuses `resolveAuditDir()` from shell-audit.ts so the `GBRAIN_AUDIT_DIR` env var override works identically across all gbrain audit trails. src/commands/jobs.ts: supervisor subcommand dispatcher - `gbrain jobs supervisor [start] [--detach] [--json] ...` — default subcommand. Without --detach, runs foreground as before. With --detach, forks a background child (inheriting stderr so the caller can still tail JSONL events), writes a stdout payload: {"event":"started","supervisor_pid":N,"pid_file":"...","detached":true} and exits 0. Stdin/stdout on the detached child are /dev/null so the parent shell isn't held open. - `gbrain jobs supervisor status [--json]` — reads the PID file, checks liveness via `kill -0`, then reads the last 24h from the supervisor audit file to compute crashes_24h / last_start / max_crashes_exceeded. Exits 0 if running, 1 if not. JSON output is machine-parseable; human output is a 5-line ASCII report. - `gbrain jobs supervisor stop [--json]` — reads PID, sends SIGTERM, polls `kill -0` every 250ms for up to 40s (supervisor's own 35s worker-drain + 5s slack). Reports outcome: drained / timeout_40s / pid_file_missing / pid_file_corrupt / process_gone. Exit 0 on clean stop. - `--json` flag is already plumbed through to the supervisor opts from Lane A — this lane adds the onEvent audit-writer callback so every supervisor emission (started, worker_spawned, worker_exited, worker_spawn_failed, backoff, health_warn, health_error, max_crashes_exceeded, shutting_down, stopped) lands in the JSONL file with the supervisor's PID attached. --help body updated: - Three separate usage lines (start / status / stop). - SUBCOMMANDS block with one-line summaries each. - EXIT CODES block (unchanged from Lane A, moved under SUBCOMMANDS). - EXAMPLES block updated with status --json + stop + --detach forms. Tests: existing 127 supervisor + minions tests continue to pass. Integration tests for the new subcommands + audit writer land with Lane E. Follow-up (Lane D): `gbrain doctor` will read readSupervisorEvents() from this module to surface a `supervisor` health check alongside its existing checks (DB connectivity, schema version, queue health). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * doctor: add supervisor health check Lane D of PR #364 review fixes. Closes the observability loop: now that Lane C writes supervisor lifecycle events to `${GBRAIN_AUDIT_DIR:-~/.gbrain/audit}/supervisor-YYYY-Www.jsonl`, `gbrain doctor` surfaces a `supervisor` check alongside its existing health indicators. Implementation (src/commands/doctor.ts, filesystem-only block 3b-bis): - Resolves DEFAULT_PID_FILE via the same three-tier logic as the start path (--pid-file > GBRAIN_SUPERVISOR_PID_FILE > ~/.gbrain/supervisor.pid). - Reads the PID file + `kill -0 <pid>` for liveness. - Calls readSupervisorEvents({sinceMs: 24h}) from the audit module to derive last_start / crashes_24h / max_crashes_exceeded. - Suppresses the check entirely when the user has never invoked the supervisor (no PID file AND no audit events) — avoids noise on installs that don't use the feature. Status thresholds: fail max_crashes_exceeded event seen in last 24h (supervisor gave up; operator needs to restart or triage) warn supervisor not running but audit shows prior use (unexpected stop — likely crash or manual kill) warn running but > 3 crashes in last 24h (supervisor recovering but worker is unstable) ok running + ≤ 3 crashes + no max_crashes event All failure paths emit a paste-ready recovery command. Read/import errors are swallowed (best-effort like the other doctor checks). Tests: all 127 supervisor + minions tests still green; 13 existing doctor tests unaffected. F3 done. All four lanes A/B/C/D are now committed; Lane E (integration tests) and Lane F (/ship v0.20.2) remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: 4 critical integration tests for supervisor lifecycle Lane E of PR #364 review fixes (blocker 10). Fills the ~15% coverage gap flagged in the eng review by actually exercising the code paths that will break in production — crash-restart loop, max-crashes exit, SIGTERM-during-backoff, env-var inheritance — via real spawn() calls against fake shell-script workers. No mocks: real fork, real signals, real env propagation, real audit file writes. test/fixtures/supervisor-runner.ts (new, 55 lines): A standalone bun script that constructs a MinionSupervisor from env vars (SUP_PID_FILE / SUP_CLI_PATH / SUP_MAX_CRASHES / SUP_BACKOFF_FLOOR_MS / SUP_HEALTH_INTERVAL_MS / SUP_ALLOW_SHELL_JOBS / SUP_AUDIT_DIR) and calls start(). Mock engine returns empty rows for executeRaw (health check path still exercised without Postgres). Tests spawn this as a subprocess because MinionSupervisor.start() calls process.exit() on shutdown — can't run it in the test runner's own process. test/supervisor.test.ts (existing; 91 → 300 lines): - Added IntegrationHarness helper: creates a unique tmpdir per test, a fake worker shell script, a PID-file path, and an audit-dir path; cleanup runs in finally. - spawnSupervisor() forks bun on the runner with env vars set. - readAudit() reads the supervisor-YYYY-Www.jsonl file via the existing readSupervisorEvents() helper (Lane C), threading GBRAIN_AUDIT_DIR through so tests don't collide on ~/.gbrain. - waitFor(pred, timeoutMs) polls helper for event-driven tests. Four integration tests (with _backoffFloorMs=5 for <1s suite runs): 1. "respawns the worker after a crash and eventually exits with max-crashes code=1" Worker always `exit 1`. maxCrashes=3. Asserts: exit code 1, PID file cleaned up, audit contains started + 3x worker_spawned + 3x worker_exited + max_crashes_exceeded + shutting_down + stopped, and the stopped event carries {reason:'max_crashes', exit_code:1}. Locks in blockers 1 (PID lock), 2+3+6 (health SQL doesn't 500), 5 (unified shutdown emits right events), F8 (spawn errors counted). 2. "receives SIGTERM while sleeping between crashes and exits 0 cleanly" Worker always `exit 1`, backoff floor 800ms to catch the sleep. Asserts: SIGTERM during backoff → exit code 0 (not 1) in <5s, no signal kill (process.exit via shutdown), audit contains shutting_down {reason:'SIGTERM'} + stopped, PID file cleaned up. Locks in eng Issue 1 (unified exit path), eng Issue 3 (signal handlers don't accumulate across shutdowns). 3. "strips inherited GBRAIN_ALLOW_SHELL_JOBS when allowShellJobs=false, even if parent has it set" ⚠ CRITICAL regression test Parent env has GBRAIN_ALLOW_SHELL_JOBS=1. SUP_ALLOW_SHELL_JOBS=0. Worker writes $GBRAIN_ALLOW_SHELL_JOBS (or 'UNSET' if absent) to an OUT_FILE. Asserts child sees 'UNSET'. Locks in codex #9 + eng #8: the `else delete env.GBRAIN_ALLOW_SHELL_JOBS` branch from Lane A is load-bearing for the supervisor's security posture; this test prevents a future refactor silently re-opening the inheritance hole. 4. "DOES pass GBRAIN_ALLOW_SHELL_JOBS to child when allowShellJobs=true" Positive-path companion to #3. SUP_ALLOW_SHELL_JOBS=1 → worker sees '1'. Confirms the else-branch doesn't over-strip and that operators who explicitly opt in still get shell-exec enabled. Plus two audit-format unit tests: - computeSupervisorAuditFilename format (regex match) - Year-boundary ISO week: 2027-01-01 → supervisor-2026-W53.jsonl (matches the shell-audit.ts pattern exactly) Before: 7 tests covering backoff math + PID helpers (~15% behavioral coverage per eng review). After: 13 tests across all critical lifecycle paths (crash-restart, max-crashes, SIGTERM, env-inheritance, audit rotation). All 146 tests in supervisor + minions + doctor suites green in ~8s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.20.2) Lane F of PR #364 review fixes. Closes the multi-lane plan with release hygiene: VERSION bump 0.19.0 → 0.20.2, package.json sync, CHANGELOG entry in GStack voice with release summary + "numbers that matter" table + "To take advantage of v0.20.2" migration block + itemized changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: escape template-literal interpolation in supervisor --help The --help body in src/commands/jobs.ts is one big backtick template literal. The supervisor subcommand description I added in Lane B used both `${GBRAIN_AUDIT_DIR:-~/.gbrain/audit}` (parsed as a template interpolation into an undefined variable) and inline `code` backticks (parsed as nested template literals). CI caught it with ~200 tsc parse errors across the file. Fix: - Escape `${...}` → `\${...}` so the audit-file path renders literally. - Replace prose inline-code backticks with plain single-quote fences (`gbrain jobs work` → 'gbrain jobs work', `~/.gbrain/supervisor.pid` → ~/.gbrain/supervisor.pid). `--help` output is human prose; the single-quote form reads cleanly in a terminal without needing to smuggle nested backticks through a template literal. `bunx tsc --noEmit` is clean. 146 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate llms-full.txt after Lane B doc rewrite CI drift guard caught that `llms-full.txt` didn't match the current generator output. Root cause: the Lane B rewrite of `docs/guides/minions-deployment.md` (supervisor as canonical, watchdog deleted) changed content that gets inlined into `llms-full.txt`, but I didn't run `bun run build:llms` to regenerate. `bun test test/build-llms.test.ts` now clean (7/7 pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: root <root@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
78ba0b5b53 |
v0.19.0 check-resolvable: add OpenClaw skills-dir fallback + docs/tests (#326)
* Add OpenClaw skills fallback for check-resolvable * feat: v0.17.0 foundation — errors/warnings split + AGENTS.md support + auto-manifest First two workstreams of the v0.17.0 "skillify end-to-end" release. Landed together because the D-CX-3 exit-code refactor is a prerequisite for W1's warning-surfaced filing audit in Workstream 3. ## D-CX-3: split ResolvableReport into errors[] + warnings[] + --strict Prior: `env.ok = report.issues.length === 0` treated warnings and errors identically for exit status. Any warning forced exit 1, which meant the planned filing-audit (W3) would break CI for every OpenClaw deployment emitting advisory warnings. New contract: - `ResolvableReport.errors[]` and `warnings[]` as separate arrays. - `issues[]` stays as deprecated backcompat union (remove in v0.18). - Default: exit 0 unless any errors. Warnings are advisory. - `--strict` flag promotes warnings to fail CI (explicit opt-in). Files: src/core/check-resolvable.ts, src/commands/check-resolvable.ts (added --strict flag + help text + header doc), src/commands/doctor.ts (use new fields), test/check-resolvable-cli.test.ts (rewrite REGRESSION-GATE to document the new contract, add 3 D-CX-3 cases). ## W1: AGENTS.md support + auto-manifest + priority fix The reference OpenClaw deployment uses AGENTS.md (not RESOLVER.md) at the workspace root, and ships without a manifest.json. check-resolvable silently false-passed against it pre-W1: 0 manifest entries meant 0 reachability iterations meant 0 errors reported. Post-W1 behavior against ~/git/<redacted>/workspace (smoke-tested live): - Detects 102 skills via SKILL.md walk (no manifest.json needed) - Flags 15 unreachable errors (exactly the essay's '~15% dark' finding) - Flags 108 warnings (overlaps, gaps) — advisory, not blocking - Auto-detects via \$OPENCLAW_WORKSPACE without --skills-dir Changes: - NEW src/core/resolver-filenames.ts: one source of truth for the filename policy. \`RESOLVER_FILENAMES = ['RESOLVER.md', 'AGENTS.md']\`. Callers import from here, never hardcode either name. - NEW src/core/skill-manifest.ts: \`loadOrDeriveManifest()\` — reads manifest.json when present+valid, otherwise walks \`skillsDir/*/SKILL.md\` to derive a synthetic manifest. Both check-resolvable.ts AND dry-fix.ts now call this, replacing the two duplicated loaders that silently returned [] on missing file (F-ENG-1, D-CX-12). - src/core/repo-root.ts (rewrite): auto-detect priority changed to put \$OPENCLAW_WORKSPACE ahead of findRepoRoot() walk when explicitly set (D-CX-4). Adds workspace-root AGENTS.md detection — OpenClaw layout places routing at workspace/AGENTS.md with skills/ below. New SkillsDirSource variants \`openclaw_workspace_env_root\` and \`openclaw_workspace_home_root\` for --verbose log clarity. - src/core/check-resolvable.ts: accepts RESOLVER.md or AGENTS.md at the skills dir or one level up (workspace root). Uses loadOrDeriveManifest for reachability. Updated error messages reference both filenames. - src/core/dry-fix.ts: unified manifest loader — auto-fix now works in AGENTS.md-only workspaces where it previously no-op'd silently. - src/commands/check-resolvable.ts: new AUTO_DETECT_HINT import for clearer missing-skills-dir errors; updated sourceLabel map for the two new workspace-root variants. Tests: - test/skill-manifest.test.ts: 14 cases covering explicit-manifest, derived-manifest, malformed JSON, wrong shape, empty explicit array (honored as 'zero skills' declaration), dirname fallback when no name: frontmatter, underscore/dotfile dir skipping. - test/repo-root.test.ts: new tests for the priority swap, AGENTS.md skills-dir variant, AGENTS.md workspace-root variant, both-files present (RESOLVER.md wins). - test/check-resolvable-cli.test.ts: updated regression-gate to the new contract; added three D-CX-3 cases. All 105 tests passing across the foundation surface. Plan + reviews: ~/.claude/plans/p1-lets-just-vast-blanket.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v0.17.0 W2 — Check 5 trigger routing eval (structural) Check 5 of the 10-step skillify checklist (the essay's "resolver trigger eval") now runs structurally by default and has a dedicated CLI verb for CI. Ships Layer A; Layer B (LLM tie-break) is reserved for v0.18. ## New module: src/core/routing-eval.ts The harness. Pure functions: - `normalizeText(s)`: lowercase, strip non-alnum to spaces, collapse whitespace. Unicode-friendly, quote-agnostic, punctuation-tolerant. - `extractTriggerPhrases(cellText)`: split quoted alternatives like `"search for", "find me"` into separate normalized phrases; fall back to the whole cell when unquoted (OpenClaw-style descriptions). - `indexResolverTriggers(resolverContent)`: build a skill-slug → normalized-trigger-phrases map from the resolver table. - `structuralRouteMatch(intent, index)`: substring-match the normalized intent against every trigger phrase; return the set of matched skills + whether the match was ambiguous (more than one specific skill, excluding always-on family). - `lintRoutingFixtures`: rejects fixtures whose intent is verbatim-equal to a trigger (D-CX-6: fixtures must paraphrase the framing, not copy the trigger text) and unknown expected_skill references. - `loadRoutingFixtures(skillsDir)`: walks `skills/<name>/routing-eval.jsonl`, handles JSONL line-comments (`//` / `#`), collects malformed lines separately without crashing. - `runRoutingEval(resolver, fixtures)`: pure scoring. Supports negative cases (`expected_skill: null` — nothing should match) and an `ambiguous_with` allow-list for skills that co-fire with always-on handlers (signal-detector, brain-ops, ingest). Outcomes per fixture: `pass`, `missed`, `ambiguous`, `false_positive`. Metrics: `top1Accuracy`, `passed`, `missed`, `ambiguous`, `falsePositives`. ## Integration: check-resolvable runs Layer A by default `checkResolvable()` now loads `routing-eval.jsonl` fixtures from every skill, runs the structural eval, and appends non-pass outcomes as warning-severity issues. New issue types: - `routing_miss` — expected skill did not match - `routing_ambiguous` — expected matched AND unexpected skills - `routing_false_positive` — negative case unexpectedly matched - `routing_fixture_lint` — linter or malformed-JSONL finding All four are warnings — routing issues don't break exit in default mode, but `--strict` promotes them (D-CX-3 contract). Advisories without breaking CI. ## New CLI verb: `gbrain routing-eval` Standalone Check 5 runner. `--json` envelope, `--llm` flag reserved, `--skills-dir` override. Exit codes: 0 clean, 1 any failure/lint, 2 setup error. Suitable for CI gating separately from check-resolvable. Removed from DEFERRED in CLI: `{check: 5, name: trigger_routing_eval}`. Check 6 (brain_filing) still deferred; lands in W3. ## Seed fixtures - skills/query/routing-eval.jsonl - skills/citation-fixer/routing-eval.jsonl (includes a negative case) These are intentionally modest. Additional fixtures per skill are the natural next step; routing-eval itself passes cleanly under check-resolvable default mode even when fixtures surface real gaps (they're warnings, not errors). Running `gbrain routing-eval` reveals the gaps immediately. ## Tests (34 new cases + updated integrations) - test/routing-eval.test.ts: full harness coverage including normalization, trigger extraction (quoted and unquoted), indexer, structural match with ambiguity + always-on exemption, fixture linter (verbatim-equality rule, unknown-skill rule, shape rule, negative-case skip), JSONL loader (comments, malformed lines, missing dirs, underscore/dot skipping), and every runRoutingEval outcome (pass, miss, ambiguous, negative-pass, false-positive, empty). - test/check-resolvable-cli.test.ts: updated DEFERRED unit test + `--json` envelope test + `--verbose` test to reflect Check 5 shipping. 140/140 passing across the W1 + W2 surface. ## Live smoke `gbrain routing-eval --json` against the current gbrain repo: 6 fixtures, 1 passing, 5 missed. The misses correctly surface resolver-trigger narrowness (intents users naturally phrase differently than trigger text). Fixtures will iterate in follow-up PRs; the machinery ships now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v0.17.0 W3 — Check 6 brain filing audit Check 6 ships. Every skill that writes brain pages is now audited against a machine-readable filing-rules doc at `skills/_brain-filing-rules.json`. ## New: skills/_brain-filing-rules.json Canonical filing rules, JSON (D-CX-8: the pre-existing yaml-lite parser handles flat maps only, so YAML would have needed a new dependency for one file). The companion `_brain-filing-rules.md` stays as the human explainer. 14 rule entries + explicit `sources_dir` carve-out for bulk/raw data. ## New module: src/core/filing-audit.ts - `loadFilingRules(skillsDir)`: returns parsed doc or null (missing file → no-op; malformed JSON throws loud). - `allowedDirectories(rules)`: normalized set of every rules[] directory + sources_dir. - `runFilingAudit(skillsDir)`: walks skills/*/SKILL.md, parses frontmatter, audits any skill with `writes_pages: true`. Two checks per qualifying skill: 1. `writes_to:` list is non-empty. 2. Every entry in `writes_to:` appears in allowedDirectories. Both failures emit warning-severity issues. No errors — advisories only, per D-CX-3. ## Distinction: writes_pages vs mutating (D-CX-7) v0.17 introduces a new boolean frontmatter field `writes_pages:`. `mutating: true` already means "has any side effect" (cron schedulers, report writers, config mutators). Filing audit targets ONLY skills with `writes_pages: true`, correctly excluding side- effect-but-not-page-writing skills. The codex outside voice caught this: conflating the two fields would drag ~100 skills into filing-audit noise in the reference OpenClaw deployment. ## Integration: check-resolvable runs Check 6 by default `checkResolvable()` calls `runFilingAudit(skillsDir)` and appends issues as warnings. On missing/malformed rules doc, surfaces a single advisory rather than bailing. `DEFERRED` array in the CLI is now empty — v0.17 ships both Check 5 (W2) and Check 6 (W3). The export stays in place (stable --json field) for future deferred checks. ## Seeded frontmatter on 7 canonical writers Added `writes_pages: true` + `writes_to:` to: - brain-ops (people, companies, deals, concepts, meetings) - enrich (people, companies) - ingest (people, companies, concepts, meetings, sources) - idea-ingest (people, concepts, sources) - media-ingest (concepts, people, companies, sources) - meeting-ingestion (meetings, people, companies) - signal-detector (people, companies, concepts) Live smoke: `gbrain check-resolvable --json` on gbrain repo shows `ok: true`, zero filing errors, zero filing warnings on seeded skills. Every other mutating:true skill (citation-fixer, cron-scheduler, data-research, maintain, migrate, minion-orchestrator, reports, setup, skill-creator, soul-audit, webhook-transforms) correctly skipped as side-effectful-but-not-page-writing. ## Tests (17 new cases + 3 updated CLI integrations) test/filing-audit.test.ts covers: - rules loader: missing (null), valid, malformed (throw), non-array rules (throw) - directory normalization (trailing slash, leading slash) - clean case - missing writes_to on writes_pages:true - unknown directory - D-CX-7: mutating:true alone does not trigger audit - writes_pages:false skips - no frontmatter skips - inline `writes_to: [a, b]` syntax - block `writes_to:\n - a` syntax - sources/ allowed - underscore/dot dir skipping - total counts (totalScanned vs writesPagesSkills) - missing dir graceful - action string quality guard Plus: CLI integration tests updated for empty DEFERRED array (Checks 5 and 6 both shipped). 158/158 passing across the v0.17 foundation + W1 + W2 + W3 surface. ## v0.18 preview (D-CX-13) v0.17 filing-audit is declaration-level only. A future `gbrain filing-audit --pages` walks the brain itself, infers primary subject from page content via LLM judgment, and flags actual misfilings vs. declarations. Declaration audit is the leading indicator; pages audit is the ground truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v0.17.0 W4 — gbrain skillify {scaffold,check} subcommand namespace The essay's "skillify it!" verb becomes a CLI primitive pair. Two subcommands, both promoted/factored so there's one source of truth: ## `gbrain skillify scaffold <name>` (mechanical) Pure file generation. Zero LLM, zero judgment. Writes 5 stub files atomically: 1. skills/<name>/SKILL.md frontmatter + body template 2. skills/<name>/scripts/<name>.mjs deterministic-code stub 3. skills/<name>/routing-eval.jsonl routing fixture seed 4. test/<name>.test.ts vitest skeleton 5. Appended trigger row to the detected resolver file (RESOLVER.md or AGENTS.md — whatever W1's auto-detect found) Flags: --description (required), --triggers, --writes-to, --writes-pages, --mutating, --force, --dry-run, --json, --skills-dir. Kebab-case name validation (`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`). Works against gbrain-native RESOLVER.md layout AND OpenClaw-native AGENTS.md-at-workspace-root layout (W1 interop). ## `gbrain skillify check [path]` (audit) Promoted from scripts/skillify-check.ts per codex D-CX-2. The legacy script stays as a 12-line shim that delegates to the new module so existing callers (docs, cron, tests) keep working. Wrapped in a subcommand namespace: `gbrain skillify {scaffold, check}` is one coherent verb for the whole post-task loop. The essay's "skillify it!" triggers the markdown skill, which orchestrates the CLI primitives. ## Idempotency contract (D-CX-7) `skillify scaffold --force` regenerates stub FILES but never re-appends a resolver row that already references `skills/<name>/SKILL.md`. Unit test pins this: two applies produce one resolver row, not two. ## D-CX-9 SKILLIFY_STUB sentinel Every scaffolded script + SKILL.md body carries a SKILLIFY_STUB sentinel. `check-resolvable` walks every skill's script dir looking for the marker and emits a `skillify_stub_unreplaced` warning when found. Default mode: advisory. `--strict` mode: error, blocks CI. This is the gate that catches "we scaffolded and forgot to implement" — the exact failure codex flagged as "scaffold verification is theater" in the outside-voice review. ## Files - NEW src/core/skillify/templates.ts (template strings) - NEW src/core/skillify/generator.ts (planScaffold / applyScaffold + SkillifyScaffoldError with typed error codes) - NEW src/commands/skillify.ts (top-level dispatcher + scaffold handler) - NEW src/commands/skillify-check.ts (promoted check logic) - scripts/skillify-check.ts: rewritten to 12-line shim - skills/skillify/SKILL.md: Phase 2 now references the scaffold primitive; legacy manual path kept for extending existing skills - src/cli.ts: `skillify` added to CLI_ONLY + dispatcher - src/core/check-resolvable.ts: SKILLIFY_STUB sentinel scan + new issue type `skillify_stub_unreplaced` ## Tests (14 new scaffold cases) test/skillify-scaffold.test.ts covers: - SKILL_NAME_PATTERN validation (kebab-case, no spaces, no leading digit, no underscores/uppercase) - planScaffold against fresh + existing-file + --force paths - SKILLIFY_STUB sentinel presence in SKILL.md AND script stub (both gate paths) - D-CX-7 idempotency: resolverAppend null when row pre-exists, second apply doesn't duplicate the row - TBD-trigger placeholder when --triggers empty - writes_pages / writes_to / mutating flow through to frontmatter - applyScaffold writes files + appends resolver - Full AGENTS.md-layout workspace interop (W1) Existing test/skillify-check.test.ts still passes against the legacy shim — zero regression for downstream consumers. 178/178 passing across v0.17 foundation + W1..W4. ## Live smoke \`gbrain skillify scaffold webhook-verify --description "verify incoming webhook signatures" --triggers "verify webhook,check tunnel" --skills-dir /tmp/smoke --dry-run\` produces the expected 4-file plan plus a 115-byte resolver append. \`--help\` works on both the top-level and scaffold levels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v0.17.0 W5 — gbrain skillpack install (deps closure + lockfile + diff/dry-run) The essay's "drop it into YOUR OpenClaw" promise lands as a CLI verb. One command installs a curated bundle of gbrain skills + the shared convention files they depend on into a target OpenClaw workspace. Data-loss protected, concurrency-safe, atomic on the AGENTS.md managed block. ## openclaw.plugin.json refresh - Bumped version from stale 0.4.1 → 0.17.0 (codex flagged this drift F-ENG-4 / D-CX-4). - Expanded curated skill list from 7 → 25. Uses skills/manifest.json top-level (v0.10.0 sourced) minus setup/migrate/publish (install-time / code+skill pairs) minus private skills. - Added \`shared_deps: [...]\` listing convention files every skill references: conventions/, _brain-filing-rules.{md,json}, _output-rules.md. Installer always pulls these (D-CX-10 dependency closure). - Added \`excluded_from_install: [...]\` for setup/migrate/publish — surfaces the intentional exclusion as data rather than a comment. ## New module: src/core/skillpack/bundle.ts - \`findGbrainRoot(start)\` — walks up looking for openclaw.plugin.json + src/cli.ts. The pair identifies a gbrain checkout. - \`loadBundleManifest(root)\` — strict validation + typed BundleError codes (manifest_not_found, manifest_malformed, skill_not_found). - \`enumerateBundle({gbrainRoot, skillSlug?, manifest})\` — flat list of source → target-relative paths. When skillSlug is set, scopes to that one skill BUT always pulls shared_deps. \`--all\` walks every skill in the manifest. - \`bundledSkillSlugs(manifest)\` — sorted slugs for \`skillpack list\`. ## New module: src/core/skillpack/installer.ts - \`planInstall(opts)\` — builds InstallPlan with per-file existing/identical diff state. Pure; no writes. - \`applyInstall(plan, opts)\` — writes files + managed block with the contracts below. - \`diffSkill(root, slug, skillsDir)\` — read-only per-file status for \`skillpack diff <name>\`. **Per-file diff protection (D-CX-3 / F4):** wrote_new fresh file wrote_overwrite local diff + --overwrite-local passed skipped_identical bytes match the bundle (silent re-install) skipped_locally_modified target differs + no --overwrite-local → PROTECTED DEFAULT **Concurrency + atomic AGENTS.md (D-CX-11):** - \`.gbrain-skillpack.lock\` at workspace root. Acquired on the first write, released in finally. - Lock stale threshold configurable (default 10min). --force-unlock overrides. - Managed-block writes via tmp-file-plus-rename (atomic on POSIX). **Managed-block format:** <!-- gbrain:skillpack:begin --> <!-- Installed by gbrain <version> — do not hand-edit between markers. --> | Trigger | Skill | |---------|-------| | "alpha" | \`skills/alpha/SKILL.md\` | | ... <!-- gbrain:skillpack:end --> extractManagedSlugs() roundtrips: single-skill installs accumulate into the same block rather than overwriting each other. ## New CLI: gbrain skillpack {list, install, diff, check} Namespaced alongside W4's \`gbrain skillify\`. Subcommands: list bundle inventory (human + --json) install <name> single skill + deps closure install --all entire curated bundle diff <name> per-file diff vs target; read-only check delegates to the pre-existing skillpack-check (same CLI just namespaced) Flags on install: --overwrite-local, --force-unlock, --dry-run, --json, --skills-dir, --workspace. Exit codes: 0 clean, 1 files skipped (protected local edits), 2 setup error / lock held. ## Live smoke \`gbrain skillpack list\`: 25 skills. \`skillpack install query --dry-run\` against a fresh temp workspace: 12 files planned (SKILL.md, routing-eval.jsonl, 7 convention files, 3 rule files, managed block to AGENTS.md). All shared_deps flagged [shared]. ## Tests (36 new cases) test/skillpack-install.test.ts: - findGbrainRoot walks up, returns null when absent - loadBundleManifest validates + rejects malformed - enumerateBundle pulls shared_deps on single-skill scope (D-CX-10) - buildManagedBlock + updateManagedBlock: append when absent, in-place replace when present, extractManagedSlugs roundtrip - planInstall + applyInstall: fresh install, dry-run, idempotency (skipped_identical), local-edit protection, --overwrite-local, lock-held concurrency (D-CX-11), --force-unlock, atomic managed-block write, multi-skill accumulation in managed block, AGENTS.md-at-workspace-root interop (W1 cross-check) - diffSkill: missing, identical, differs test/skillpack-sync-guard.test.ts (F-ENG-4): - both manifests exist - every skill in plugin.json exists on disk - every shared_dep exists on disk - plugin.json skills ⊂ skills/manifest.json - excluded skills aren't in the install list - plugin version ≥ 0.17 (kills the 0.4.1 stale drift) 204/204 passing across the v0.17 foundation + W1..W5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v0.17.0 guards — privacy scrub + OpenClaw-reference E2E + v0.16.4 regression Three ship-blocker work items from the eng review + codex outside voice round out v0.17: ## scripts/check-privacy.sh (CLAUDE.md:550 enforcement) Greps for the banned OpenClaw fork name (case-insensitive) across tracked files. Two modes: scripts/check-privacy.sh scan working tree scripts/check-privacy.sh --staged scan git-staged files (pre-commit) Exit 1 on any finding outside the allow-list. Allow-list covers files where the name is legitimately present: this script itself (defines the rule), CLAUDE.md (the canonical rule text), llms-full.txt (auto-generated from CLAUDE.md), the historical upgrade guide, and test/integrations.test.ts (whose personal-info regex ENFORCES the rule against recipes/). Scrubbed existing leaks: - CHANGELOG.md:366 reference in a closes-# line → "from the OpenClaw reference deployment" - test/doctor-minions-check.test.ts:171 comment → "an OpenClaw host's cron script" - test/plugin-loader.test.ts fixture plugin name → "openclaw-ref" ## test/e2e/openclaw-reference-compat.test.ts (ship-blocker gate) The test that proves v0.17 delivers on the headline claim. New fixture at test/fixtures/openclaw-reference-minimal/ mimics the reference OpenClaw deployment layout: AGENTS.md at workspace root, skills/ below, no manifest.json. Four fixture skills (signal-detector, query, brain-ops, context-now). Every v0.17 surface gets exercised end-to-end: - autoDetectSkillsDir with $OPENCLAW_WORKSPACE (D-CX-4 priority) - loadOrDeriveManifest walks SKILL.md (F-ENG-1 auto-manifest) - checkResolvable accepts AGENTS.md at workspace root, all 4 skills reachable via resolver rows, zero errors - Filing audit clean (brain-ops declares writes_pages+writes_to) - CLI subprocess via `--skills-dir` → exit 0 - CLI subprocess via $OPENCLAW_WORKSPACE (no flag) → exit 0, correct skillsDir detection - skillpack install against the layout writes managed block into AGENTS.md at workspace root This is THE ship-blocker test. If the W1 + W5 stack ever regresses against an AGENTS.md-layout workspace, this fails first. ## test/regression-v0_16_4.test.ts (F-ENG-8) Guards v0.17 against adding "surprise" warnings. Builds a clean fixture matching v0.16.4 canonical shape (manifest.json, RESOLVER.md, 2 skills, no routing-eval fixtures, no writes_pages). Runs v0.17 checkResolvable and asserts: - zero errors, zero routing_*/filing_*/skillify_stub_* warnings - JSON envelope keys unchanged (errors, warnings, issues, ok, summary) — deprecated `issues[]` still equals errors ∪ warnings - summary shape unchanged If someone adds a new check that fires unexpectedly on a v0.16.4-era fixture, this test catches it immediately. ## Fixture test/fixtures/openclaw-reference-minimal/ ├── AGENTS.md (4 rows, 3 sections) └── skills/ ├── brain-ops/SKILL.md (writes_pages+writes_to) ├── context-now/SKILL.md ├── query/SKILL.md └── signal-detector/SKILL.md Intentionally small (4 skills, 1 AGENTS.md, ~30 lines total) so the fixture is maintainable. The OPENCLAW-reference deployment has 107 skills — this fixture is the minimum shape that exercises the full v0.17 code path. ## Tests 215/215 passing across the full v0.17 surface: - foundation + W1 + W2 + W3 + W4 + W5 (204) - regression-v0_16_4 (3) - openclaw-reference-compat (7) - privacy guard (separate bash; exits 0 clean) Plus: privacy pre-commit hook is a drop-in wrapper (documented in the script header). Wiring into .github/workflows is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v0.17.0 — skillify goes end-to-end Every skill. Every check. Every install. One command each. Five workstreams land in one release: - W1: AGENTS.md + auto-manifest + env-priority - W2: Check 5 routing eval - W3: Check 6 brain filing - W4: gbrain skillify {scaffold,check} - W5: gbrain skillpack {list,install,diff} Plus D-CX-3 foundation (errors/warnings split + --strict), plus codex outside-voice fixes (D-CX-1..12 applied), plus privacy pre- commit guard, plus OpenClaw-reference E2E fixture, plus v0.16.4 regression guard. Live against the reference OpenClaw deployment: 102 skills detected via auto-manifest, 15 unreachable errors + 108 warnings surfaced — exactly the essay's "~15% dark" finding. The magic word from the essay finally works the way the essay describes. Tests: 2156 unit (178 new) + 152 E2E Tier 1 + 3 Tier 2 + 8 new openclaw-reference fixture cases. 0 failures across all tiers. Plan + reviews: ~/.claude/plans/p1-lets-just-vast-blanket.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): add missing 'strict' field to 5 Flags literals in check-resolvable-cli.test.ts CI failed `tsc --noEmit` after the D-CX-3 errors/warnings split added `strict: boolean` as a required field on the `Flags` interface. Five test sites in test/check-resolvable-cli.test.ts still construct Flags object literals (for direct `resolveSkillsDir()` calls) and hadn't been updated. Added `strict: false` to all five literals: - line 129 --skills-dir absolute path - line 135 --skills-dir relative path - line 148 no --skills-dir - line 160 no --skills-dir + no env - line 178 --skills-dir + OPENCLAW_WORKSPACE (REGRESSION-GATE) Unit tests: 207/207 pass across the v0.19 surface. tsc --noEmit exits 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: adopt gstack's branch-scoped CHANGELOG rule + rewrite v0.19.0 entry CLAUDE.md gains a new top section before "CHANGELOG voice" that codifies what gstack's CLAUDE.md already says: CHANGELOG is user-facing product release notes, not a log of internal decisions. Every entry describes what THIS branch adds vs master. Plan-file IDs, decision tags (D-CX-#, F-ENG-#), review rounds, test counts as marketing, and contributor- facing metrics don't belong in it. The v0.19.0 entry is rewritten to the new bar: Removed: - Version-collision note about v0.17.0/v0.18.0 shipping on master - All D-CX-## and W# tags (meaningless outside the plan file) - "codex caught" / CEO + Eng review round-up narrative - Plan file path reference - "215 new cases across 13 test files" marketing metrics - W1..W5 bucketing in itemized changes Kept / sharpened: - User-facing headline (what your agent can now do) - Numbers that mean something to users (unreachable-skills count, scaffold timing, pre/post AGENTS.md support) - Upgrade instructions - Added/Changed/Fixed/For-contributors itemized sections (standard keep-a-changelog shape) Version sequence (`grep "^## \["`) is contiguous v0.19.0 → v0.16.4. Privacy guard clean. Tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: update README/CLAUDE/TODOS for v0.19.0 skills + skillify loop Skill count was stale (README said 26, actual is 28: skillify + skillpack-check were missing from the tables and count). Corrected throughout. Marked TODOS item "Checks 5 + 6 deferred in PR #325" as completed in v0.19 — they shipped as real implementations, not just filed issues. README: - Skill count 26 → 28 (headline, install flow, table section, architecture diagram) - Added `skillify` + `skillpack-check` rows to the operational skills table - Rewrote the "Skillify" section to lead with the four v0.19 CLI verbs (`gbrain skillify scaffold/check`, `gbrain skillpack list/install/diff`, `gbrain routing-eval`, `gbrain check-resolvable --strict`) instead of describing the pre-v0.19 state. Added the "works on your OpenClaw" pitch around AGENTS.md + auto-manifest. Added the "drop 25 curated skills into your OpenClaw" section for skillpack install. - Added v0.19 skills block + v0.18 multi-source + v0.17 dream to the Commands reference at the bottom. - Standalone instruction sets count: 25 → 28 (with a parenthetical noting the curated 25-skill bundle that `skillpack install` ships). CLAUDE.md: - Skill count 26 → 28 in the Skills section. - New "Skillify loop (v0.19)" sub-bullet listing skillify + skillpack-check. - Noted that `AGENTS.md` is also accepted as a resolver filename. TODOS.md: - Created "## Completed" section at the top. - Moved the "Checks 5 + 6" item there with completion note linking to the actual implementation files (routing-eval.ts + filing-audit.ts). Privacy scan clean. Version sequence contiguous v0.19.0 → v0.16.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): regenerate llms-full.txt + llms.txt after README/CLAUDE edits CI failed on `build-llms generator > committed llms.txt + llms-full.txt match current generator output`. The drift was expected: the prior commit edited README.md and CLAUDE.md (skill count + skillify section), both of which are inlined into llms-full.txt by `scripts/build-llms.ts`. Fix: `bun run build:llms` + commit the regenerated output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Wintermute <wintermute@garrytan.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |