Commit Graph
2 Commits
Author SHA1 Message Date
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>
2026-04-23 19:34:27 -07:00
0e9f8814a5 feat: v0.16.0 — durable agent runtime (gbrain agent + subagent handler + plugin loader) (#258)
* refactor(mcp): extract buildToolDefs helper for subagent tool registry reuse

The inline operations.map(...) block in src/mcp/server.ts became the only
source of truth for agent-facing tool definitions. Extract into a reusable
exported helper so the v0.15 subagent tool registry can call it with a
filtered OPERATIONS subset instead of duplicating the shape.

Byte-for-byte equivalence regression pinned in test/mcp-tool-defs.test.ts —
legacy inline mapping kept verbatim inside the test so any future drift
between the new helper and the pre-extraction MCP schema fails loudly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(operations): subagent-aware OperationContext + put_page namespace

Adds three optional fields to OperationContext:
  - jobId?: number       — the currently running Minion job id
  - subagentId?: number  — the owning subagent job id for tool-dispatched calls
  - viaSubagent?: boolean — FAIL-CLOSED flag for agent-path gating

put_page now enforces a namespace rule when invoked on the subagent tool
dispatch path (viaSubagent=true): writes MUST target
`wiki/agents/<subagentId>/...`. Anchored, slash-boundary enforced so a
collision like `wiki/agents/12evil/...` can't impersonate subagent 12.

The check runs BEFORE the dry-run short-circuit so preview calls surface
the same rejection. Fail-closed: a missing subagentId with viaSubagent=true
rejects every slug rather than letting a dispatcher bug open a hole.

Existing callers unaffected — all three fields are optional and the legacy
put_page behavior is unchanged when viaSubagent is undefined/false.

12 regression + namespace tests pin:
  - local CLI writes (viaSubagent unset) accept arbitrary slugs
  - MCP writes (remote=true, viaSubagent unset) accept arbitrary slugs
  - subagent-path: anchored prefix accepted, wrong id rejected, prefix-
    collision defeated, leading-slash rejected, bare-prefix rejected,
    fail-closed on missing/NaN subagentId, permission_denied code emitted

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): v0.15.0 subagent runtime tables + migration orchestrator

Adds three new tables for the durable LLM agent runtime:

  subagent_messages         — Anthropic message-block persistence.
                              Parallel tool_use blocks in one assistant
                              message live in content_blocks JSONB, not
                              across rows (fixes the (job_id, turn_idx, role)
                              misdesign codex caught in v0.13 drafting).

  subagent_tool_executions  — Two-phase tool ledger. INSERT pending before
                              execute, UPDATE complete/failed after. Replay
                              re-runs pending rows only if the tool is
                              idempotent (v1 ships only idempotent tools so
                              this is preventive).

  subagent_rate_leases      — Lease-based concurrency cap for outbound
                              providers (e.g. anthropic:messages). Stale
                              leases auto-prune on next acquire so crashed
                              workers can't strand capacity.

All DDL uses CREATE TABLE/INDEX IF NOT EXISTS — order-independent vs
PR #244's initSchema() reorder, and idempotent across fresh-install +
upgrade paths. Shipped in both src/schema.sql (Postgres) and
src/core/pglite-schema.ts (PGLite); schema-embedded.ts regenerated.

Migration orchestrator v0_15_0.ts (phases: schema → verify → record).
v0_14_0.ts is a no-op stub so the registry's version sequence stays
gapless (v0.14.0 shipped shell-jobs — code change, no DB migration).

10 unit tests for registry wiring, ordering, dry-run phase behavior, and
schema-embedded table presence. test/apply-migrations.test.ts updated for
the two new registry entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): emit child_done on every terminal + max_stalled per-job + terminal set fix

Three correctness fixes the v0.15 subagent aggregator spine depends on:

