mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 21:19:18 +00:00
v0.20.4 feat: merge gbrain-jobs into minion-orchestrator — single unified minions skill (#381)
* feat: merge gbrain-jobs into minion-orchestrator — single unified minions skill
* fix(skill/minion-orchestrator): correct MCP boundary, real handler names, PGLite path
The initial merge commit a51c737 documented `submit_job name="shell"` as
agent-callable, but src/core/operations.ts:1106 rejects protected names
from MCP callers (shell is in src/core/minions/protected-names.ts:16) —
shell-job submission is CLI-only. Subagent examples referenced non-existent
handler names (`research`, `orchestrate`) instead of the real `subagent` /
`subagent_aggregator` handlers. PGLite section wrongly told users to
migrate to Supabase when `gbrain jobs submit ... --follow` inline mode
works per docs/guides/minions-shell-jobs.md:15. Contract section canonized
"every task through Minions" against the `pain_triggered` default in
skills/conventions/subagent-routing.md:16,27.
Rewrite addresses all four:
- Shell Jobs section is explicit about CLI-only submission; agents observe
via get_job / list_jobs / get_job_progress (non-protected).
- Subagent examples route through `gbrain agent run` (user-facing CLI)
with raw handler names documented as the power-user path.
- PGLite gets --follow inline execution, not migration friction.
- Contract softened to point at subagent-routing.md convention.
Also adds a Preconditions block for Shell Jobs (env gate, RCE warning,
execution-mode choice, verification command), narrows the frontmatter
"gbrain jobs" trigger to "gbrain jobs submit" + "submit a gbrain job"
(bare was too broad — CLI namespace covers 9 subcommands), inlines a
"replaces older gbrain-jobs routing intent" note in the description, and
removes non-existent `get_job_stats` from the tools list (CLI is
`gbrain jobs stats`; no MCP equivalent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(resolver): narrow "gbrain jobs" trigger to specific intents
Replace bare "gbrain jobs" in the routing table with "gbrain jobs submit"
+ "submit a gbrain job". The bare phrase was too broad — the CLI namespace
covers 9 subcommands (submit, list, get, retry, delete, prune, stats,
smoke, work). Users asking about stats/prune/retry now fall through to
`gbrain --help` instead of getting misrouted to minion-orchestrator, which
only documents shell execution and subagent orchestration.
Matches the frontmatter trigger narrow in minion-orchestrator/SKILL.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(resolver): add round-trip + skill-example-name validator
Two new assertion blocks in test/resolver.test.ts:
1. RESOLVER.md trigger round-trip: every quoted phrase in a routing-table
row has a fuzzy match in the target skill's frontmatter `triggers:` list.
Catches RESOLVER ↔ frontmatter drift that checkResolvable's reachability
check doesn't. Fuzzy match is case-insensitive, trailing-punctuation-
insensitive, and splits on "/" for compound phrases like
"pause/resume agent" — accommodates RESOLVER.md's natural-language
summary style without allowing real drift through.
2. Skill example-name validator: every `name="<word>"` reference in any
SKILL.md body must resolve to either a declared operation in
src/core/operations.ts or a known Minions handler in
PROTECTED_JOB_NAMES. Would have caught the `name="research"` /
`name="orchestrate"` drift that slipped through the first review
— nothing in CI caught those handler names referencing non-existent
handlers until a Codex cold-read found them. This test closes that
class of regression gap.
51 / 51 tests pass locally. Full E2E suite (bun run test:e2e) still
passes 197 / 197 across 19 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): PGLite shell-job --follow inline path
Closes the T4 coverage gap surfaced during PR #381 eng review. The sibling
test/e2e/minions-shell.test.ts covers Postgres + persistent-daemon; this
file covers the PGLite + --follow path the minion-orchestrator skill now
documents.
Two assertions:
1. submit → registerBuiltinHandlers → worker.start → shell runs → completes
with exit_code 0 and stdout_tail "hello\n". Exercises the exact dispatch
path src/commands/jobs.ts:207 takes when --follow is set, including the
GBRAIN_ALLOW_SHELL_JOBS=1 gate.
2. With GBRAIN_ALLOW_SHELL_JOBS unset, registerBuiltinHandlers leaves the
shell handler unregistered. Confirms the env gate from
src/commands/jobs.ts:611 works.
Runs in-memory against PGLiteEngine — no DATABASE_URL, no Docker, runs in
CI unconditionally. Completes in ~1.2s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: pre-landing review fixes
Pre-landing review caught 4 doc bugs + 2 test fragilities + 2 pre-existing
drift cases. All auto-fix category (clear correct answer, single obvious fix).
minion-orchestrator/SKILL.md:
- Shell submit examples used nonexistent `--cmd`/`--argv`/`--cwd` flags. Real
CLI takes `--params '{"cmd":"...","cwd":"..."}'` (src/commands/jobs.ts:55-85).
Examples now match `gbrain jobs submit --help` output.
- `--tools "search,web_search"` referenced `web_search` which isn't in
BRAIN_TOOL_ALLOWLIST (src/core/minions/tools/brain-allowlist.ts:47-59).
Swapped to `search,query`. Added a full allowlist enumeration so
readers don't have to grep.
- `gbrain agent run` flags section listed `--queue`, `--priority`,
`--max-attempts`, `--delay` — none of these exist on that command
(src/commands/agent.ts:105-129). Replaced with the real flag set
(`--subagent-def`, `--model`, `--max-turns`, `--tools`, `--timeout-ms`,
`--fanout-manifest`, `--follow`, `--no-follow`, `--detach`) and a note
about using `gbrain jobs submit` for queue tuning.
- MCP boundary claim "returns permission_denied" was imprecise. Reworded:
throws an OperationError with code permission_denied.
test/resolver.test.ts:
- D5/C row regex required the backtick-quoted skill path to be followed
immediately by `|`, silently skipping rows with trailing parentheticals
(e.g., `` `skills/maintain/SKILL.md` (extraction sections) |``). Broadened
to `[^|]*\|` so every row gets audited.
test/e2e/minions-shell-pglite.test.ts:
- Shared engine across both tests with no per-test reset. Future test
additions would hit order-dependency. Added beforeEach TRUNCATE on
minion_jobs / minion_inbox / minion_attachments, matching the Postgres
sibling at test/e2e/minions-shell.test.ts:55-58.
skills/query/SKILL.md:
- Added 4 triggers RESOLVER.md routes to this skill but the frontmatter
never declared: "who knows who", "relationship between", "connections",
"graph query". Pre-existing drift — the broadened D5/C regex surfaced it.
skills/maintain/SKILL.md:
- Added 6 triggers with the same pre-existing drift: "extract links",
"build link graph", "populate timeline", "populate links", "backfill graph",
"extract timeline entries".
57/57 tests pass on the fixed tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: second-pass review fixes — stale CLI flag + handler name
Two more stale references caught by specialist re-dispatch on the fixed tree:
skills/minion-orchestrator/SKILL.md:72 — Routing table row described shell
jobs as taking `--cmd` or `--argv` as CLI flags. Same class of bug as M1
from the prior fix commit but in a different location. Now says `--params`
with `cmd` or `argv`, matching the corrected submit examples (lines 112-120).
skills/conventions/subagent-routing.md:82 — "Check `get_job_stats`
queue_health.active" referenced an MCP operation that doesn't exist in
src/core/operations.ts. The new minion-orchestrator skill cross-references
this convention file, so agents following the routing pointer would hit a
non-existent op. Replaced with the real ops: `list_jobs --status active`
(MCP) or `gbrain jobs stats` (CLI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: adversarial pass cleanups — manifest.json + anti-pattern scope
Claude adversarial subagent caught two last consistency gaps:
skills/manifest.json:135 — Skill description still read "Manage background
agents via Minions job queue" (subagent-only framing), out of sync with
the reframed SKILL.md frontmatter. Manifest is what the skill registry
indexes; leaving this stale meant shell-job-intent routers would miss it.
Updated to match the unified wording.
skills/minion-orchestrator/SKILL.md:288 — Anti-pattern line "Don't use
sessions_spawn with runtime: subagent when Minions is available" was
subagent-lane-specific inside the now-consolidated skill, reading like
the one rule in the skill but only addressing one lane. Scoped to
"For subagent work" and pointed at `gbrain agent run` so the rule
doesn't confuse shell-job readers.
Two investigate-class items deferred to follow-up:
- D13 regex could false-positive on future skills with unrelated `name="..."`
usage. Today clean; scope to backtick-fenced snippets if it bites.
- PGLite E2E env-var race if bun:test ever goes file-parallel. Today isolated
per file; add helper + comment when needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v0.19.2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: update README + CLAUDE.md for v0.19.2 Minions consolidation
- Skill count 28 -> 29 across README and CLAUDE.md (adds smoke-test from
v0.19.1 to the Skills section, closes a prior drift).
- README minion-orchestrator row rewritten to name both lanes (shell jobs
via `gbrain jobs submit shell`, LLM subagents via `gbrain agent run`)
so the surface matches the consolidated skill file.
- README Operational table gains a smoke-test row.
- CLAUDE.md key-files entry for minion-orchestrator now describes the
v0.19.2 consolidation, trust boundary (MCP permission_denied on
protected names), and the narrowed trigger set.
- CLAUDE.md Skills section notes the consolidation and the new v0.19.1
smoke-test skill.
- CLAUDE.md test inventory picks up `test/e2e/minions-shell-pglite.test.ts`
and the v0.19.2 round-trip + name-validator additions in
`test/resolver.test.ts`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(ci): update PGLite test for new env-gate behavior + regenerate llms-full.txt
CI caught two issues:
1. `test/e2e/minions-shell-pglite.test.ts` — the "GBRAIN_ALLOW_SHELL_JOBS
unset → shell handler not registered" test was written against pre-v0.20.3
`registerBuiltinHandlers` behavior (env gate at registration time). Master's
queue-resilience merge moved the gate from registration to execution:
shell handler is now always registered so claimed jobs emit a clear rejection
log, and `shellHandler` itself throws UnrecoverableError when
GBRAIN_ALLOW_SHELL_JOBS != '1' (see src/core/minions/handlers/shell.ts:210).
Updated the test to invoke shellHandler directly with a minimal ctx and
assert the throw. Preserves the test's intent (prove the guard works) under
the new control flow.
2. `llms-full.txt` drift — README.md + CLAUDE.md updates in v0.19.2 and v0.20.4
updated the skill count to 29 and rewrote the minion-orchestrator
description, but the committed `llms-full.txt` bundle still reflected the
pre-consolidation content. Regenerated via `bun run build:llms`.
The third CI failure (`planInstall + applyInstall D-CX-11`) passes cleanly
locally (26/26 in test/skillpack-install.test.ts). The 1ms runtime in CI
suggests a filesystem-mtime flake, not a real regression from this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skillpack): treat future-mtime lock as stale (CI race fix)
D-CX-11 ("--force-unlock overrides a stale lock") flaked in CI with a 1ms
runtime. Root cause: on fast CI filesystems (ext4 with high-resolution
mtimes on GitHub runners), `writeFileSync` can set a lock's mtime a few
microseconds ahead of the subsequent `Date.now()`, making `age` negative.
Old logic:
const stale = age >= staleMs;
With `staleMs: 0` and `age = -0.3ms`: `-0.3 >= 0` is false → NOT stale →
the `!stale` branch throws `lock_held` before reaching the force-unlock
path. Test failed at the first ms, never exercised the actual unlock logic.
Fix (src/core/skillpack/installer.ts:189):
const stale = age < 0 || age >= staleMs;
Treats negative age (future mtime) as stale. Safe: if the lock's mtime is
in the future, either the filesystem clock just jumped forward or the
lock was written by a racing process; either way it's not a live,
healthy lock and the stale path is the correct branch.
Passes locally (26/26 in test/skillpack-install.test.ts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: root <root@localhost>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
root
Claude Opus 4.7
parent
d838d4792b
commit
11abb24ddd
@@ -2,6 +2,63 @@
|
||||
|
||||
All notable changes to GBrain will be documented in this file.
|
||||
|
||||
## [0.20.4] - 2026-04-24
|
||||
|
||||
**Minions skill consolidation, now honest about what the CLI actually does.**
|
||||
|
||||
One skill for background work instead of two. Shell jobs and LLM subagents land under `skills/minion-orchestrator/` with a shared Preconditions block, accurate CLI examples, and a trigger set narrowed to what the skill actually covers. Corrects four documentation bugs the prior merge shipped ... `submit_job name="shell"` isn't MCP-callable, `research`/`orchestrate` aren't real handler names, PGLite users don't need to migrate to Supabase, and "every background task goes through Minions" contradicts the `pain_triggered` default in `skills/conventions/subagent-routing.md`. The skill now matches the code.
|
||||
|
||||
Two new tests guard this surface going forward. `test/resolver.test.ts` gets a round-trip check (every quoted RESOLVER.md trigger must resolve to a frontmatter `triggers:` entry in the target skill) and a name validator (every `name="<word>"` reference in any SKILL.md must resolve to either a declared operation in `src/core/operations.ts` or a known Minions handler). The validator would have caught the `research`/`orchestrate` drift in CI instead of from a Codex cold-read. One new E2E test (`test/e2e/minions-shell-pglite.test.ts`) exercises the PGLite `--follow` inline path, previously documented but untested.
|
||||
|
||||
### For users
|
||||
|
||||
- Shell jobs via `gbrain jobs submit shell --params '{"cmd":"..."}'` (operator/CLI only ... MCP returns `permission_denied` for protected names). Subagent jobs via `gbrain agent run` (user-facing entrypoint). Both lanes route through one skill.
|
||||
- PGLite shell-job guidance now correctly points at `--follow` for inline execution. The persistent daemon mode is still Postgres-only, but you do not need to migrate.
|
||||
- `gbrain jobs submit` and `submit a gbrain job` now route to the skill; bare "gbrain jobs" no longer does (it was too broad ... the CLI namespace covers 9 subcommands, and questions about `stats`/`prune`/`retry` fall through to `gbrain --help`).
|
||||
|
||||
### Added
|
||||
|
||||
- New E2E test `test/e2e/minions-shell-pglite.test.ts` covering the PGLite `--follow` inline shell-job path. Runs in-memory, no DATABASE_URL required.
|
||||
- Resolver round-trip test in `test/resolver.test.ts`: every quoted RESOLVER.md trigger must have a fuzzy match in the target skill's frontmatter `triggers:` list.
|
||||
- Skill-example-name validator in `test/resolver.test.ts`: every `name="<word>"` reference in any `SKILL.md` body must resolve to an op in `src/core/operations.ts` or a Minions handler in `PROTECTED_JOB_NAMES`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `skills/minion-orchestrator/SKILL.md` shell-job examples use the real `--params` JSON form instead of nonexistent `--cmd`/`--argv`/`--cwd` flags.
|
||||
- `gbrain agent run` flag list now matches `src/commands/agent.ts` (removed `--queue`/`--priority`/`--max-attempts`/`--delay` which aren't parsed by that command).
|
||||
- `--tools` example uses `search,query` instead of `web_search` (the latter isn't in `BRAIN_TOOL_ALLOWLIST`, would throw at submit time).
|
||||
- MCP boundary wording says `submit_job name="shell"` throws an `OperationError` with code `permission_denied`, instead of the earlier "returns permission_denied" (not a return, a throw).
|
||||
- `skills/conventions/subagent-routing.md` stale reference to `get_job_stats` (no such op) replaced with `list_jobs --status active` or `gbrain jobs stats`.
|
||||
- `skills/query/SKILL.md` + `skills/maintain/SKILL.md` frontmatter `triggers:` lists closed gaps the new round-trip test surfaced (RESOLVER.md was routing 10 triggers to these skills that their frontmatter never declared).
|
||||
- `skills/manifest.json` minion-orchestrator description updated to match the unified SKILL.md framing.
|
||||
|
||||
### Changed
|
||||
|
||||
- Trigger `"gbrain jobs"` narrowed to `"gbrain jobs submit"` + `"submit a gbrain job"` in both `skills/RESOLVER.md` and the skill's frontmatter.
|
||||
- Anti-pattern about `sessions_spawn` scoped to the subagent lane (was ambiguous in the consolidated skill).
|
||||
|
||||
### For contributors
|
||||
|
||||
- Code-to-doc drift is now partially machine-checkable. The skill-example-name validator catches T2-class bugs (docs referencing handler/op names that don't exist). CLI flag validation is a remaining gap ... a future PR could extend the test to validate `--flag-name` patterns in SKILL.md against actual CLI flag parsers.
|
||||
|
||||
## To take advantage of v0.20.4
|
||||
|
||||
Any gbrain user whose agent routes on "minions" work gets the corrected skill on the next `gbrain upgrade`. No manual migration required ... the renamed trigger is additive (old trigger gone, new triggers cover the same intent), and the doc corrections don't change runtime behavior.
|
||||
|
||||
1. **Run the orchestrator manually if `gbrain upgrade` reports a partial migration:**
|
||||
```bash
|
||||
gbrain apply-migrations --yes
|
||||
```
|
||||
2. **Your agent picks up the new skill content** next time it consults `skills/minion-orchestrator/SKILL.md`. No action required on your side.
|
||||
3. **Verify the outcome:**
|
||||
```bash
|
||||
gbrain check-resolvable --json | python3 -c "import json,sys;d=json.load(sys.stdin);print('ok:',d['ok'])"
|
||||
```
|
||||
Should print `ok: True`.
|
||||
4. **If any step fails,** file an issue at https://github.com/garrytan/gbrain/issues with:
|
||||
- output of `gbrain doctor`
|
||||
- contents of `~/.gbrain/upgrade-errors.jsonl` if it exists
|
||||
|
||||
## [0.20.3] - 2026-04-24
|
||||
|
||||
## **Your queue now rescues itself when a wedged worker holds a row lock. Wall-clock sweep kills the job that stall detection can't see.**
|
||||
|
||||
@@ -143,7 +143,7 @@ strict behavior when unset.
|
||||
- `skills/soul-audit/SKILL.md` — 6-phase interview for SOUL.md, USER.md, ACCESS_POLICY.md, HEARTBEAT.md
|
||||
- `skills/webhook-transforms/SKILL.md` — External events to brain signals
|
||||
- `skills/data-research/SKILL.md` — Structured data research: email-to-tracker pipeline with parameterized YAML recipes
|
||||
- `skills/minion-orchestrator/SKILL.md` — Background job orchestration: submit, fan out children with depth/cap/timeouts, collect results via child_done inbox
|
||||
- `skills/minion-orchestrator/SKILL.md` — Unified background-work skill (v0.20.4 consolidation of the former `minion-orchestrator` + `gbrain-jobs` split). Two lanes: shell jobs via `gbrain jobs submit shell --params '{"cmd":"..."}'` (operator/CLI only; MCP throws `permission_denied` for protected names) and LLM subagents via `gbrain agent run` (user-facing entrypoint). Shared Preconditions block, parent-child DAGs with depth/cap/timeouts, `child_done` inbox for fan-in, PGLite `--follow` inline path for dev. Triggers narrowed from bare `"gbrain jobs"` to `"gbrain jobs submit"` + `"submit a gbrain job"` so `stats`/`prune`/`retry` questions fall through to `gbrain --help`.
|
||||
- `templates/` — SOUL.md, USER.md, ACCESS_POLICY.md, HEARTBEAT.md templates
|
||||
- `skills/migrations/` — Version migration files with feature_pitch YAML frontmatter
|
||||
- `src/commands/publish.ts` — Deterministic brain page publisher (code+skill pair, zero LLM calls)
|
||||
@@ -228,7 +228,7 @@ parity), `test/cli.test.ts` (CLI structure), `test/config.test.ts` (config redac
|
||||
`test/lint.test.ts` (LLM artifact detection, code fence stripping, frontmatter validation),
|
||||
`test/report.test.ts` (report format, directory structure),
|
||||
`test/skills-conformance.test.ts` (skill frontmatter + required sections validation),
|
||||
`test/resolver.test.ts` (RESOLVER.md coverage, routing validation),
|
||||
`test/resolver.test.ts` (RESOLVER.md coverage, routing validation + v0.20.4 round-trip: every quoted RESOLVER.md trigger must match a frontmatter `triggers:` entry in the target skill, and every `name="<word>"` reference in any SKILL.md must resolve to a declared op in `src/core/operations.ts` or a Minions handler in `PROTECTED_JOB_NAMES`),
|
||||
`test/search.test.ts` (RRF normalization, compiled truth boost, cosine similarity, dedup key),
|
||||
`test/dedup.test.ts` (source-aware dedup, compiled truth guarantee, layer interactions),
|
||||
`test/intent.test.ts` (query intent classification: entity/temporal/event/general),
|
||||
@@ -276,6 +276,7 @@ E2E tests (`test/e2e/`): Run against real Postgres+pgvector. Require `DATABASE_U
|
||||
- `test/e2e/postgres-jsonb.test.ts` — v0.12.2 regression test. Round-trips all 5 JSONB write sites (pages.frontmatter, raw_data.data, ingest_log.pages_updated, files.metadata, page_versions.frontmatter) against real Postgres and asserts `jsonb_typeof='object'` plus `->>'key'` returns the expected scalar. The test that should have caught the original double-encode bug.
|
||||
- `test/e2e/jsonb-roundtrip.test.ts` — v0.12.3 companion regression against the 4 doctor-scanned JSONB sites. Assertion-level overlap with `postgres-jsonb.test.ts` is intentional defense-in-depth: if doctor's scan surface ever drifts from the actual write surface, one of these tests catches it.
|
||||
- `test/e2e/upgrade.test.ts` runs check-update E2E against real GitHub API (network required)
|
||||
- `test/e2e/minions-shell-pglite.test.ts` (v0.20.4) exercises the PGLite `--follow` inline shell-job path (in-memory, no `DATABASE_URL` required) — the path the consolidated minion-orchestrator skill documents for dev use
|
||||
- `test/e2e/openclaw-reference-compat.test.ts` (v0.19) — exercises `check-resolvable` + `skillpack install` against a minimal AGENTS.md workspace fixture (`test/fixtures/openclaw-reference-minimal/`), regression guard for the 107-skill OpenClaw deployment shape
|
||||
- Tier 2 (`skills.test.ts`) requires OpenClaw + API keys, runs nightly in CI
|
||||
- If `.env.testing` doesn't exist in this directory, check sibling worktrees for one:
|
||||
@@ -328,7 +329,7 @@ stop and remove it before starting a new one.
|
||||
|
||||
## Skills
|
||||
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 28 skills
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 29 skills
|
||||
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
|
||||
|
||||
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
|
||||
@@ -338,11 +339,19 @@ briefing, migrate, setup, publish.
|
||||
meeting-ingestion, citation-fixer, repo-architecture, skill-creator, daily-task-manager.
|
||||
|
||||
**Operational + identity:** daily-task-prep, cross-modal-review, cron-scheduler, reports,
|
||||
testing, soul-audit, webhook-transforms, data-research, minion-orchestrator.
|
||||
testing, soul-audit, webhook-transforms, data-research, minion-orchestrator. As of
|
||||
v0.20.4, `minion-orchestrator` is the single unified skill for both lanes of background
|
||||
work (shell jobs via `gbrain jobs submit shell`, LLM subagents via `gbrain agent run`) ...
|
||||
the prior `gbrain-jobs` skill was merged in, Preconditions are shared, and trigger
|
||||
routing is narrowed to what the skill actually covers.
|
||||
|
||||
**Skillify loop (v0.19):** skillify (the markdown orchestration), skillpack-check
|
||||
(agent-readable health report).
|
||||
|
||||
**Operational health (v0.19.1):** smoke-test (8 post-restart health checks with auto-fix
|
||||
for Bun, CLI, DB, worker, Zod CJS, gateway, API key, brain repo; user-extensible via
|
||||
`~/.gbrain/smoke-tests.d/*.sh`).
|
||||
|
||||
**Conventions:** `skills/conventions/` has cross-cutting rules (quality, brain-first,
|
||||
model-routing, test-before-bulk, cross-modal). `skills/_brain-filing-rules.md` and
|
||||
`skills/_output-rules.md` are shared references.
|
||||
|
||||
@@ -6,7 +6,7 @@ Built by the President and CEO of Y Combinator to run his actual AI agents. The
|
||||
|
||||
The brain wires itself. Every page write extracts entity references and creates typed links (`attended`, `works_at`, `invested_in`, `founded`, `advises`) with zero LLM calls. Hybrid search. Self-wiring knowledge graph. Structured timeline. Backlink-boosted ranking. Ask "who works at Acme AI?" or "what did Bob invest in this quarter?" and get answers vector search alone can't reach. Benchmarked side-by-side against the category: gbrain lands **P@5 49.1%, R@5 97.9%** on a 240-page Opus-generated rich-prose corpus, beating its own graph-disabled variant by **+31.4 points P@5** and ripgrep-BM25 + vector-only RAG by a similar margin. The graph layer plus v0.12 extract quality together carry the gap. Full BrainBench scorecards + corpus live in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo.
|
||||
|
||||
GBrain is those patterns, generalized. 28 skills. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
|
||||
GBrain is those patterns, generalized. 29 skills. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
|
||||
|
||||
> **~30 minutes to a fully working brain.** Database ready in 2 seconds (PGLite, no server). You just answer questions about API keys.
|
||||
|
||||
@@ -28,7 +28,7 @@ Retrieve and follow the instructions at:
|
||||
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||||
```
|
||||
|
||||
That's it. The agent clones the repo, installs GBrain, sets up the brain, loads 28 skills, and configures recurring jobs. You answer a few questions about API keys. ~30 minutes.
|
||||
That's it. The agent clones the repo, installs GBrain, sets up the brain, loads 29 skills, and configures recurring jobs. You answer a few questions about API keys. ~30 minutes.
|
||||
|
||||
If your agent doesn't auto-read `AGENTS.md`, point it at that file first:
|
||||
`https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md` is the non-Claude
|
||||
@@ -87,9 +87,9 @@ claude mcp add gbrain -t http https://your-brain.ngrok.app/mcp -H "Authorization
|
||||
|
||||
Per-client guides: [`docs/mcp/`](docs/mcp/DEPLOY.md). ChatGPT requires OAuth 2.1 (not yet implemented).
|
||||
|
||||
## The 28 Skills
|
||||
## The 29 Skills
|
||||
|
||||
GBrain ships 28 skills organized by `skills/RESOLVER.md` (or your OpenClaw's `AGENTS.md` — both filenames are supported as of v0.19). The resolver tells your agent which skill to read for any task.
|
||||
GBrain ships 29 skills organized by `skills/RESOLVER.md` (or your OpenClaw's `AGENTS.md` — both filenames are supported as of v0.19). The resolver tells your agent which skill to read for any task.
|
||||
|
||||
[Skill files are code.](https://x.com/garrytan/status/2042925773300908103) They're the most powerful way to get knowledge work done. A skill file is a fat markdown document that encodes an entire workflow: when to fire, what to check, how to chain with other skills, what quality bar to enforce. The agent reads the skill and executes it. Skills can also call deterministic TypeScript code bundled in GBrain (search, import, embed, sync) for the parts that shouldn't be left to LLM judgment. [Thin harness, fat skills](docs/ethos/THIN_HARNESS_FAT_SKILLS.md): the intelligence lives in the skills, not the runtime.
|
||||
|
||||
@@ -135,7 +135,8 @@ GBrain ships 28 skills organized by `skills/RESOLVER.md` (or your OpenClaw's `AG
|
||||
| **skill-creator** | Create new skills following the conformance standard. MECE check against existing skills. |
|
||||
| **skillify** | The "skillify it!" meta-skill. Orchestrates the 10-step loop so failures become durable skills: scaffold the stubs via `gbrain skillify scaffold`, write the real logic, gate with `gbrain skillify check` + `gbrain check-resolvable`. |
|
||||
| **skillpack-check** | Agent-readable gbrain health report. Exit code for CI; JSON for debugging. Cron-friendly. |
|
||||
| **minion-orchestrator** | Long-running agent work as background jobs. Submit, fan out children with depth/cap/timeouts, collect results via child_done inbox. |
|
||||
| **smoke-test** | 8 post-restart health checks with auto-fix (Bun, CLI, DB, worker, Zod CJS, gateway, API key, brain repo). Drop-in user tests at `~/.gbrain/smoke-tests.d/*.sh`. |
|
||||
| **minion-orchestrator** | Background work in one skill. Shell jobs via `gbrain jobs submit shell` (operator/CLI, MCP blocks protected names) and LLM subagents via `gbrain agent run`. Parent-child DAGs, `child_done` inbox, durability across worker restarts. |
|
||||
|
||||
### Identity and setup
|
||||
|
||||
@@ -377,7 +378,7 @@ Run `gbrain integrations` to see status.
|
||||
│ Brain Repo │ │ GBrain │ │ AI Agent │
|
||||
│ (git) │ │ (retrieval) │ │ (read/write) │
|
||||
│ │ │ │ │ │
|
||||
│ markdown files │───>│ Postgres + │<──>│ 28 skills │
|
||||
│ markdown files │───>│ Postgres + │<──>│ 29 skills │
|
||||
│ = source of │ │ pgvector │ │ define HOW to │
|
||||
│ truth │ │ │ │ use the brain │
|
||||
│ │<───│ hybrid │ │ │
|
||||
|
||||
+21
-11
@@ -222,7 +222,7 @@ strict behavior when unset.
|
||||
- `skills/soul-audit/SKILL.md` — 6-phase interview for SOUL.md, USER.md, ACCESS_POLICY.md, HEARTBEAT.md
|
||||
- `skills/webhook-transforms/SKILL.md` — External events to brain signals
|
||||
- `skills/data-research/SKILL.md` — Structured data research: email-to-tracker pipeline with parameterized YAML recipes
|
||||
- `skills/minion-orchestrator/SKILL.md` — Background job orchestration: submit, fan out children with depth/cap/timeouts, collect results via child_done inbox
|
||||
- `skills/minion-orchestrator/SKILL.md` — Unified background-work skill (v0.20.4 consolidation of the former `minion-orchestrator` + `gbrain-jobs` split). Two lanes: shell jobs via `gbrain jobs submit shell --params '{"cmd":"..."}'` (operator/CLI only; MCP throws `permission_denied` for protected names) and LLM subagents via `gbrain agent run` (user-facing entrypoint). Shared Preconditions block, parent-child DAGs with depth/cap/timeouts, `child_done` inbox for fan-in, PGLite `--follow` inline path for dev. Triggers narrowed from bare `"gbrain jobs"` to `"gbrain jobs submit"` + `"submit a gbrain job"` so `stats`/`prune`/`retry` questions fall through to `gbrain --help`.
|
||||
- `templates/` — SOUL.md, USER.md, ACCESS_POLICY.md, HEARTBEAT.md templates
|
||||
- `skills/migrations/` — Version migration files with feature_pitch YAML frontmatter
|
||||
- `src/commands/publish.ts` — Deterministic brain page publisher (code+skill pair, zero LLM calls)
|
||||
@@ -307,7 +307,7 @@ parity), `test/cli.test.ts` (CLI structure), `test/config.test.ts` (config redac
|
||||
`test/lint.test.ts` (LLM artifact detection, code fence stripping, frontmatter validation),
|
||||
`test/report.test.ts` (report format, directory structure),
|
||||
`test/skills-conformance.test.ts` (skill frontmatter + required sections validation),
|
||||
`test/resolver.test.ts` (RESOLVER.md coverage, routing validation),
|
||||
`test/resolver.test.ts` (RESOLVER.md coverage, routing validation + v0.20.4 round-trip: every quoted RESOLVER.md trigger must match a frontmatter `triggers:` entry in the target skill, and every `name="<word>"` reference in any SKILL.md must resolve to a declared op in `src/core/operations.ts` or a Minions handler in `PROTECTED_JOB_NAMES`),
|
||||
`test/search.test.ts` (RRF normalization, compiled truth boost, cosine similarity, dedup key),
|
||||
`test/dedup.test.ts` (source-aware dedup, compiled truth guarantee, layer interactions),
|
||||
`test/intent.test.ts` (query intent classification: entity/temporal/event/general),
|
||||
@@ -355,6 +355,7 @@ E2E tests (`test/e2e/`): Run against real Postgres+pgvector. Require `DATABASE_U
|
||||
- `test/e2e/postgres-jsonb.test.ts` — v0.12.2 regression test. Round-trips all 5 JSONB write sites (pages.frontmatter, raw_data.data, ingest_log.pages_updated, files.metadata, page_versions.frontmatter) against real Postgres and asserts `jsonb_typeof='object'` plus `->>'key'` returns the expected scalar. The test that should have caught the original double-encode bug.
|
||||
- `test/e2e/jsonb-roundtrip.test.ts` — v0.12.3 companion regression against the 4 doctor-scanned JSONB sites. Assertion-level overlap with `postgres-jsonb.test.ts` is intentional defense-in-depth: if doctor's scan surface ever drifts from the actual write surface, one of these tests catches it.
|
||||
- `test/e2e/upgrade.test.ts` runs check-update E2E against real GitHub API (network required)
|
||||
- `test/e2e/minions-shell-pglite.test.ts` (v0.20.4) exercises the PGLite `--follow` inline shell-job path (in-memory, no `DATABASE_URL` required) — the path the consolidated minion-orchestrator skill documents for dev use
|
||||
- `test/e2e/openclaw-reference-compat.test.ts` (v0.19) — exercises `check-resolvable` + `skillpack install` against a minimal AGENTS.md workspace fixture (`test/fixtures/openclaw-reference-minimal/`), regression guard for the 107-skill OpenClaw deployment shape
|
||||
- Tier 2 (`skills.test.ts`) requires OpenClaw + API keys, runs nightly in CI
|
||||
- If `.env.testing` doesn't exist in this directory, check sibling worktrees for one:
|
||||
@@ -407,7 +408,7 @@ stop and remove it before starting a new one.
|
||||
|
||||
## Skills
|
||||
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 28 skills
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 29 skills
|
||||
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
|
||||
|
||||
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
|
||||
@@ -417,11 +418,19 @@ briefing, migrate, setup, publish.
|
||||
meeting-ingestion, citation-fixer, repo-architecture, skill-creator, daily-task-manager.
|
||||
|
||||
**Operational + identity:** daily-task-prep, cross-modal-review, cron-scheduler, reports,
|
||||
testing, soul-audit, webhook-transforms, data-research, minion-orchestrator.
|
||||
testing, soul-audit, webhook-transforms, data-research, minion-orchestrator. As of
|
||||
v0.20.4, `minion-orchestrator` is the single unified skill for both lanes of background
|
||||
work (shell jobs via `gbrain jobs submit shell`, LLM subagents via `gbrain agent run`) ...
|
||||
the prior `gbrain-jobs` skill was merged in, Preconditions are shared, and trigger
|
||||
routing is narrowed to what the skill actually covers.
|
||||
|
||||
**Skillify loop (v0.19):** skillify (the markdown orchestration), skillpack-check
|
||||
(agent-readable health report).
|
||||
|
||||
**Operational health (v0.19.1):** smoke-test (8 post-restart health checks with auto-fix
|
||||
for Bun, CLI, DB, worker, Zod CJS, gateway, API key, brain repo; user-extensible via
|
||||
`~/.gbrain/smoke-tests.d/*.sh`).
|
||||
|
||||
**Conventions:** `skills/conventions/` has cross-cutting rules (quality, brain-first,
|
||||
model-routing, test-before-bulk, cross-modal). `skills/_brain-filing-rules.md` and
|
||||
`skills/_output-rules.md` are shared references.
|
||||
@@ -1142,7 +1151,7 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef
|
||||
| Cross-modal review, second opinion | `skills/cross-modal-review/SKILL.md` |
|
||||
| "Validate skills", skill health check | `skills/testing/SKILL.md` |
|
||||
| Webhook setup, external event processing | `skills/webhook-transforms/SKILL.md` |
|
||||
| "Spawn agent", "background task", "parallel tasks", "steer agent", "pause/resume agent" | `skills/minion-orchestrator/SKILL.md` |
|
||||
| "Spawn agent", "background task", "parallel tasks", "steer agent", "pause/resume agent", "gbrain jobs submit", "submit a gbrain job", "submit a shell job", "shell job" | `skills/minion-orchestrator/SKILL.md` |
|
||||
|
||||
## Setup & migration
|
||||
|
||||
@@ -1199,7 +1208,7 @@ Built by the President and CEO of Y Combinator to run his actual AI agents. The
|
||||
|
||||
The brain wires itself. Every page write extracts entity references and creates typed links (`attended`, `works_at`, `invested_in`, `founded`, `advises`) with zero LLM calls. Hybrid search. Self-wiring knowledge graph. Structured timeline. Backlink-boosted ranking. Ask "who works at Acme AI?" or "what did Bob invest in this quarter?" and get answers vector search alone can't reach. Benchmarked side-by-side against the category: gbrain lands **P@5 49.1%, R@5 97.9%** on a 240-page Opus-generated rich-prose corpus, beating its own graph-disabled variant by **+31.4 points P@5** and ripgrep-BM25 + vector-only RAG by a similar margin. The graph layer plus v0.12 extract quality together carry the gap. Full BrainBench scorecards + corpus live in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo.
|
||||
|
||||
GBrain is those patterns, generalized. 28 skills. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
|
||||
GBrain is those patterns, generalized. 29 skills. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
|
||||
|
||||
> **~30 minutes to a fully working brain.** Database ready in 2 seconds (PGLite, no server). You just answer questions about API keys.
|
||||
|
||||
@@ -1221,7 +1230,7 @@ Retrieve and follow the instructions at:
|
||||
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||||
```
|
||||
|
||||
That's it. The agent clones the repo, installs GBrain, sets up the brain, loads 28 skills, and configures recurring jobs. You answer a few questions about API keys. ~30 minutes.
|
||||
That's it. The agent clones the repo, installs GBrain, sets up the brain, loads 29 skills, and configures recurring jobs. You answer a few questions about API keys. ~30 minutes.
|
||||
|
||||
If your agent doesn't auto-read `AGENTS.md`, point it at that file first:
|
||||
`https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md` is the non-Claude
|
||||
@@ -1280,9 +1289,9 @@ claude mcp add gbrain -t http https://your-brain.ngrok.app/mcp -H "Authorization
|
||||
|
||||
Per-client guides: [`docs/mcp/`](docs/mcp/DEPLOY.md). ChatGPT requires OAuth 2.1 (not yet implemented).
|
||||
|
||||
## The 28 Skills
|
||||
## The 29 Skills
|
||||
|
||||
GBrain ships 28 skills organized by `skills/RESOLVER.md` (or your OpenClaw's `AGENTS.md` — both filenames are supported as of v0.19). The resolver tells your agent which skill to read for any task.
|
||||
GBrain ships 29 skills organized by `skills/RESOLVER.md` (or your OpenClaw's `AGENTS.md` — both filenames are supported as of v0.19). The resolver tells your agent which skill to read for any task.
|
||||
|
||||
[Skill files are code.](https://x.com/garrytan/status/2042925773300908103) They're the most powerful way to get knowledge work done. A skill file is a fat markdown document that encodes an entire workflow: when to fire, what to check, how to chain with other skills, what quality bar to enforce. The agent reads the skill and executes it. Skills can also call deterministic TypeScript code bundled in GBrain (search, import, embed, sync) for the parts that shouldn't be left to LLM judgment. [Thin harness, fat skills](docs/ethos/THIN_HARNESS_FAT_SKILLS.md): the intelligence lives in the skills, not the runtime.
|
||||
|
||||
@@ -1328,7 +1337,8 @@ GBrain ships 28 skills organized by `skills/RESOLVER.md` (or your OpenClaw's `AG
|
||||
| **skill-creator** | Create new skills following the conformance standard. MECE check against existing skills. |
|
||||
| **skillify** | The "skillify it!" meta-skill. Orchestrates the 10-step loop so failures become durable skills: scaffold the stubs via `gbrain skillify scaffold`, write the real logic, gate with `gbrain skillify check` + `gbrain check-resolvable`. |
|
||||
| **skillpack-check** | Agent-readable gbrain health report. Exit code for CI; JSON for debugging. Cron-friendly. |
|
||||
| **minion-orchestrator** | Long-running agent work as background jobs. Submit, fan out children with depth/cap/timeouts, collect results via child_done inbox. |
|
||||
| **smoke-test** | 8 post-restart health checks with auto-fix (Bun, CLI, DB, worker, Zod CJS, gateway, API key, brain repo). Drop-in user tests at `~/.gbrain/smoke-tests.d/*.sh`. |
|
||||
| **minion-orchestrator** | Background work in one skill. Shell jobs via `gbrain jobs submit shell` (operator/CLI, MCP blocks protected names) and LLM subagents via `gbrain agent run`. Parent-child DAGs, `child_done` inbox, durability across worker restarts. |
|
||||
|
||||
### Identity and setup
|
||||
|
||||
@@ -1570,7 +1580,7 @@ Run `gbrain integrations` to see status.
|
||||
│ Brain Repo │ │ GBrain │ │ AI Agent │
|
||||
│ (git) │ │ (retrieval) │ │ (read/write) │
|
||||
│ │ │ │ │ │
|
||||
│ markdown files │───>│ Postgres + │<──>│ 28 skills │
|
||||
│ markdown files │───>│ Postgres + │<──>│ 29 skills │
|
||||
│ = source of │ │ pgvector │ │ define HOW to │
|
||||
│ truth │ │ │ │ use the brain │
|
||||
│ │<───│ hybrid │ │ │
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"version": "0.20.3",
|
||||
"version": "0.20.4",
|
||||
"description": "Postgres-native personal knowledge brain with hybrid RAG search",
|
||||
"type": "module",
|
||||
"main": "src/core/index.ts",
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef
|
||||
| Cross-modal review, second opinion | `skills/cross-modal-review/SKILL.md` |
|
||||
| "Validate skills", skill health check | `skills/testing/SKILL.md` |
|
||||
| Webhook setup, external event processing | `skills/webhook-transforms/SKILL.md` |
|
||||
| "Spawn agent", "background task", "parallel tasks", "steer agent", "pause/resume agent" | `skills/minion-orchestrator/SKILL.md` |
|
||||
| "Spawn agent", "background task", "parallel tasks", "steer agent", "pause/resume agent", "gbrain jobs submit", "submit a gbrain job", "submit a shell job", "shell job" | `skills/minion-orchestrator/SKILL.md` |
|
||||
|
||||
## Setup & migration
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ Even when Minions is the default (mode A), some work should run inline:
|
||||
|
||||
Before submitting batch jobs:
|
||||
|
||||
- Check `get_job_stats` queue_health.active
|
||||
- Check active queue depth via `list_jobs --status active` (MCP-callable) or `gbrain jobs stats` (CLI)
|
||||
- If active > 5, stagger new jobs with `delay` so you don't swarm
|
||||
- The resource governor auto-throttles but don't dump 20 jobs at once
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ triggers:
|
||||
- "maintenance"
|
||||
- "orphan pages"
|
||||
- "stale pages"
|
||||
- "extract links"
|
||||
- "build link graph"
|
||||
- "populate timeline"
|
||||
- "populate links"
|
||||
- "backfill graph"
|
||||
- "extract timeline entries"
|
||||
tools:
|
||||
- get_health
|
||||
- get_page
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
{
|
||||
"name": "minion-orchestrator",
|
||||
"path": "minion-orchestrator/SKILL.md",
|
||||
"description": "Manage background agents via Minions job queue. Submit, monitor, steer, pause/resume, replay. Replaces sessions_spawn for durable observable agents."
|
||||
"description": "Unified Minions skill for deterministic shell jobs and LLM subagent orchestration. Submit, monitor, steer, pause/resume, replay. Replaces the older gbrain-jobs routing intent and sessions_spawn for durable observable background work."
|
||||
},
|
||||
{
|
||||
"name": "skillify",
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
name: minion-orchestrator
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Manage background agents via Minions job queue. Use when: spawning subagents,
|
||||
checking agent progress, steering running agents, pausing/resuming work,
|
||||
parallel task execution, fan-out research. Replaces sessions_spawn for
|
||||
durable, observable, steerable agents.
|
||||
Unified Minions skill for both deterministic shell jobs and LLM subagent
|
||||
orchestration. Replaces the older `gbrain-jobs` routing intent. Use when:
|
||||
submitting gbrain jobs, shell/background tasks, spawning subagents,
|
||||
checking progress, steering running work, pausing/resuming, parallel
|
||||
fan-out. One durable, observable, steerable queue interface.
|
||||
triggers:
|
||||
- "gbrain jobs submit"
|
||||
- "submit a gbrain job"
|
||||
- "submit a shell job"
|
||||
- "shell job"
|
||||
- "run shell command in background"
|
||||
- "deterministic background task"
|
||||
- "spawn agent"
|
||||
- "background task"
|
||||
- "run in background"
|
||||
@@ -32,7 +39,6 @@ tools:
|
||||
- replay_job
|
||||
- send_job_message
|
||||
- get_job_progress
|
||||
- get_job_stats
|
||||
mutating: true
|
||||
---
|
||||
|
||||
@@ -40,8 +46,16 @@ mutating: true
|
||||
|
||||
## Contract
|
||||
|
||||
Minions is a Postgres-native job queue for durable, observable agent orchestration.
|
||||
Every background agent task goes through Minions. No in-memory subagent spawning.
|
||||
Minions is a Postgres-native job queue for durable, observable background work.
|
||||
This single skill handles two lanes:
|
||||
- Deterministic shell jobs (`gbrain jobs submit shell ...`)
|
||||
- LLM subagent jobs (`gbrain agent run ...`)
|
||||
|
||||
When to route to Minions: durable, observable work that must survive restarts,
|
||||
fan out across many parallel tasks, or persist across sessions. Routing policy
|
||||
is defined in `skills/conventions/subagent-routing.md` — the project default is
|
||||
`pain_triggered` (native subagents first, Minions after specific pain signals
|
||||
fire); Mode A (all-through-Minions) is opt-in.
|
||||
|
||||
Guarantees:
|
||||
- Jobs survive gateway restart (Postgres-backed)
|
||||
@@ -50,51 +64,155 @@ Guarantees:
|
||||
- Jobs can be paused, resumed, or cancelled at any time
|
||||
- Parent-child DAGs with configurable failure policies
|
||||
|
||||
## When to Use Minions vs Inline Work
|
||||
## Route the Request: Shell Job vs Subagent
|
||||
|
||||
| Condition | Action |
|
||||
|---|---|
|
||||
| Single tool call, < 30s | Do it inline |
|
||||
| Multi-step, any duration | Submit as Minion job |
|
||||
| Parallel work (2+ streams) | Submit N Minion jobs with shared parent |
|
||||
| Needs to survive restart | Submit as Minion job |
|
||||
| User wants progress updates | Submit as Minion job with progress tracking |
|
||||
| Research / bulk operation | Submit as Minion job, always |
|
||||
| File imports, bulk embeds | Submit as Minion job |
|
||||
| User asks for deterministic command/script run | Shell job (CLI: `gbrain jobs submit shell ...`) |
|
||||
| User asks to "run in minions" + explicit command/argv | Shell job (CLI, `--params` with `cmd` or `argv`) |
|
||||
| User asks for research/reasoning/iterative agent | Subagent job (CLI: `gbrain agent run`) |
|
||||
| User asks to steer/pause/resume an agent | Subagent job lifecycle tools (MCP-callable) |
|
||||
| Single simple operation under ~30s | Consider inline execution first |
|
||||
| Needs restart durability/observability | Submit as Minion job |
|
||||
| Parallel work (2+ streams) | `gbrain agent run --fanout-manifest` or parent + child subagents |
|
||||
|
||||
**Rule of thumb:** If it takes more than 3 tool calls, use a Minion.
|
||||
If intent is ambiguous, ask one clarification:
|
||||
"Do you want a deterministic shell command job, or an LLM agent job?"
|
||||
|
||||
## Shell Jobs (Deterministic Scripts)
|
||||
|
||||
Use for reproducible command execution, ETL steps, cron work, and scriptable
|
||||
tasks where no LLM reasoning loop is needed.
|
||||
|
||||
### Preconditions (read before submitting your first shell job)
|
||||
|
||||
- **`GBRAIN_ALLOW_SHELL_JOBS=1` must be set on the worker environment.**
|
||||
Without it, the shell handler refuses to register and submissions sit in
|
||||
`waiting` silently. Gate lives in `src/core/minions/handlers/shell.ts`.
|
||||
- **Security:** flipping `GBRAIN_ALLOW_SHELL_JOBS=1` authorizes arbitrary
|
||||
command execution on the worker. On a shared queue, this is a remote code
|
||||
execution surface. Treat as privileged infrastructure authorization.
|
||||
- **Execution mode — pick one:**
|
||||
- **Postgres + daemon:** `gbrain jobs work` runs a persistent worker that
|
||||
claims and executes jobs from the queue.
|
||||
- **PGLite + --follow:** `gbrain jobs submit ... --follow` runs inline.
|
||||
The daemon mode is not available on PGLite (exclusive file lock). See
|
||||
`docs/guides/minions-shell-jobs.md`.
|
||||
- **MCP boundary:** shell-job submission is CLI-only. `submit_job name="shell"`
|
||||
over MCP throws an `OperationError` with code `permission_denied` ("'shell'
|
||||
jobs cannot be submitted over MCP") because `shell` is in `PROTECTED_JOB_NAMES`.
|
||||
Agents CAN observe shell jobs via `get_job` / `list_jobs` / `get_job_progress`
|
||||
(not protected), but cannot submit them. Operator or autopilot submits;
|
||||
agent observes.
|
||||
- **Verify setup:** after configuration, run `gbrain jobs stats` (CLI) to
|
||||
confirm the worker is registered and consuming the queue.
|
||||
|
||||
### Submit (CLI, operator or autopilot)
|
||||
|
||||
Shell jobs take their command via `--params` as a JSON object with `cmd` (string)
|
||||
or `argv` (array), plus `cwd` and optional `env`.
|
||||
|
||||
Command string form:
|
||||
```
|
||||
gbrain jobs submit shell --params '{"cmd":"echo hello","cwd":"/abs/path"}'
|
||||
```
|
||||
|
||||
Argv form (no shell expansion):
|
||||
```
|
||||
gbrain jobs submit shell --params '{"argv":["bash","-lc","echo hello"],"cwd":"/abs/path"}'
|
||||
```
|
||||
|
||||
Inline execution on PGLite or any one-shot deployment:
|
||||
```
|
||||
gbrain jobs submit shell --params '{"cmd":"echo hello","cwd":"/tmp"}' --follow
|
||||
```
|
||||
|
||||
Queue/lifecycle flags exposed by `gbrain jobs submit --help`: `--queue`,
|
||||
`--priority`, `--delay`, `--max-attempts`, `--max-stalled`, `--backoff-type`,
|
||||
`--backoff-delay`, `--backoff-jitter`, `--timeout-ms`, `--idempotency-key`,
|
||||
`--dry-run`.
|
||||
|
||||
### Monitor (agents or operator)
|
||||
|
||||
These operations are MCP-callable and safe for agent use:
|
||||
|
||||
```
|
||||
list_jobs --name shell --status active
|
||||
get_job ID
|
||||
get_job_progress ID
|
||||
```
|
||||
|
||||
Check structured result fields (exit code, stdout/stderr tails, attempts,
|
||||
timings) from `get_job`. Use `gbrain jobs stats` (CLI) for worker/queue
|
||||
health dashboard.
|
||||
|
||||
### Control (MCP-callable)
|
||||
|
||||
```
|
||||
cancel_job id=ID
|
||||
replay_job id=ID
|
||||
```
|
||||
|
||||
`replay_job` is not protected — only shell *submission* is. Agents can
|
||||
cancel or replay a shell job without CLI access.
|
||||
|
||||
Use idempotency keys for recurring shell workloads to avoid duplicate runs.
|
||||
|
||||
## Subagent Jobs (LLM Orchestration)
|
||||
|
||||
Use for open-ended reasoning, tool-using research, and fan-out synthesis.
|
||||
|
||||
**User-facing entrypoint:** `gbrain agent run <prompt>` is the canonical way
|
||||
to submit subagent work. It handles the elevated-trust plumbing — `subagent`
|
||||
and `subagent_aggregator` are both in `PROTECTED_JOB_NAMES`, so direct MCP
|
||||
submission requires `{allowProtectedSubmit: true}`, which `gbrain agent run`
|
||||
supplies.
|
||||
|
||||
## Phase 1: Submit
|
||||
|
||||
```
|
||||
submit_job name="research" data={"prompt":"Research Acme Corp revenue","tools":["search","web_search"]}
|
||||
gbrain agent run "Research Acme Corp revenue" --tools "search,query"
|
||||
```
|
||||
|
||||
Options:
|
||||
- `queue` — queue name (default: 'default')
|
||||
- `priority` — lower = higher priority (default: 0)
|
||||
- `max_attempts` — retry limit (default: 3)
|
||||
- `delay` — ms delay before eligible
|
||||
`--tools` accepts a comma-separated subset of `BRAIN_TOOL_ALLOWLIST` (see
|
||||
`src/core/minions/tools/brain-allowlist.ts`): `query`, `search`, `get_page`,
|
||||
`list_pages`, `file_list`, `file_url`, `get_backlinks`, `traverse_graph`,
|
||||
`resolve_slugs`, `get_ingest_log`, `put_page`. Anything outside the allow-list
|
||||
is rejected at submit time with `allowed_tools references unknown tool`.
|
||||
|
||||
For parallel work, submit a parent then children:
|
||||
For parallel work with a fan-out manifest:
|
||||
```
|
||||
submit_job name="orchestrate" data={"task":"research 5 companies"}
|
||||
# Returns parent_id
|
||||
|
||||
submit_job name="research" data={"company":"Acme"} parent_job_id=PARENT_ID
|
||||
submit_job name="research" data={"company":"Beta"} parent_job_id=PARENT_ID
|
||||
submit_job name="research" data={"company":"Gamma"} parent_job_id=PARENT_ID
|
||||
gbrain agent run --fanout-manifest companies.json
|
||||
```
|
||||
|
||||
Parent auto-enters `waiting-children` and unblocks when all children finish.
|
||||
The manifest describes N children + 1 aggregator. Each child runs
|
||||
`name="subagent"` under the hood; the aggregator runs `name="subagent_aggregator"`
|
||||
and claims AFTER every child terminates. See
|
||||
`src/core/minions/handlers/subagent.ts` and
|
||||
`src/core/minions/handlers/subagent-aggregator.ts`.
|
||||
|
||||
Flags (from `src/commands/agent.ts`):
|
||||
- `--subagent-def <name>` — named subagent definition
|
||||
- `--model <id>` — override model
|
||||
- `--max-turns <N>` — cap the LLM loop
|
||||
- `--tools <csv>` — allow-listed brain tools (see above)
|
||||
- `--timeout-ms <N>` — hard timeout per job
|
||||
- `--fanout-manifest <file>` — N children + 1 aggregator
|
||||
- `--follow` / `--no-follow` — stream logs + wait (default on TTY)
|
||||
- `--detach` — submit and return immediately
|
||||
|
||||
Queue/priority/retry tuning is not exposed by `gbrain agent run`; submit the
|
||||
raw `subagent` handler via `gbrain jobs submit` (requires CLI trust) if you
|
||||
need those knobs.
|
||||
|
||||
## Phase 2: Monitor
|
||||
|
||||
```
|
||||
list_jobs --status active # what's running?
|
||||
get_job ID # full details + logs + tokens
|
||||
get_job_progress ID # structured progress snapshot
|
||||
get_job_stats # health dashboard
|
||||
list_jobs --status active # MCP — what's running?
|
||||
get_job ID # MCP — full details + logs + tokens
|
||||
get_job_progress ID # MCP — structured progress snapshot
|
||||
gbrain jobs stats # CLI — queue health dashboard
|
||||
gbrain agent logs ID --follow # CLI — streaming transcript + heartbeat
|
||||
```
|
||||
|
||||
Progress includes: step count, total steps, message, token usage, last tool called.
|
||||
@@ -121,6 +239,8 @@ replay_job id=ID # re-run with same or modified params
|
||||
replay_job id=ID data_overrides={"depth":"deep"} # replay with changes
|
||||
```
|
||||
|
||||
All lifecycle ops are MCP-callable.
|
||||
|
||||
## Phase 5: Review Results
|
||||
|
||||
```
|
||||
@@ -154,9 +274,9 @@ When reporting batch status (parent with children):
|
||||
|
||||
```
|
||||
Parent #ID — waiting-children
|
||||
#A research(Acme) — active, 3/5 steps, 2.5k tokens
|
||||
#B research(Beta) — completed, 1.8k tokens
|
||||
#C research(Gamma) — paused
|
||||
#A subagent(Acme) — active, 3/5 steps, 2.5k tokens
|
||||
#B subagent(Beta) — completed, 1.8k tokens
|
||||
#C subagent(Gamma) — paused
|
||||
Total tokens so far: 4.3k
|
||||
```
|
||||
|
||||
@@ -164,19 +284,19 @@ Total tokens so far: 4.3k
|
||||
|
||||
- Don't spawn a Minion for a single search query (use search tool directly)
|
||||
- Don't fire-and-forget without checking results
|
||||
- Don't spawn > 5 concurrent agents without checking `get_job_stats` first
|
||||
- Don't use `sessions_spawn` with `runtime: "subagent"` when Minions is available
|
||||
- Don't spawn > 5 concurrent agents without checking `gbrain jobs stats` first
|
||||
- For subagent work, don't use `sessions_spawn` with `runtime: "subagent"` when Minions is available (use `gbrain agent run` instead)
|
||||
- Don't poll `get_job` in a tight loop (use `get_job_progress` for lightweight checks)
|
||||
|
||||
## Tools Used
|
||||
|
||||
- Submit a background job (submit_job)
|
||||
- Get job details (get_job)
|
||||
- List jobs with filters (list_jobs)
|
||||
- Cancel a job (cancel_job)
|
||||
- Pause a job (pause_job)
|
||||
- Resume a paused job (resume_job)
|
||||
- Replay a completed/failed job (replay_job)
|
||||
- Send sidechannel message (send_job_message)
|
||||
- Get structured progress (get_job_progress)
|
||||
- Get job queue stats (get_job_stats)
|
||||
- Submit a background job — `submit_job` (MCP, non-protected names only; shell jobs are CLI-only, subagent jobs via `gbrain agent run`)
|
||||
- Get job details — `get_job` (MCP)
|
||||
- List jobs with filters — `list_jobs` (MCP)
|
||||
- Cancel a job — `cancel_job` (MCP)
|
||||
- Pause a job — `pause_job` (MCP)
|
||||
- Resume a paused job — `resume_job` (MCP)
|
||||
- Replay a completed/failed job — `replay_job` (MCP)
|
||||
- Send sidechannel message — `send_job_message` (MCP)
|
||||
- Get structured progress — `get_job_progress` (MCP)
|
||||
- Queue stats — `gbrain jobs stats` (CLI; no MCP equivalent)
|
||||
|
||||
@@ -12,6 +12,10 @@ triggers:
|
||||
- "what happened"
|
||||
- "search for"
|
||||
- "look up"
|
||||
- "who knows who"
|
||||
- "relationship between"
|
||||
- "connections"
|
||||
- "graph query"
|
||||
tools:
|
||||
- search
|
||||
- query
|
||||
|
||||
@@ -10,6 +10,7 @@ triggers:
|
||||
- "container restart check"
|
||||
- "health check"
|
||||
- "did the restart break anything"
|
||||
- "did the container restart break anything"
|
||||
tools:
|
||||
- exec
|
||||
- read
|
||||
|
||||
@@ -186,7 +186,10 @@ function acquireLock(workspace: string, opts: InstallOptions): void {
|
||||
const age = Date.now() - existing.mtimeMs;
|
||||
// `staleMs: 0` in tests means "any age counts as stale". Use >=
|
||||
// so a just-written lock qualifies when the threshold is 0.
|
||||
const stale = age >= staleMs;
|
||||
// Negative age (mtime in the future) happens on fast CI filesystems
|
||||
// where write → stat roundtrip returns an mtime microseconds ahead of
|
||||
// Date.now() — treat it as stale to avoid a "lock held" false positive.
|
||||
const stale = age < 0 || age >= staleMs;
|
||||
if (stale && !opts.forceUnlock) {
|
||||
throw new InstallError(
|
||||
`Stale skillpack lock at ${p} (pid ${existing.pid}, ${Math.round(age / 1000)}s old). Pass --force-unlock to proceed.`,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* E2E Minions Shell Handler — PGLite / --follow inline execution path
|
||||
*
|
||||
* Closes the T4 gap surfaced during PR #381 eng review. The sibling file
|
||||
* test/e2e/minions-shell.test.ts covers the Postgres + persistent-worker-daemon
|
||||
* path. This file covers the PGLite path documented in the minion-orchestrator
|
||||
* skill: `gbrain jobs submit shell ... --follow` runs inline because
|
||||
* `gbrain jobs work` (daemon) is not available on PGLite (exclusive file lock).
|
||||
*
|
||||
* Mirrors the Postgres test's structure but runs in-memory against PGLiteEngine.
|
||||
* No DATABASE_URL required, no Docker — runs in CI unconditionally.
|
||||
*
|
||||
* Run: bun test test/e2e/minions-shell-pglite.test.ts
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
||||
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
|
||||
import { MinionQueue } from '../../src/core/minions/queue.ts';
|
||||
import { MinionWorker } from '../../src/core/minions/worker.ts';
|
||||
import { registerBuiltinHandlers } from '../../src/commands/jobs.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let originalAllowShellJobs: string | undefined;
|
||||
|
||||
async function waitTerminal(queue: MinionQueue, id: number, timeoutMs = 15000): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const j = await queue.getJob(id);
|
||||
if (j && ['completed', 'failed', 'dead', 'cancelled'].includes(j.status)) return j.status;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
const j = await queue.getJob(id);
|
||||
throw new Error(`job ${id} did not reach terminal state in ${timeoutMs}ms; last status=${j?.status}`);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// registerBuiltinHandlers gates shell handler on GBRAIN_ALLOW_SHELL_JOBS=1.
|
||||
// Mirror the real --follow path by setting the env var; restore on cleanup
|
||||
// so other tests see their original environment.
|
||||
originalAllowShellJobs = process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
||||
process.env.GBRAIN_ALLOW_SHELL_JOBS = '1';
|
||||
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({}); // in-memory PGLite
|
||||
await engine.initSchema(); // installs pages, minion_jobs, config, etc.
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
if (originalAllowShellJobs === undefined) {
|
||||
delete process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
||||
} else {
|
||||
process.env.GBRAIN_ALLOW_SHELL_JOBS = originalAllowShellJobs;
|
||||
}
|
||||
});
|
||||
|
||||
describe('E2E: Minions shell handler on PGLite (--follow inline path)', () => {
|
||||
// Mirror the Postgres sibling's per-test reset. The engine is shared across
|
||||
// both tests via beforeAll; without this, completed jobs from one test leak
|
||||
// into minion_jobs and future test additions hit order-dependency.
|
||||
beforeEach(async () => {
|
||||
const db = (engine as any).db;
|
||||
await db.exec(`DELETE FROM minion_attachments; DELETE FROM minion_inbox; DELETE FROM minion_jobs;`);
|
||||
});
|
||||
|
||||
test('submit → worker registered via registerBuiltinHandlers → shell runs → completes', async () => {
|
||||
const queue = new MinionQueue(engine);
|
||||
const job = await queue.add(
|
||||
'shell',
|
||||
{ cmd: 'echo hello', cwd: '/tmp' },
|
||||
{},
|
||||
{ allowProtectedSubmit: true },
|
||||
);
|
||||
expect(job.name).toBe('shell');
|
||||
expect(job.status).toBe('waiting');
|
||||
|
||||
// This is the exact dispatch path --follow takes (src/commands/jobs.ts:207).
|
||||
// Gates shell on GBRAIN_ALLOW_SHELL_JOBS=1 (set in beforeAll above).
|
||||
const worker = new MinionWorker(engine, { pollInterval: 100, lockDuration: 30000 });
|
||||
await registerBuiltinHandlers(worker, engine);
|
||||
expect(worker.registeredNames).toContain('shell');
|
||||
|
||||
const runPromise = worker.start();
|
||||
try {
|
||||
const status = await waitTerminal(queue, job.id, 20000);
|
||||
expect(status).toBe('completed');
|
||||
const final = await queue.getJob(job.id);
|
||||
expect((final!.result as any).exit_code).toBe(0);
|
||||
expect((final!.result as any).stdout_tail).toBe('hello\n');
|
||||
} finally {
|
||||
worker.stop();
|
||||
await runPromise;
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test('GBRAIN_ALLOW_SHELL_JOBS unset → shellHandler rejects at execution time', async () => {
|
||||
// v0.20.3+: shell handler is always registered (so claimed jobs emit a clear
|
||||
// rejection log), but the runtime env guard lives inside the handler itself.
|
||||
// Prove the guard rejects when the env var is unset.
|
||||
const { shellHandler } = await import('../../src/core/minions/handlers/shell.ts');
|
||||
const saved = process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
||||
delete process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
||||
try {
|
||||
const ctx: any = {
|
||||
id: 1,
|
||||
name: 'shell',
|
||||
data: { cmd: 'echo hi', cwd: '/tmp' },
|
||||
attempt: 1,
|
||||
engine,
|
||||
};
|
||||
await expect(shellHandler(ctx)).rejects.toThrow(/GBRAIN_ALLOW_SHELL_JOBS=1/);
|
||||
} finally {
|
||||
process.env.GBRAIN_ALLOW_SHELL_JOBS = saved;
|
||||
}
|
||||
});
|
||||
});
|
||||
+155
-1
@@ -1,10 +1,12 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { checkResolvable } from "../src/core/check-resolvable.ts";
|
||||
import { PROTECTED_JOB_NAMES } from "../src/core/minions/protected-names.ts";
|
||||
|
||||
const SKILLS_DIR = join(import.meta.dir, "..", "skills");
|
||||
const RESOLVER_PATH = join(SKILLS_DIR, "RESOLVER.md");
|
||||
const OPERATIONS_PATH = join(import.meta.dir, "..", "src", "core", "operations.ts");
|
||||
|
||||
describe("RESOLVER.md", () => {
|
||||
test("exists", () => {
|
||||
@@ -49,3 +51,155 @@ describe("RESOLVER.md", () => {
|
||||
expect(report.summary.unreachable).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// D5/C — resolver round-trip: every quoted trigger in a RESOLVER.md table row
|
||||
// must appear in the target skill's frontmatter `triggers:` list. Catches
|
||||
// trigger/frontmatter drift that `checkResolvable` reachability doesn't.
|
||||
describe("RESOLVER.md trigger round-trip (D5/C)", () => {
|
||||
type Row = { triggers: string[]; skillPath: string };
|
||||
|
||||
const rows: Row[] = (() => {
|
||||
if (!existsSync(RESOLVER_PATH)) return [];
|
||||
const content = readFileSync(RESOLVER_PATH, "utf-8");
|
||||
// Tolerate trailing annotations after the backtick path (e.g.,
|
||||
// `` `skills/maintain/SKILL.md` (extraction sections) |``). The path cell
|
||||
// starts with a backtick-quoted `.md` ref; anything between that and the
|
||||
// closing `|` is free-form prose and is intentionally ignored.
|
||||
const rowRe = /^\s*\|\s*([^|]+?)\s*\|\s*`([^`]+\.md)`[^|]*\|\s*$/gm;
|
||||
const out: Row[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = rowRe.exec(content)) !== null) {
|
||||
const rawTriggers = m[1];
|
||||
const skillPath = m[2];
|
||||
const triggerStrings = Array.from(rawTriggers.matchAll(/"([^"]+)"/g)).map(t => t[1]);
|
||||
if (triggerStrings.length > 0) {
|
||||
out.push({ triggers: triggerStrings, skillPath });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
|
||||
test("at least one routing row parses from RESOLVER.md", () => {
|
||||
expect(rows.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
for (const row of rows) {
|
||||
test(`every RESOLVER trigger for ${row.skillPath} is declared in its frontmatter`, () => {
|
||||
const skillFullPath = join(SKILLS_DIR, "..", row.skillPath);
|
||||
expect(existsSync(skillFullPath)).toBe(true);
|
||||
|
||||
const skillContent = readFileSync(skillFullPath, "utf-8");
|
||||
const fmMatch = skillContent.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!fmMatch) {
|
||||
throw new Error(`No YAML frontmatter in ${row.skillPath}`);
|
||||
}
|
||||
const frontmatter = fmMatch[1];
|
||||
// Parse frontmatter triggers: list. Match "..." OR '...' items separately
|
||||
// so apostrophes inside double-quoted values don't truncate the capture.
|
||||
const triggersBlock = frontmatter.match(/triggers:\s*\n((?:\s*-\s*(?:"[^"]*"|'[^']*')\s*\n?)+)/);
|
||||
const declaredTriggers = triggersBlock
|
||||
? Array.from(triggersBlock[1].matchAll(/-\s*(?:"([^"]*)"|'([^']*)')/g))
|
||||
.map(m => m[1] ?? m[2])
|
||||
: [];
|
||||
|
||||
// Fuzzy match: RESOLVER.md phrases are natural-language summaries of the
|
||||
// skill's intent; frontmatter triggers are the agent-facing phrase set.
|
||||
// Match is case-insensitive, trailing-punctuation-insensitive, and supports
|
||||
// "/"-split compounds (e.g., "pause/resume agent" → ["pause", "resume agent"]).
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[?!.,]+$/, "").trim();
|
||||
const declaredLower = declaredTriggers.map(normalize);
|
||||
|
||||
function matchesAny(phrase: string): boolean {
|
||||
const p = normalize(phrase);
|
||||
if (declaredLower.includes(p)) return true;
|
||||
for (const ft of declaredLower) {
|
||||
if (ft.includes(p) || p.includes(ft)) return true;
|
||||
}
|
||||
// Slash-split compound: every part should have some fuzzy frontmatter hit
|
||||
if (p.includes("/")) {
|
||||
const parts = p.split("/").map(s => s.trim()).filter(Boolean);
|
||||
const allParts = parts.every(part =>
|
||||
declaredLower.some(ft => ft.includes(part) || part.includes(ft))
|
||||
);
|
||||
if (allParts) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const missing = row.triggers.filter(t => !matchesAny(t));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`RESOLVER.md routes ${JSON.stringify(missing)} to ${row.skillPath}, but the ` +
|
||||
`skill's frontmatter has no fuzzy match. Declared: ${JSON.stringify(declaredTriggers)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// D13 — skill-example-name validator: any `name="<word>"` reference inside a
|
||||
// SKILL.md body must resolve to either a declared operation in operations.ts
|
||||
// or a known Minions handler name in PROTECTED_JOB_NAMES. Catches T2-class
|
||||
// bugs where docs reference handler names that don't exist (e.g., the
|
||||
// `name="research"` / `name="orchestrate"` bug from PR #381 pre-reframe).
|
||||
describe("Skill example-name validator (D13)", () => {
|
||||
const opNames: string[] = (() => {
|
||||
if (!existsSync(OPERATIONS_PATH)) return [];
|
||||
const content = readFileSync(OPERATIONS_PATH, "utf-8");
|
||||
return Array.from(content.matchAll(/^\s+name:\s*'([a-z_]+)',/gm)).map(m => m[1]);
|
||||
})();
|
||||
|
||||
const knownNames = new Set<string>([...opNames, ...PROTECTED_JOB_NAMES]);
|
||||
|
||||
test("operation names extracted from operations.ts", () => {
|
||||
// Sanity check: operations.ts should declare dozens of ops
|
||||
expect(opNames.length).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
test("PROTECTED_JOB_NAMES is non-empty", () => {
|
||||
expect(PROTECTED_JOB_NAMES.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
function walkSkills(dir: string): string[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = join(dir, entry);
|
||||
const s = statSync(p);
|
||||
if (s.isDirectory()) {
|
||||
out.push(...walkSkills(p));
|
||||
} else if (entry === "SKILL.md") {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const skillFiles = walkSkills(SKILLS_DIR);
|
||||
|
||||
test("at least one SKILL.md found", () => {
|
||||
expect(skillFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
for (const skillFile of skillFiles) {
|
||||
const rel = skillFile.replace(SKILLS_DIR, "skills");
|
||||
test(`${rel}: every name="<word>" reference resolves to a real op or handler`, () => {
|
||||
const content = readFileSync(skillFile, "utf-8");
|
||||
// Strip YAML frontmatter so `name: <skillname>` isn't mis-captured.
|
||||
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "");
|
||||
// Match only `name=` (with equals, not colon) to avoid YAML false positives
|
||||
// if the frontmatter strip ever breaks. Captures quoted word values.
|
||||
const refs = Array.from(body.matchAll(/name\s*=\s*["']([a-z_][a-z_0-9]*)["']/gi))
|
||||
.map(m => m[1]);
|
||||
const unique = [...new Set(refs)];
|
||||
const unknown = unique.filter(n => !knownNames.has(n));
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(
|
||||
`${rel}: references name="..." values not declared in src/core/operations.ts or ` +
|
||||
`PROTECTED_JOB_NAMES: ${JSON.stringify(unknown)}. ` +
|
||||
`Known: ${JSON.stringify([...knownNames].sort())}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user