* 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>
472 lines
19 KiB
Markdown
472 lines
19 KiB
Markdown
# Upgrading Downstream Agents
|
|
|
|
GBrain ships skills in `skills/`. Downstream agents (custom OpenClaw deployments,
|
|
agent forks of any kind) often **copy** these skill files into their own workspace and
|
|
diverge over time — adding agent-specific phases, removing irrelevant ones, tightening
|
|
language. Once that happens, gbrain can't push updates to those forks. The agent has
|
|
to apply the diffs by hand.
|
|
|
|
This doc lists the exact diffs each downstream agent needs to apply when upgrading.
|
|
Cross-reference against your fork's local skill files.
|
|
|
|
## Why this exists
|
|
|
|
`gbrain upgrade` ships the new binary. `gbrain post-upgrade [--execute --yes]` runs
|
|
the schema migrations and backfills the data. But the **skill files themselves**
|
|
that tell the agent how to behave — those are user-owned. If your `~/git/<your-agent>/workspace/skills/brain-ops/SKILL.md`
|
|
says `# Based on gbrain v0.10.0` at the top, it doesn't know about v0.12.0 features.
|
|
|
|
The agent will keep manually calling `gbrain link` after every `put_page` (now redundant —
|
|
auto-link does it), miss out on `gbrain graph-query` for relationship questions, and
|
|
not know to backfill the structured timeline.
|
|
|
|
## How to apply
|
|
|
|
1. Identify your forked skill files. Typically at `~/git/<your-agent>/workspace/skills/` or wherever your agent's skill directory lives.
|
|
2. For each skill listed below, find the matching phase/section in your fork.
|
|
3. Apply the diff (paste the new block in the indicated location).
|
|
4. Update the version banner at the top of your fork (`# Based on gbrain v0.12.0`).
|
|
5. Verify: ask the agent to write a test page and confirm the response includes
|
|
`auto_links: { created, removed, errors }`.
|
|
|
|
Total time: ~10 minutes for all four skills.
|
|
|
|
---
|
|
|
|
## 1. brain-ops/SKILL.md
|
|
|
|
**Where:** Insert a new `### Phase 2.5` section immediately after `### Phase 2: On Every Inbound Signal`.
|
|
|
|
**Why:** Phase 2.5 declares that auto-link runs automatically. Without this, the
|
|
agent's mental model says it must call `gbrain link` after every `put_page`, which
|
|
is now redundant and can cause double-add warnings.
|
|
|
|
```markdown
|
|
### Phase 2.5: Structured Graph Updates (automatic)
|
|
|
|
Every `put_page` call automatically extracts entity references and writes them
|
|
to the graph (`links` table) with inferred relationship types. Stale links
|
|
(refs no longer in the page text) are removed in the same call. This is
|
|
"auto-link" reconciliation.
|
|
|
|
- No manual `add_link` calls needed for ordinary page writes.
|
|
- Inferred link types: `attended` (meeting -> person), `works_at`, `invested_in`,
|
|
`founded`, `advises`, `source` (frontmatter), `mentions` (default).
|
|
- The `put_page` MCP response includes `auto_links: { created, removed, errors }`
|
|
so the agent can verify outcomes.
|
|
- To disable: `gbrain config set auto_link false`. Default is on.
|
|
- Timeline entries with specific dates still need explicit `gbrain timeline-add`
|
|
(or batch via `gbrain extract timeline --source db`).
|
|
```
|
|
|
|
**Also update the Iron Law section.** If your fork still says "Back-links maintained
|
|
on every brain write (Iron Law)" without qualification, append:
|
|
|
|
```markdown
|
|
**v0.12.0 update:** Auto-link satisfies the Iron Law for entity-reference links
|
|
on every `put_page`. The agent's Iron Law obligation is now: include the
|
|
entity reference in the page content (e.g., `[Alice](people/alice)`); auto-link
|
|
handles the structured row. Manual `add_link` calls are reserved for
|
|
relationships you can't express in markdown content.
|
|
```
|
|
|
|
---
|
|
|
|
## 2. meeting-ingestion/SKILL.md
|
|
|
|
**Where:** Append to the end of `### Phase 3: Attendee enrichment`.
|
|
|
|
**Why:** Eliminates redundant `gbrain link` calls per attendee (auto-link handles them
|
|
when the meeting page references attendees as `[Name](people/slug)`).
|
|
|
|
```markdown
|
|
**Note (v0.12.0):** Once the meeting page is written via `gbrain put`, the
|
|
auto-link post-hook automatically creates `attended` links from the meeting
|
|
to each attendee whose page is referenced as `[Name](people/slug)`. You don't
|
|
need to call `gbrain link` for attendees. You DO still need `gbrain timeline-add`
|
|
for dated events (auto-link only handles links, not timeline entries).
|
|
```
|
|
|
|
**Where:** In `### Phase 4: Entity propagation`, the line "Back-link from entity page
|
|
to meeting page" can be replaced with:
|
|
|
|
```markdown
|
|
4. Entity references in the meeting page body auto-create the link via auto-link.
|
|
For incoming references on the entity page (entity page → meeting page), edit
|
|
the entity page to mention the meeting and `put_page` it — auto-link handles
|
|
the rest.
|
|
```
|
|
|
|
---
|
|
|
|
## 3. signal-detector/SKILL.md
|
|
|
|
**Where:** Append to the end of `### Phase 2: Entity Detection`.
|
|
|
|
**Why:** Same logic as brain-ops — eliminates manual `gbrain link` after writing
|
|
originals/ideas pages that reference people or companies.
|
|
|
|
```markdown
|
|
**Auto-link (v0.12.0):** When you write/update an originals or ideas page that
|
|
references a person or company, the auto-link post-hook on `put_page`
|
|
automatically creates the link from the new page to that entity. You don't
|
|
need to call `gbrain link` manually. Timeline entries still need explicit calls.
|
|
```
|
|
|
|
---
|
|
|
|
## 4. enrich/SKILL.md
|
|
|
|
**Where:** Replace `### Step 7: Cross-reference` with the v0.12.0 version.
|
|
|
|
**Why:** Step 7 used to be primarily about creating links between related entity
|
|
pages. With auto-link, that's automatic. Step 7 is now about content updates,
|
|
not link creation.
|
|
|
|
Old (delete):
|
|
```markdown
|
|
### Step 7: Cross-reference
|
|
|
|
- Update company pages from person enrichment (and vice versa)
|
|
- Update related project/deal pages if relevant context surfaced
|
|
- Check index files if the brain uses them
|
|
- Add back-links manually via `gbrain link` for any new entity references
|
|
```
|
|
|
|
New (paste):
|
|
```markdown
|
|
### Step 7: Cross-reference
|
|
|
|
- Update company pages from person enrichment (and vice versa)
|
|
- Update related project/deal pages if relevant context surfaced
|
|
- Check index files if the brain uses them
|
|
|
|
**Note (v0.12.0):** Links between brain pages are auto-created on every
|
|
`put_page` call (auto-link post-hook). Step 7 focuses on content
|
|
cross-references (updating related pages' compiled truth with new signal
|
|
from this enrichment), not on creating links. Verify via the `auto_links`
|
|
field in the put_page response (`{ created, removed, errors }`).
|
|
Timeline entries still need explicit `gbrain timeline-add` calls.
|
|
```
|
|
|
|
---
|
|
|
|
## After all four diffs are applied
|
|
|
|
1. **Bump the version banner** at the top of each forked file:
|
|
```
|
|
# Based on gbrain v0.12.0 skills/<skill-name>, extended with <your-agent>-specific config
|
|
```
|
|
|
|
2. **Run the v0.12.0 backfill** (this populates the graph for your existing brain):
|
|
```bash
|
|
gbrain post-upgrade
|
|
```
|
|
The v0.12.0 release wires post-upgrade to call `apply-migrations --yes`
|
|
automatically, which runs the v0_12_0 orchestrator (schema → config check →
|
|
`extract links --source db` → `extract timeline --source db` → verify).
|
|
Idempotent; cheap when nothing is pending.
|
|
|
|
3. **Verify auto-link works:** ask the agent to write a test page that references
|
|
`[Some Person](people/some-person)`. Confirm the put_page response includes
|
|
`auto_links: { created: 1, removed: 0, errors: 0 }`.
|
|
|
|
4. **Verify graph traversal works:**
|
|
```bash
|
|
gbrain graph-query people/some-well-connected-person --depth 2
|
|
```
|
|
Should return an indented tree of typed edges.
|
|
|
|
---
|
|
|
|
## v0.12.2 hotfix (data-correctness, no skill edits)
|
|
|
|
v0.12.2 is a Postgres data-correctness hotfix. No forked skill files need to
|
|
change — the skill contracts are unchanged. But you DO need to run the migration,
|
|
and you should know about one behavior change in markdown parsing.
|
|
|
|
### 1. Run the migration (Postgres-backed brains)
|
|
|
|
```bash
|
|
gbrain upgrade
|
|
```
|
|
|
|
The `v0_12_2` orchestrator runs `gbrain repair-jsonb` automatically. It rewrites
|
|
rows where `jsonb_typeof = 'string'` across `pages.frontmatter`, `raw_data.data`,
|
|
`ingest_log.pages_updated`, `files.metadata`, and `page_versions.frontmatter`.
|
|
Idempotent, safe to re-run. PGLite brains no-op cleanly.
|
|
|
|
Verify after upgrade:
|
|
|
|
```bash
|
|
gbrain repair-jsonb --dry-run --json # expect totalRepaired: 0
|
|
```
|
|
|
|
### 2. Recover any truncated wiki articles
|
|
|
|
If your brain imported wiki-style markdown before v0.12.2, some pages were
|
|
silently truncated (any standalone `---` in body content was treated as a
|
|
timeline separator). Re-import from source:
|
|
|
|
```bash
|
|
gbrain sync --full
|
|
```
|
|
|
|
The new `splitBody` rebuilds `compiled_truth` correctly.
|
|
|
|
### 3. Know the splitBody contract going forward
|
|
|
|
`splitBody` now requires an explicit timeline sentinel. Recognized markers
|
|
(priority order):
|
|
|
|
1. `<!-- timeline -->` (preferred — what `serializeMarkdown` emits)
|
|
2. `--- timeline ---` (decorated separator)
|
|
3. `---` directly before `## Timeline` or `## History` heading (backward-compat)
|
|
|
|
A bare `---` in body text is now a markdown horizontal rule, not a timeline
|
|
separator. If your agent writes pages with a bare `---` delimiter, migrate to
|
|
`<!-- timeline -->` — the `serializeMarkdown` helper already does this.
|
|
|
|
### 4. Wiki subtypes now auto-typed
|
|
|
|
`inferType` now auto-detects five additional directory patterns as their own
|
|
page types (previously they all defaulted to `concept`):
|
|
|
|
| Path pattern | New type |
|
|
|------------------------|----------------|
|
|
| `/wiki/analysis/` | `analysis` |
|
|
| `/wiki/guides/` | `guide` |
|
|
| `/wiki/hardware/` | `hardware` |
|
|
| `/wiki/architecture/` | `architecture` |
|
|
| `/writing/` | `writing` |
|
|
|
|
If your skills or queries filter by `type=concept` and expect wiki content in
|
|
that bucket, update them to include the new types.
|
|
|
|
---
|
|
|
|
## v0.13.0 — Frontmatter Relationship Indexing
|
|
|
|
**Verdict: no action required for most skills.** v0.13 projects YAML frontmatter fields into the graph as typed edges. The ingestion API is unchanged — keep calling `put_page` with frontmatter the way you do today; the graph auto-populates behind the scenes.
|
|
|
|
Three skills get an optional new phase if you want to consume the new `auto_links.unresolved` response field. Without this, unresolvable frontmatter names silently skip (same as v0.12 behavior).
|
|
|
|
### 1. meeting-ingestion/SKILL.md (optional)
|
|
|
|
**Where:** Add a new section after "Phase 3: Write Meeting Page".
|
|
|
|
```markdown
|
|
### Phase 3.5: Check for unresolved attendees (v0.13+)
|
|
|
|
After `put_page`, inspect `response.auto_links.unresolved` — an array of frontmatter
|
|
references that did not resolve to existing pages. For meetings, this usually means
|
|
attendees you haven't created a person page for yet.
|
|
|
|
If `unresolved.length > 0`:
|
|
- Option 1 (create pages now): trigger an enrichment pass to build the missing people pages.
|
|
- Option 2 (defer): log the unresolved names to the enrichment queue for later.
|
|
- Option 3 (accept the gap): the attendee edge will not be created until a page exists.
|
|
Re-running `gbrain extract links --source db --include-frontmatter` after creating
|
|
the page fills in the missing edges.
|
|
```
|
|
|
|
### 2. enrich/SKILL.md (optional)
|
|
|
|
**Where:** Add to the enrichment trigger list.
|
|
|
|
```markdown
|
|
### Drain unresolved frontmatter names (v0.13+)
|
|
|
|
If any `put_page` response includes `auto_links.unresolved` entries, the enrichment
|
|
tier should pick up those (field, name) pairs and try to create the missing entity
|
|
pages. Example flow:
|
|
|
|
1. signal-detector captures a meeting with `attendees: [Alice Known, Unknown Person]`
|
|
2. put_page returns `auto_links.unresolved = [{field: 'attendees', name: 'Unknown Person'}]`
|
|
3. enrichment tier consumes `Unknown Person` → web search → creates `people/unknown-person.md`
|
|
4. The next put_page (or a backfill run) wires up the `attended` edge automatically
|
|
```
|
|
|
|
### 3. idea-ingest/SKILL.md (optional)
|
|
|
|
**Where:** Same pattern as meeting-ingestion — check `auto_links.unresolved` after `put_page`, route names to enrichment.
|
|
|
|
### Unchanged skills (no diffs needed)
|
|
|
|
- **brain-ops/SKILL.md** — auto-link mechanics are internal; the write path stays the same.
|
|
- **signal-detector/SKILL.md** — signal capture path unchanged.
|
|
- **query/SKILL.md** — `traverse_graph` now returns richer results automatically.
|
|
- **daily-task-manager/SKILL.md**, **briefing/SKILL.md**, **citation-fixer/SKILL.md**, **media-ingest/SKILL.md** — unchanged.
|
|
|
|
### New edge types you can filter in graph queries
|
|
|
|
v0.13 edges carry new `link_type` values. If your fork has graph-query skills that filter by type, these are now available:
|
|
|
|
- `works_at` (person → company) — from `company:`, `companies:`, or `key_people:`
|
|
- `founded` (person → company) — from `founded:`
|
|
- `invested_in` (investor → deal/company) — from `investors:` or `lead:`
|
|
- `led_round` (lead → deal) — from `lead:`
|
|
- `yc_partner` (partner → company) — from `partner:`
|
|
- `attended` (person → meeting) — from `attendees:`
|
|
- `discussed_in` (source → page) — from `sources:`
|
|
- `source` (page → source) — from `source:`
|
|
- `related_to` (page → target) — from `related:` or `see_also:`
|
|
|
|
### Migration timing
|
|
|
|
`gbrain upgrade` takes 2-5 min on a 46K-page brain (one-time). Runs out-of-process via `gbrain post-upgrade`. If your agent holds a DB connection during the upgrade, reconnect after; otherwise keep serving.
|
|
|
|
### Type normalization NOT in v0.13
|
|
|
|
Legacy rows with `link_type='attendee'` or `link_type='mention'` coexist with new `'attended'` / `'mentions'` rows. Your queries filtering on old type names keep working. A separate opt-in `gbrain normalize-types` command in v0.14 handles the rename.
|
|
## v0.14.0 shell jobs (optional adoption, no skill edits)
|
|
|
|
Adds a `shell` job type to Minions so deterministic cron scripts (API fetch, token
|
|
refresh, scrape + write) move off the LLM gateway. Zero tokens per fire. ~60%
|
|
gateway CPU headroom at typical scale. Feature is **off by default**, existing
|
|
installs keep running exactly as they did before. Nothing breaks.
|
|
|
|
To adopt, follow `skills/migrations/v0.14.0.md`. The short version:
|
|
|
|
1. Set `GBRAIN_ALLOW_SHELL_JOBS=1` on the worker process, then `gbrain jobs work`
|
|
(Postgres). On PGLite, every crontab invocation uses `--follow` for inline
|
|
execution; no persistent worker.
|
|
2. Classify each of your host's cron entries: LLM-requiring (keep on gateway) vs
|
|
deterministic (candidate for shell). Typical splits:
|
|
- **Deterministic → shell:** `ycli-token-refresh`, `x-oauth2-refresh`,
|
|
`x-garrytan-unified`, `calendar-sync-to-brain`, `github-pulse`,
|
|
`frameio-scan`, `flight-tracker`, `x-raw-json-backfill`.
|
|
- **LLM-requiring → stay:** `social-radar`, `content-ideas`, `adversary-vacuum`,
|
|
`ea-inbox-sweep`, `morning-briefing`, `brain-maintenance`.
|
|
3. For each deterministic cron, rewrite as:
|
|
```cron
|
|
3 13,16,19,22,1,4,7,10 * * * \
|
|
gbrain jobs submit shell \
|
|
--params '{"cmd":"node scripts/your-script.mjs","cwd":"/data/.openclaw/workspace"}' \
|
|
--max-attempts 3 --timeout-ms 300000
|
|
```
|
|
4. Watch `gbrain jobs get <id>` for exit_code / stdout_tail / stderr_tail on each
|
|
fire. Compare against pre-migration behavior before approving the next batch.
|
|
|
|
**No skill edits required.** The handler runs worker-side; skill files don't
|
|
change. If your host exposed custom handlers via the plugin contract (v0.11.0),
|
|
they still work the same way.
|
|
|
|
Iron rule: **never auto-rewrite the operator's crontab.** Every rewrite is
|
|
per-cron, human-approved, with a diff. If you want automation later, the
|
|
upcoming `gbrain crontab-to-minions <file>` helper is P1 in TODOS.
|
|
|
|
---
|
|
|
|
## v0.16.0: durable agent runtime
|
|
|
|
v0.15 ships `gbrain agent run` / `gbrain agent logs`, a new `subagent` handler
|
|
type in Minions, and a plugin contract for host-repo subagent defs. None of the
|
|
existing skills need surgery. The question for downstream agents is *how* to
|
|
adopt the new runtime, not how to patch around a breaking change.
|
|
|
|
### 1. Run a worker with an Anthropic key
|
|
|
|
The subagent handlers (`subagent` and `subagent_aggregator`) are always
|
|
registered on the worker. No separate opt-in flag — `ANTHROPIC_API_KEY` is
|
|
the natural cost gate (no key, the SDK call fails on the first turn), and
|
|
who-can-submit is already protected (`PROTECTED_JOB_NAMES` + trusted-submit:
|
|
MCP callers get `permission_denied`; only `gbrain agent run` can insert
|
|
these rows).
|
|
|
|
```bash
|
|
ANTHROPIC_API_KEY=sk-ant-... gbrain jobs work
|
|
```
|
|
|
|
Worker startup prints:
|
|
|
|
```
|
|
[minion worker] subagent handlers enabled
|
|
```
|
|
|
|
### 2. Ship your subagents as a plugin (OpenClaw + similar)
|
|
|
|
Move your custom subagent definitions out of your gbrain fork and into your own
|
|
repo as a plugin. Concretely:
|
|
|
|
```
|
|
~/<your-agent>/gbrain-plugin/
|
|
├── gbrain.plugin.json
|
|
└── subagents/
|
|
├── meeting-ingestion.md
|
|
├── signal-detector.md
|
|
└── daily-task-prep.md
|
|
```
|
|
|
|
`gbrain.plugin.json`:
|
|
|
|
```json
|
|
{
|
|
"name": "your-openclaw",
|
|
"version": "2026.4.20",
|
|
"plugin_version": "gbrain-plugin-v1"
|
|
}
|
|
```
|
|
|
|
Each `subagents/*.md` is a plain-text agent definition — YAML frontmatter +
|
|
body-as-system-prompt. Recognized frontmatter fields: `name`, `model`,
|
|
`max_turns`, `allowed_tools` (must subset the derived brain-tool registry).
|
|
|
|
Turn it on:
|
|
|
|
```bash
|
|
export GBRAIN_PLUGIN_PATH="$HOME/<your-agent>/gbrain-plugin"
|
|
```
|
|
|
|
Worker startup prints `[plugin-loader] loaded '<name>' v<ver> (N subagents)`
|
|
per plugin; any rejection (bad manifest, unknown tool in `allowed_tools`,
|
|
version mismatch) shows up as a loud warning at startup, not a silent dispatch-
|
|
time failure. See `docs/guides/plugin-authors.md` for the full contract.
|
|
|
|
### 3. Replace ephemeral subagent runs with durable ones
|
|
|
|
If your agent currently spawns ephemeral subagents (OpenClaw `Agent()`, ad-hoc
|
|
Anthropic API calls, etc.) for work that should survive crashes, sleeps, or
|
|
worker restarts, migrate those to `gbrain agent run`. The durability is free:
|
|
|
|
```bash
|
|
gbrain agent run "analyze my last 50 journal pages for recurring themes" \
|
|
--subagent-def analyzer --fanout-manifest manifests/journal-pages.json
|
|
```
|
|
|
|
Every turn persists to `subagent_messages`, every tool call is a two-phase
|
|
ledger, and `gbrain agent logs <job>` shows where it died + what the last
|
|
successful call returned. No more "re-run from scratch because the session
|
|
context evaporated."
|
|
|
|
### 4. `put_page` from subagents writes under an agent namespace
|
|
|
|
If you adopted the v0.15 subagent runtime, note that `put_page` calls
|
|
originating from a subagent's tool dispatch MUST target
|
|
`wiki/agents/<subagent_id>/...`. The schema shown to the model enforces this
|
|
on first try; a server-side fail-closed check rejects anything else. This
|
|
does NOT affect your skill files, CLI put_page calls, or MCP put_page —
|
|
only tool-dispatched writes from inside an LLM loop.
|
|
|
|
Aggregation output (the final "here's what all N children found" brain page)
|
|
goes via a separate trusted CLI path, not through a subagent tool call, so
|
|
it can write anywhere you want.
|
|
|
|
Iron rule: **never grant an agent write access beyond its namespace**. The
|
|
server-side check exists because dispatcher bugs happen; treat it as defense
|
|
in depth, not the primary boundary.
|
|
|
|
---
|
|
|
|
## Future versions
|
|
|
|
When gbrain ships a new version, this doc will be updated with the diffs for that
|
|
version. Each new version appends a section; old sections stay so you can catch up
|
|
multiple versions at once.
|
|
|
|
To check what your fork is missing:
|
|
```bash
|
|
diff <(grep -A3 "Based on gbrain" ~/<your-fork>/skills/brain-ops/SKILL.md) \
|
|
<(grep "v[0-9]" ~/gbrain/skills/migrations/ | tail -3)
|
|
```
|