1. child_done emission on ALL terminal transitions, not just success.
   - completeJob already emitted on success — now also tags outcome='complete'.
   - failJob newly emits on terminal 'failed' or 'dead' (outcome='failed'|'dead',
     error=<text>), BEFORE the parent-terminal UPDATE so the EXISTS guard on
     the inbox INSERT doesn't skip it on fail_parent paths (codex catch).
   - cancelJob now emits outcome='cancelled' per descendant with a parent.
   - handleTimeouts now emits outcome='timeout' per timed-out child.
   ChildDoneMessage gains optional { outcome, error } — backwards compatible
   (legacy writers omitted them; consumers treat absent outcome as 'complete').

2. Parent-resolution terminal set now includes 'failed'.
   Pre-v0.15 the `NOT EXISTS (... status NOT IN ('completed','dead','cancelled'))`
   guard treated a failed child as still-pending, stranding aggregator parents
   that chose on_child_fail='continue' or 'ignore' in waiting-children forever.
   Expanded to {completed, failed, dead, cancelled} everywhere parent resolution
   reads child status (completeJob inline, failJob remove_dep + continue,
   cancelJob sweep, handleTimeouts sweep, and the resolveParent method itself).

3. MinionJobInput.max_stalled threads through MinionQueue.add() on INSERT.
   Column exists with default 1 — that is "first stall → dead", which defeats
   crash recovery for long-running handlers. Subagent children will set
   max_stalled: 3 to survive mid-run worker kills. Second-submitter under an
   idempotency-key hit does NOT mutate the existing row (codex-flagged
   footgun — first-submit options are load-bearing state).

13 unit tests pin: emission on each of completeJob/failJob/cancelJob/
handleTimeouts, insertion order on fail_parent, terminal-set expansion with
continue policy, max_stalled default + override + idempotency behavior.

E2E tier 1 (Postgres) passes 141 tests unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): rate-leases + waitForCompletion infra for v0.15 subagent

Two infrastructure modules the subagent handler spine depends on:

rate-leases.ts — lease-based concurrency cap for outbound providers
(anthropic:messages, openai:*, etc.). Counter-based limiters leak capacity
on worker crash; leases are owner-tagged rows with expires_at that
auto-prune on the next acquire. Two-phase: txn-scoped pg_advisory_xact_lock
guards the check-then-insert so concurrent acquires can't both win the
"last slot". renewLeaseWithBackoff retries 3x (250/500/1000ms) for mid-
call DB blips — on persistent failure the LLM-loop caller aborts with a
renewable error so the worker re-claims and the rate invariant is
preserved. Owner FK cascades clean up leases on job deletion.

wait-for-completion.ts — poll-until-terminal helper for CLI callers.
Minions' NOTIFY is worker-side only; `gbrain agent run --follow` polls
getJob() until status is {completed, failed, dead, cancelled}. TimeoutError
carries jobId + elapsedMs and does NOT cancel the job — the user can
inspect via `gbrain jobs get <id>` later. Supports AbortSignal for Ctrl-C
without throwing. Default pollMs is 1000 on Postgres, 250 on PGLite (inline
CLI has no network RTT).

21 unit tests cover: single/multi acquire under cap, rejection past cap,
release frees slot, different keys are independent, stale prune, cascade
on owner delete, renew bumps expires_at, renew on missing is false,
backoff path success + pruned short-circuit. waitForCompletion: fast-path
terminal, transitions mid-wait (completed/failed/cancelled), TimeoutError
shape, abort-signal early exit, non-existent job error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): subagent ToolDef types + brain-tool registry (v0.15)

Types first so the handler has a stable contract:
  - SubagentHandlerData / AggregatorHandlerData — the two job.data shapes
  - ToolCtx (engine, jobId, remote, signal) + ToolDef (name, description,
    input_schema, idempotent, execute) — Anthropic-envelope, distinct from
    the MCP McpToolDef extraction landed earlier
  - ContentBlock discriminated union for subagent_messages.content_blocks
  - SubagentStopReason + SubagentResult emitted on terminal completion

