* feat(minions): add protected-name constant + ctx.shutdownSignal
Introduce PROTECTED_JOB_NAMES ('shell') in a side-effect-free core module
so queue.ts can check it without importing from handlers/. MinionJobContext
gains shutdownSignal (distinct from signal) — handlers that need to run
SIGTERM-triggered cleanup subscribe to both; most handlers ignore shutdown
and run through the worker's 30s cleanup race to natural completion.
* fix(minions): MinionQueue.add gains trusted 4th arg + trim-normalized guard
Adds allowProtectedSubmit opt-in as a separate 4th parameter (NOT folded into
opts) so callers spreading user-provided opts ({...userOpts}) can't accidentally
carry the trust flag. PROTECTED_JOB_NAMES check runs on the trimmed name BEFORE
insert, closing the queue.add(' shell ', ...) whitespace bypass that would have
evaded a has(name) check.
* fix(minions): worker calls failJob on abort + wires ctx.shutdownSignal
Pre-v0.13.0 worker returned silently when ctx.signal.aborted fired, leaving
jobs in 'active' until stall sweep. Handlers using cooperative cancel had
no deterministic status flip — timeout/cancel/lock-loss all looked the same
from downstream callers (gbrain jobs get, --follow loops).
Fix: derive abort reason from abort.signal.reason ('timeout' | 'cancel' |
'lock-lost' | 'shutdown') and call failJob with 'aborted: <reason>' text.
failJob is idempotent via token+status match, so no-op when another path
already flipped status (handleTimeouts, cancelJob, stall).
Also: new shutdownAbort (instance-level AbortController) fires on process
SIGTERM/SIGINT and propagates to every handler's ctx.shutdownSignal.
Shell handler listens to both signals and runs SIGTERM→5s→SIGKILL on its
child on either; other handlers only listen to ctx.signal so deploy
restarts don't cancel them mid-flight.
* feat(minions): add shell job handler + submission audit log
New 'shell' job type spawns arbitrary commands under the Minions worker.
Deterministic cron scripts (API fetch, token refresh, scrape+write) can
move off the LLM gateway — zero Opus tokens per fire.
Handler contract:
- cmd or argv (exactly one required). cmd spawns via /bin/sh -c (absolute
path, not 'sh', to block PATH-override shell substitution). argv spawns
direct with no shell.
- cwd required, must be absolute. Operator-trust boundary.
- env defaults to SHELL_ENV_ALLOWLIST ({PATH, HOME, USER, LANG, TZ,
NODE_ENV}) picked from process.env, with caller overrides merged on top.
Prevents accidental $OPENAI_API_KEY interpolation into scripts.
- stdout/stderr retained as UTF-8-safe tails (64KB/16KB) via
string_decoder.StringDecoder. Prepends [truncated N bytes] marker.
- Abort (either ctx.signal or ctx.shutdownSignal) fires SIGTERM → 5s grace
→ SIGKILL on child. Timer NOT .unref'd so worker's 30s race waits for
the child to actually die.
shell-audit.ts writes a JSONL line per submission to
~/.gbrain/audit/shell-jobs-YYYY-Www.jsonl (ISO-week rotated, override via
GBRAIN_AUDIT_DIR). argv logged as JSON array (not space-joined, which would
flatten args with spaces). Never logs env values. Best-effort writes:
failures log to stderr but don't block submission.
* feat(jobs): submit_job MCP guard + CLI --timeout-ms + starvation warning
submit_job operation gains timeout_ms param (was missing — couldn't plumb
the existing MinionJobInput field through from either CLI or MCP). When
ctx.remote=true and name is in PROTECTED_JOB_NAMES, throws
OperationError('permission_denied'). Combined with the queue.add trusted
guard, MCP callers can never submit shell jobs even if the env flag is on.
CLI submit: new --timeout-ms N flag. Passes {allowProtectedSubmit:true}
as the 4th arg to queue.add only when the submitted name is protected
(not blanket-set for every job). Prints a starvation-warning block to
stderr when a shell job is submitted without --follow, pointing at both
--follow and 'gbrain jobs work' remediation. Fires for every shell submit
regardless of the submitter's env — the submitter env is a weak proxy for
the worker env.
Worker handler registration: conditional on GBRAIN_ALLOW_SHELL_JOBS=1.
Default: off. 'gbrain jobs submit --help' now lists handler types with a
pointer to docs/guides/minions-shell-jobs.md for shell.
* test(minions): 40 unit + 4 E2E cases for shell handler
Unit (test/minions-shell.test.ts):
- Protected names: trim-normalized, case-sensitive, whitespace bypass defense
- MinionQueue.add: trusted opt-in, whitespace bypass, non-protected untouched
- Handler validation: cmd|argv exclusive, cwd required/absolute, env strings
- Spawn: cmd/argv happy paths, non-zero exit, ENOENT, result shape
- Env allowlist: leaked-secret blocked, PATH inherited, caller override
- Abort: ctx.signal, ctx.shutdownSignal, pre-aborted signal
- Audit: ISO-week year boundary (2027-01-01 → W53 2026), mid-year W52/W53,
GBRAIN_AUDIT_DIR override, argv as JSON array, env never logged, EACCES
non-blocking
- Output truncation: 100KB → last 64KB with [truncated N bytes] marker
E2E (test/e2e/minions-shell.test.ts):
- Full lifecycle: submit → worker claim → spawn → complete
- MinionQueue.add without trusted arg throws (including whitespace bypass)
- submit_job with ctx.remote=true rejects shell (MCP guard)
- submit_job with ctx.remote=false allows shell (CLI path)
* chore: bump version and changelog (v0.13.0)
Move gateway crons to Minions. Zero LLM tokens per cron fire.
Worker abort path finally marks aborted jobs dead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: reframe v0.13.0 copy for OpenClaw operators (not Wintermute-specific)
gbrain is an open-source product for any OpenClaw/Hermes operator, not
Garry's personal Wintermute deployment. Rewords the v0.13.0 CHANGELOG
entry, the minions-shell-jobs guide, and the deferred TODOS entries to
speak to "your OpenClaw" / "OpenClaw operators" instead.
Replaces /data/wintermute cwd examples with the canonical
/data/.openclaw/workspace path. Pre-existing Wintermute references in
older CHANGELOG entries (v0.11/v0.10.3) left unchanged.
* feat(migrations): add v0.13.0 adoption playbook for shell jobs
Adding the migration file the CEO review originally scoped out. Without
it, operators upgrade to v0.13.0 and the capability ships but adoption
doesn't happen — the 60% gateway CPU reduction only lands if someone
actually rewrites their crontab.
skills/migrations/v0.13.0.md is the instruction manual the host agent
reads on gbrain upgrade:
- Enable worker: GBRAIN_ALLOW_SHELL_JOBS=1 gbrain jobs work (Postgres)
or per-tick --follow (PGLite)
- Audit cron manifest: classify LLM-requiring vs deterministic
- Propose per-cron rewrites with diffs, approved one at a time
- Env allowlist guidance for scripts that need API keys
- Verification playbook: run one fire, compare pre/post, only then
approve the next batch
- Starvation sanity-check runbook item
Iron rules: never auto-rewrite the operator's crontab (host-specific
code per CLAUDE.md). LLM-requiring crons stay on the gateway. Ambiguous
cases ask the operator.
No mechanical orchestrator ships with this migration — every rewrite
is operator judgment. A future gbrain crontab-to-minions helper is
tracked in TODOS.md as P1.
* docs: sync UPGRADING + SKILLPACK with v0.13.0 shell jobs
UPGRADING_DOWNSTREAM_AGENTS.md: append v0.13.0 section per the file's
convention (each release appends). No skill edits required, feature is
off-by-default, optional adoption via skills/migrations/v0.13.0.md.
Lists typical LLM-vs-deterministic classifications so operators know
which of their crons are candidates for migration.
GBRAIN_SKILLPACK.md: add shell-jobs guide row to the cron/Minions guide
table so it's discoverable alongside existing Cron via Minions, Plugin
Handlers, and Minions fix guides.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
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
- Identify your forked skill files. Typically at
~/git/<your-agent>/workspace/skills/or wherever your agent's skill directory lives. - For each skill listed below, find the matching phase/section in your fork.
- Apply the diff (paste the new block in the indicated location).
- Update the version banner at the top of your fork (
# Based on gbrain v0.12.0). - 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.
### 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:
**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)).
**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:
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.
**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):
### 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):
### 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
-
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 -
Run the v0.12.0 backfill (this populates the graph for your existing brain):
gbrain post-upgradeThe v0.12.0 release wires post-upgrade to call
apply-migrations --yesautomatically, 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. -
Verify auto-link works: ask the agent to write a test page that references
[Some Person](people/some-person). Confirm the put_page response includesauto_links: { created: 1, removed: 0, errors: 0 }. -
Verify graph traversal works:
gbrain graph-query people/some-well-connected-person --depth 2Should 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)
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:
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:
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):
<!-- timeline -->(preferred — whatserializeMarkdownemits)--- timeline ---(decorated separator)---directly before## Timelineor## Historyheading (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".
### 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.
### 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_graphnow 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) — fromcompany:,companies:, orkey_people:founded(person → company) — fromfounded:invested_in(investor → deal/company) — frominvestors:orlead:led_round(lead → deal) — fromlead:yc_partner(partner → company) — frompartner:attended(person → meeting) — fromattendees:discussed_in(source → page) — fromsources:source(page → source) — fromsource:related_to(page → target) — fromrelated:orsee_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:
- Set
GBRAIN_ALLOW_SHELL_JOBS=1on the worker process, thengbrain jobs work(Postgres). On PGLite, every crontab invocation uses--followfor inline execution; no persistent worker. - 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.
- Deterministic → shell:
- For each deterministic cron, rewrite as:
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 - 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.
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:
diff <(grep -A3 "Based on gbrain" ~/<your-fork>/skills/brain-ops/SKILL.md) \
<(grep "v[0-9]" ~/gbrain/skills/migrations/ | tail -3)