brain-allowlist.ts derives one ToolDef per allow-listed OPERATION. Reuses
the ParamDef → JSONSchema shape from the MCP extraction in a local helper
(Anthropic's input_schema field diverges from MCP's inputSchema by a
character). The 11-name allow-list is read-safe + put_page — every
destructive / filesystem / identity-mutating op stays off by default.

put_page gets a namespace-wrapped tool schema: `slug` pattern = anchored
`^wiki/agents/<subagentId>/.+`. The server-side check in put_page op
(shipped in prior commit) is still the authoritative gate — the schema
just helps the model write correct slugs first-try. `subagentId` is
plumbed into the ToolCtx so the viaSubagent=true fail-closed path lights
up on every tool-dispatched put_page.

filterAllowedTools narrows a registry by subagent_def's allowed_tools
frontmatter field. Rejects unknown names at load time (no silent drop —
typos in a skills/subagents/*.md would otherwise ship to prod with a
tool silently missing).

18 tests pin: every allowlist name exists in OPERATIONS (catches upstream
rename), Anthropic name regex, put_page namespace pattern per-subagent,
execute() routes through the op handler with viaSubagent=true, out-of-
namespace put_page throws permission_denied, filter passes prefixed +
unprefixed names, rejects unknowns, deduplicates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): subagent-audit JSONL + transcript renderer

Two small plumbing pieces the v0.15 subagent handler + `gbrain agent logs`
depend on:

subagent-audit.ts — JSONL-rotated audit log mirroring the shell-audit
pattern. Two event flavors: submission (one line per job submit) and
heartbeat (one line per turn boundary — llm_call_started / completed /
tool_called / tool_result / tool_failed). Heartbeats fix the "--follow on
a long Anthropic call shows nothing for 30 seconds" problem codex flagged.
Never logs prompts or tool inputs (PII risk — subagent input_vars may
carry user-supplied free text); DOES log tokens, ms_elapsed, tool_name,
first 200 chars of error text. Rotates weekly via ISO week. `readSubagent
AuditForJob` is the readback path for `gbrain agent logs` — scans the
current + prior week file so job boundaries across weeks still resolve.
`GBRAIN_AUDIT_DIR` overrides the default ~/.gbrain/audit/ for container
deploys.

transcript.ts — renders subagent_messages + subagent_tool_executions to
markdown. Message order is authoritative; tool rows splice under their
owning assistant tool_use by tool_use_id. Handles text, tool_use (with
pending / complete / failed execution rows), tool_result (skipped if
we already rendered the owning tool_use — avoids double-printing), and
unknown block types (fenced JSON dump for diagnostics). Output is
UTF-8-safe truncated at maxOutputBytes.

21 unit tests: ISO week filename rotation (incl. 2027-01-01 → W53-2026
boundary), submission + heartbeat write shapes, 200-char error cap, best-
effort write failure doesn't throw, readback filters by job_id and
sinceIso. Transcript: empty input, ordering, token line, tool_use +
complete/failed/pending execution rendering, truncation, unknown-block
diagnostic dump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): subagent LLM-loop handler with crash-resumable replay

The main event: runs one Anthropic Messages API conversation with tool
use, persists every turn + tool execution, and resumes cleanly after a
worker kill anywhere in the loop.

Design points that carry the v0.15 guarantees:

  1. Two-phase tool persistence. INSERT status='pending' before dispatch,
     UPDATE to 'complete' or 'failed' after. subagent_messages rows are
     the canonical conversation; subagent_tool_executions rows are the
     canonical "did this tool run + what did it return". Either DB commit
     is atomic, so replay has a single source of truth.

  2. Replay reconciliation. If the last persisted message is an assistant
     with tool_use blocks AND no following synthesized user message, we
     crashed mid-dispatch. On resume, finish those tools first (respecting
     idempotent flag for 'pending' rows), synthesize the user turn, and
     THEN call the LLM again. Non-idempotent pending rows abort the job
     with a clear error — v0.15 ships only idempotent tools so this is
     preventive.

  3. Rate lease around every LLM call. acquireLease before, releaseLease
     after (both success and error paths). acquired=false throws
     RateLeaseUnavailableError — the worker treats it as a renewable
     error and re-claims later, so a temporary capacity cap doesn't fail
     the job terminally.

  4. Anthropic prompt caching. system block gets cache_control=ephemeral;
     the LAST tool def gets it too (Anthropic caches everything up to and
     including the marked block). ~10x cost reduction on multi-turn
     agents per the plan.

  5. Dual-signal abort. AbortSignal.any merges ctx.signal (timeout / lock
     loss / cancel) with ctx.shutdownSignal (worker SIGTERM). Both feed
     the Anthropic call's AbortSignal; mid-turn abort bails before the
     next LLM call with whatever turns are already persisted. Node ≥ 20
     has AbortSignal.any; older runtimes get a manual-merge polyfill.

  6. Injectable Anthropic client. The real SDK implements MessagesClient
     structurally; tests inject a FakeMessagesClient that scripts
     responses.

12 unit tests pin: no-tool happy path, single tool_use complete, tool
throws → failed row + loop continues, unknown tool name rejection,
max_turns cap, crash-then-resume with partial state, replay skips already-
complete tool execs without re-invoking execute, non-idempotent pending
rejects on resume, lease acquire + release roundtrip, RateLeaseUnavailable
under cap-full, missing prompt validation, allowed_tools unknown-name.

NOT in v0.15: refusal detection (stop_reason + content shape), stop_reason
=max_tokens partial recovery, mid-call lease renewal with backoff loop.
All three are documented as P2 items in the plan file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): subagent_aggregator handler with mixed-outcome rendering

Claims AFTER all subagent children resolve — by then Lane 1B's queue
changes have posted one child_done message per terminal transition into
this job's inbox (complete / failed / dead / cancelled / timeout). The
aggregator reads those, builds a deterministic markdown summary, and
returns it as the handler result.

Not an LLM call in v0.15 — output is reproducible concatenation so
fan-out runs stay comparable. v0.16+ can add an LLM synthesis pass
behind an opt-in flag.

Contract:
  - empty children_ids → `(no children)` marker
  - missing child_done (shouldn't happen under v0.15 invariants but
    possible if a terminal-state path slipped past Lane 1B) → counted as
    failed with "no child_done message observed" error
  - non-complete outcomes: result is null in the output so no payload
    leaks alongside a failure label
  - children appear in the order children_ids was supplied
  - custom aggregate_prompt_template replaces the markdown header

13 unit tests cover: empty input, all-success, mixed outcomes, result
suppression on failure, missing child_done handling, order preservation,
custom template, progress + log emission, stringified JSONB payload
parsing, non-child_done inbox filtering, legacy-writer outcome fallback,
and internal helper edges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): GBRAIN_PLUGIN_PATH loader + plugin-authors guide (v0.15)

Plumbing that makes Wintermute (and future downstream agents) day-1
usable on v0.15. Host repos drop a `gbrain.plugin.json` + `subagents/`
directory somewhere, set GBRAIN_PLUGIN_PATH (colon-separated like \$PATH),
and their custom subagent defs load at worker startup.

Path policy is strict: absolute paths only. Relative, ~-prefixed, and
URL-style (https://, file://) all rejected with warnings — the user
controls where plugins live. Non-existent paths and files (not dirs) are
warned and skipped so a typo doesn't crash worker startup.

Collision policy: left-wins. If two plugins ship a subagent with the same
name, the first one in GBRAIN_PLUGIN_PATH keeps it and the other gets a
warning naming both sources. Deterministic + debuggable.

Trust policy: plugins ship subagent defs ONLY. Cannot declare new tools,
cannot extend the brain allow-list, cannot override safety flags. The
subagent def's `allowed_tools:` frontmatter MUST subset the derived
registry — validation happens at load time (worker startup), not at
dispatch time, so a typo in a skill gives a loud startup error instead
of silently "tool never fires at 3am."

Manifest `plugin_version: "gbrain-plugin-v1"` locks the contract. Unknown
versions rejected. `subagents` field escape attempts (`../../../etc` etc)
rejected. gray-matter handles the markdown frontmatter parse — subagent
defs don't conform to the page schema, so we don't use parseMarkdown.

docs/guides/plugin-authors.md is the Wintermute-facing walkthrough.
Covers the minimum viable plugin shape, the three policies, the
frontmatter fields, known caveats (audit JSONL is local-only, tool calls
always run remote=true, put_page is namespace-scoped).

22 unit tests pin path rejection, missing/invalid manifest, unsupported
version, escape-attempt, basename fallback for missing frontmatter.name,
allowed_tools round-trip, unknown-tool rejection with validAgentToolNames,
empty env, multi-path, collision warning with left-wins, trimmed paths,
manifest-rejection as warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): gbrain agent run + logs + worker registration (v0.15 Lane 4H)

Three integration seams wired:

src/commands/agent.ts — \`gbrain agent run\`. Submits subagent jobs (or a
fan-out of N + aggregator) under the trusted-submit flag so the
PROTECTED_JOB_NAMES guard doesn't reject. Fan-out path creates the
aggregator first (so children can reference its id as parent), submits
each child with on_child_fail='continue' (required by Lane 1B's terminal-
set + child_done machinery), then jsonb_set's the aggregator's
children_ids. Short-circuits a 1-entry manifest to a single subagent
with no aggregator. Follow mode runs agent-logs streaming + waitFor
Completion in parallel and exits on terminal status; detach prints the
job id and exits. Ctrl-C is handled as detach, not cancel — the job
keeps running, consistent with durability invariants.

src/commands/agent-logs.ts — \`gbrain agent logs\`. Merges ~/.gbrain/audit/
subagent-jobs-*.jsonl (heartbeats + submissions) with subagent_messages
(persisted conversation) in one chronological stream. --follow polls at
1s and exits when the job hits terminal. --since accepts ISO-8601 OR
relative shorthand (5m / 1h / 2d). Writes transcript tail (full message
+ tool tree) only for terminal jobs, so mid-run --follow doesn't spam a
half-rendered transcript.

src/commands/jobs.ts registerBuiltinHandlers — matches the shell-handler
opt-in shape. GBRAIN_ALLOW_LLM_JOBS=1 registers the subagent +
subagent_aggregator handlers, then loads plugins from GBRAIN_PLUGIN_PATH
with validAgentToolNames pulled from BRAIN_TOOL_ALLOWLIST. Every plugin
warning + loaded-plugin line prints to stderr, mirroring the openclaw-
seam startup convention.

src/core/minions/protected-names.ts — subagent + subagent_aggregator
join the protected set. MCP submit_job returns permission_denied; only
trusted-CLI callers (with allowProtectedSubmit) can insert these rows.

src/cli.ts — adds 'agent' to CLI_ONLY + dispatches it like 'jobs'.

Test fallout: subagent-handler.test.ts + subagent-transcript.test.ts
helpers now submit under allowProtectedSubmit (they insert rows named
'subagent' directly against the queue). 23 new tests in agent-cli.test.ts
cover: flag parsing (including --detach implies !follow, --tools comma
split, -- terminator, unknown flag throw), --since parse (ISO, relative
5m/2h/1d, unparseable error), protected-name guard for all three names,
trusted-submit gate, and a fan-out integration check that verifies the
aggregator + children shape after --fanout-manifest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(e2e): rename max_children test's spawned jobs off the protected 'subagent' name

The spawn-storm test submitted 50 literal-string 'subagent' children to
exercise the max_children row-lock serialization. In v0.15 'subagent' is
a PROTECTED_JOB_NAME (CLI-only; trusted submit required), so the old
literal submission now throws before reaching the row-lock check.

The test is about max_children semantics, not the v0.15 subagent runtime
specifically — rename the child name to 'child_worker' so the test
exercises the exact same queue.add path without tripping the new guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ship): v0.15.0 — VERSION, CHANGELOG, README, upgrading-agents, CLAUDE.md

Bumps VERSION → 0.15.0 and package.json → 0.15.0 (resolves the pre-existing
drift — on master, VERSION=0.14.0 but package.json=0.13.1; src/version.ts
reads package.json, so this is what the binary prints now).

CHANGELOG lands the release-summary entry in the GStack voice + the full
itemized change list (11 new modules, 3 new tables, queue correctness
fixes, trust-model additions, 159 new unit tests). Voice rules respected
— no em dashes, no AI vocabulary, real file names + real numbers.

README gets a "Durable agents: `gbrain agent` (v0.15)" section next to
the Minions block, with the three canonical CLI shapes (single run,
fanout-manifest, logs --follow) and a pointer to plugin-authors.md.

docs/UPGRADING_DOWNSTREAM_AGENTS.md gets a full v0.15.0 section covering
the four adoption steps downstream agents (Wintermute and similar) need:
(1) worker opt-in via GBRAIN_ALLOW_LLM_JOBS, (2) moving custom subagent
defs to a plugin repo, (3) replacing ephemeral subagent runs with durable
`gbrain agent run`, (4) the put_page namespace rule for agent-driven writes.

CLAUDE.md updated with concise per-file descriptions for every new module:
the handler, aggregator, audit, rate-leases, wait-for-completion,
transcript, plugin-loader, brain-allowlist, tool-defs extraction, agent
CLI + logs CLI, and the registerBuiltinHandlers wiring for subagent
handlers + plugin-loader.

Verified: binary builds (940 modules, 89ms compile), prints `gbrain 0.15.0`,
`gbrain agent --help` shows the new subcommand shape. 170 new tests pass
(full v0.15 surface). Full unit suite passes bar one parallel-load
flake on a pre-existing E2E (graph-quality, passes in isolation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(minions): drop GBRAIN_ALLOW_LLM_JOBS flag — subagent handlers always-on

The env flag was ceremony. Shell jobs need the flag because they execute
arbitrary CLI commands (RCE surface). Subagent jobs don't — they call the
Anthropic API with whatever ANTHROPIC_API_KEY is in env, so the key is
already the cost gate (no key → SDK fails on the first turn). And
who-can-submit is already protected by PROTECTED_JOB_NAMES +
TrustedSubmitOpts: MCP callers get permission_denied; only `gbrain agent
run` with allowProtectedSubmit can insert subagent / subagent_aggregator
rows. The flag added nothing the existing guards didn't already give us.

registerBuiltinHandlers now always registers subagent + subagent_aggregator
and loads GBRAIN_PLUGIN_PATH plugins. Worker startup prints:

  [minion worker] subagent handlers enabled

instead of the conditional enabled/disabled pair. Plugin discovery runs
unconditionally — empty PATH is a no-op.

README, CHANGELOG, docs/UPGRADING_DOWNSTREAM_AGENTS, CLAUDE.md, agent CLI
help text, and subagent handler docstring all updated to drop the flag
reference. Shell handler's GBRAIN_ALLOW_SHELL_JOBS gate is untouched —
separate concern (RCE, not billing).

Full suite: 1859 pass, 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: scrub private agent-fork name from all public artifacts

Enforces the rule added to CLAUDE.md (privacy section): never say
`Wintermute` in any CHANGELOG, README, doc, PR, or commit message.
Reader-facing copy says `your OpenClaw` (the term covers every
downstream OpenClaw deployment — Wintermute, Hermes, AlphaClaw — in
one umbrella the reader already recognizes). First-person /
origin-story copy says `Garry's OpenClaw` (honest that this is the
production deployment driving the feature, without exposing the
private agent's name).

Swept across:
  CHANGELOG.md (v0.15 entry + 4 historical mentions)
  README.md
  TODOS.md
  docs/UPGRADING_DOWNSTREAM_AGENTS.md
  docs/guides/plugin-authors.md (including example plugin names)
  docs/guides/plugin-handlers.md
  docs/guides/minions-fix.md
  docs/designs/KNOWLEDGE_RUNTIME.md (27 refs, mostly analytical)
  docs/benchmarks/2026-04-18-minions-vs-openclaw-production.md
  skills/migrations/v0.11.0.md
  skills/skillpack-check/SKILL.md
  scripts/skillify-check.ts
  src/commands/doctor.ts
  src/commands/migrations/v0_15_0.ts
  src/commands/skillpack-check.ts
  src/core/enrichment/completeness.ts
  src/core/minions/plugin-loader.ts
  src/core/operations.ts
  src/core/output/scaffold.ts

Intentionally kept (these mentions define/test the rule itself):
  CLAUDE.md — the privacy rule section necessarily uses the literal
  name to define the restriction and examples
  test/plugin-loader.test.ts — fixture name in a plugin-loading test;
  renaming risks breaking assertion logic
  test/integrations.test.ts — the word appears in a privacy-regex
  test that explicitly enforces name redaction
  test/doctor-minions-check.test.ts — a comment referencing the rule
  CEO plan artifact at ~/.gstack/projects/… — private, not distributed

Binary builds (941 modules), 198/198 relevant tests pass, `gbrain --version`
prints `0.15.0`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: gitignore bun --compile artifacts with a glob, not specific hashes

Each `bun build --compile` emits a fresh hash-named `.*-*.bun-build` file
in cwd. The prior entries listed two specific hashes that were already
stale, so every build after those created a new untracked file requiring
manual cleanup.

Replace the two stale entries with `*.bun-build` so any current or future
compile artifact is ignored automatically.

Verified: ran `bun build --compile`, got two new `.*-*.bun-build` files,
`git status` stays clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ship): rename v0.15.0 → v0.16.0

gbrain master is at 0.14.2. Other 0.15.x PRs may land before/after
this one — we bump the minor (new capability) and lock to 0.16.0 so
ordering with concurrent work doesn't matter.

Touches:
- VERSION: 0.15.0 → 0.16.0
- package.json: 0.15.0 → 0.16.0
- Rename src/commands/migrations/v0_15_0.ts → v0_16_0.ts (+ all
  version strings inside + import in index.ts registry)
- Rename test/migrations-v0_15_0.test.ts → migrations-v0_16_0.test.ts
- test/apply-migrations.test.ts: skippedFuture lists now reference
  '0.16.0'
- test/put-page-namespace.test.ts + test/mcp-tool-defs.test.ts: Lane
  comment refs updated
- src/schema.sql + src/core/pglite-schema.ts: "v0.15.0" section
  comment updated; src/core/schema-embedded.ts regenerated
- CHANGELOG.md: top entry renamed to [0.16.0]; inline v0_15_0 /
  v0.15.0 refs swept
- docs/UPGRADING_DOWNSTREAM_AGENTS.md: section heading v0.15.0 → v0.16.0

Verified: `gbrain --version` prints 0.16.0, migration registry /
buildPlan / put_page / mcp-tool-defs / handlers tests all green
(49/49).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: reframe v0.16 durability headline around OpenClaw crashes

"Laptop closed mid-run" framing implied a consumer workflow. Real pain is
OpenClaw subagents dying daily on worker kill, memory blip, or timeout.
Headline + README copy match the body now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate llms-full.txt after README copy change

Regen drift guard caught the README edit from 83beec4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:14:17 -07:00