Files
reliquary/plugins/spire/skills/gremlins/SKILL.md
T

13 KiB

name, description, allowed-tools
name description allowed-tools
gremlins Trains one headless implementer (the base) per build and spawns gremlins from it, one per complex block, by forking the base's session (`claude -p --resume --fork-session`). Owns the command line, the prompt-cache bookkeeping and the usage ledger. Windows-tested; other platforms fail closed until tested (see Platforms). Loaded by the Athanor. Bash(uv:*)

!uv run "${CLAUDE_SKILL_DIR}/gremlins.py" where

Gremlins

What and when

The Athanor's build path for complex blocks. One base per build, trained early: a headless claude -p session that reads the code the build touches, writes the trunk notes and changes nothing. One gremlin per complex block: spawn (method fork) forks the base's session, so the gremlin starts from the base's reading at cache-read prices. Mechanical or distant blocks still go to fresh Agent-tool agents with the trunk notes.

Words: continue gives a finished gremlin one more turn (it resumes, it does not fork); rebase forks the base to absorb what changed, and that session becomes a new base; a ping is a one-turn keep-warm call the helper launches by itself.

The helper gremlins.py owns every claude -p command line, the registry, the cache bookkeeping and the usage ledger. You write briefs and read its JSON.

Claude Code only. Codex loads this skill too; it does not apply there.

Platforms

Windows-tested; other platforms fail closed until tested. On Linux, WSL and macOS where reports ok: false with the reason, init refuses, and on an existing state dir train, spawn, continue, rebase and tick refuse too (exit 2, error: "platform not supported"). Then use fresh agents for every block (the 0.2.2 shape). Do not try to work around it.

Setup

  • The report injected above is where's. script is the helper's absolute path: use it in every call, uv run "<script>" <command> …. ok: false → Platforms, or the reason. No report above (the injection failed) → treat it as ok: false.
  • The project needs an allow rule Bash(uv run:*) (or Bash(uv:*)): this skill's allowed-tools did not pre-approve the calls in a subagent (P0.8, untrusted dir). Call plain uv; use $SCRIPTS_UV only on a host without uv on PATH.
  • init --run <run log .md> --model <full id> once per run, with --effort and --agent <project agent> where the plan names them. A successor never re-runs init to "reset": it runs list and adopts the active base.
  • An init re-run changes only the flags given, except that it re-resolves the claude binary through the source it came from, unless it was pinned with --binary (N3). Its warnings say when the binary changed or could not be re-resolved; existing bases keep their own binary.
  • --warm-margin-s must stay below the TTL (at 1 h, at most 1800 takes effect).
  • --tools does not re-derive the allow rules: pass --allowed-tools together with it.

Commands

Every command except where takes --run <run log .md>. stdout is one JSON object (usage --format md: one markdown fragment); a refusal is {"error": …, …}. Exit codes: 0 ok; 2 usage error or refused; 3 wait condition not met at its timeout; 4 wait condition met and a job in it did not end ok.

Command Key flags Returns
where [--binary B] {script, python, uv, binary, git, scrub, platform, warnings, ok} (+ reason, error and exit 2 when not ok)
init --model, --effort, --agent, --cwd, --binary, --tools, --allowed-tools, --parallel-cap (2), --orphan-after (1200), --pings on|off, --warm-margin-s, --max-minutes (90), rebase thresholds {ok, state, registry, created, changed, changed_fields, bases, config, warnings}
train --base B --brief F [--phase N] [--block X] [--supersedes OLD]… {job, base, session_id}
spawn --base B --block X --brief F --phase N [--method fork] [--mode sequential|parallel] [--paths P…] [--delta-note PATH]… [--max-minutes M] [--max-budget-usd U] [--force] {job, base, method, base_state, warm_until, rebase_due, retrain_due, reasons}
continue --job J --brief F [--max-minutes M] (J is a spawn job) {job}
rebase --base B --as NEW --brief F --phase N [--delta-note PATH]… {job, base, parent, session_id} (base is NEW)
wait --all or job ids, [--any] [--timeout S] (default 540, max 570) {done[], running[], pings[]}
list — {state, bases[], jobs[], running[], lost_now[]}; base rows carry warmth, trained_at and the due flags
usage [--block X | --phase N] [--format json|md] tokens and cost per kind, cold starts, cache-hit ratio, priced total
expire --base B | --all {retired, bases, killed[], ok}
kill --job J {job, killed, status, ok, snapshot, survivors, tree_kill_rc}
tick — one keep-warm pass {state, pinged, bases} (wait and the supervisors already tick)

A first phase, in calls (G = uv run "<script>", R = --run <run log .md>; every wait with Bash timeout: 600000):

G init R --model claude-opus-5-5[1m] --effort high
G train R --base B1 --phase 1 --brief <train brief>      → G wait R --all (until exit 0 or 4)
G spawn R --base B1 --block 1.1 --phase 1 --brief <delta brief> --delta-note <notes>…
G wait R --all                                           → read notes/1.1.md and the result
G continue R --job <job> --brief <fix brief>             (only on a red gate, once)
G usage R --block 1.1 --format md                        → the block's run-log row
G usage R --phase 1 --format md;  G list R               → phase close; nothing running

Next phase: G rebase R --base B1 --as B2 --phase 2 --brief <delta> --delta-note <notes since trained_at>…, wait, then spawn from B2.

The helper treats --phase as optional; always pass it on spawn and rebase (it drives phase_boundary and usage --phase). spawn, continue and rebase refuse (exit 2) while parallel_cap jobs of those kinds run: wait first. spawn and rebase refuse a base that is not active; spawn --force overrides that.

Waiting

  • Launches return at once; the job runs under a detached supervisor.
  • Call wait from Bash with timeout: 600000; never omit it. The Bash tool backgrounds a call that passes its timeout instead of killing it (P0.1), and you are left without its output.
  • Loop wait --all until exit 0 or 4. Exit 3 means "call again", and done may already hold finished results, failed ones too (C4). Each result comes back in exactly one done: read done on every call, not only the last.
  • Pings come back in pings, never in done, and never set the exit code; a ping cannot be waited on. A ping belongs to its base's latest phase and is counted in that phase's usage.
  • running[] rows (job, block, elapsed_s, last_event_s_ago, messages) count messages (stream messages so far); results and the ledger count turns (M16). They are different numbers.
  • A failed detach of the supervisor (detach_failed) shows up as a failed result in the next wait, not in the launch return.

Results

Each done entry (also results/<job>.json): job, kind, method, base, block, phase, status, flags, fingerprint, session, parent_session, models_used, tokens{input, output, cache_creation, cache_creation_1h, cache_creation_5m, cache_read}, cost_delta_usd, cache, base_state, wall_s, turns, denials, note, raw, paths, reason, model_requested.

  • kind train|spawn|continue|rebase|ping; method is "fork" on a spawn, null on every other kind.
  • status ok|failed|timeout|killed|lost. reason: for failed one of model_refused, usage_limit, overloaded, prompt_too_long, api_error, is_error, exit_code, no_result, detach_failed, launch_error, supervisor_error; for killed one of kill, expire, orphaned; for lost supervisor_dead; null otherwise, also for timeout (over --max-minutes).
  • flags: stalled, needs_attention (permission denials: the allow rules may be short), usage_mismatch, ttl_downgrade, cold_start, clock_jump, ping_missed, orphaned, expired, parent_inactive.
  • cache hit|partial|miss, from the first request against the base's context; base_state at launch warm|expiring|cold; fingerprint ok or drift:<fields>. A cold base usually shows partial, because the shared system prefix stays warm (C5).
  • note is notes/<block>.md when the gremlin wrote it during this job, else null. paths echoes --paths.

What to do per failure reason:

  • model_refused → the model id is not admitted (availableModels); the user must change settings. Flag it, hand back.
  • usage_limit → plan usage is used up. Stop launching, flag it, hand back.
  • overloaded, api_error → transient. continue once later, else flag it.
  • prompt_too_long → the base is too large. Re-train or rebase smaller.
  • is_error, exit_code, no_result → read raw/<job>.err and the note, continue once, else flag it.
  • detach_failed, launch_error, supervisor_error → a helper or host problem (raw/<job>.sup.log). Flag it; do not retry blindly.
  • Status timeout → read the note, continue once with what is left, else flag it. Status lost (the supervisor died) → flag it.

continue takes spawn jobs only. A failed train is trained again (a failed base's name is free); a failed rebase leaves the old base active.

stalled means no stream output for stall_after_s (900 s); the helper reports it and does nothing. Together with clock_jump it is a host suspend, not a hung gremlin. While a job runs, the same shows as a large last_event_s_ago: wait again before acting; still quiet after that → kill and flag it.

Rules (flags, not judgment)

  • rebase_due (reasons phase_boundary, commits:N, lines:N, delta_tokens:N, binary_drift, runtime_drift:…) → rebase --as <new> --phase <n> before the next spawn. Always pass --phase. rebase counts toward parallel_cap.
  • retrain_due (defining:…, max_base_tokens:N, rebases:N, binary_missing) → train a new base with --supersedes <old>, else the old base keeps getting pings. binary_missing → launches refuse; train with the new binary (train --binary, or re-run init --binary first). A cold base is still spawnable; coldness never triggers a re-train.
  • Delta: attach handoff notes with spawn --delta-note PATH (repeatable). A ## Changes since base section in a brief holds this gremlin's delta only, and no line in it may start with #, also not inside fenced code: such a line ends the section (N1). When rebasing, attach every note written since the base's trained_at (shown by list): a note older than that counts as known to the base whether or not it was given to it (N2).
  • continue sends the brief without a frame: put the commit rule and the note path into it (M16).
  • Sequential gremlins run one at a time (Minor 13). Parallel only for disjoint blocks, with --mode parallel --paths …; parallel gremlins never commit, and the Athanor commits each block by the paths its note lists, one at a time.
  • Nothing running at hand-back: list shows no running job other than pings (kind ping; a ping ends within 300 s and is not a leak, Minor 7). Otherwise kill it and flag it.
  • kill and expire run long (each kill verifies for up to 10 s; expire kills its jobs one after another): call them from Bash with timeout: 600000, as wait.
  • kill: key on ok and status, not on killed or the exit code (M12). killed: true, status: ok means the job finished first; ok: false lists survivors (exit still 0): flag it.
  • Orphan watchdog (N1): a running job is killed (orphaned) after orphan_after (default 1200 s) without an orchestrator call (wait, spawn, continue, train, rebase, list, usage). kill and expire do not count (Phase 1 M3), and a gremlin's own calls never do. Keep looping wait while gremlins run.
  • Inside a gremlin SPIRE_GREMLIN_CHILD=1 is set, and train, spawn, continue, rebase, kill, expire, init and tick refuse (N3); where, list, usage and wait work.
  • Models: full ids only; spawn takes the base's model (no --model). Admission is permissive at init and enforced at runtime (M15): a substituted model ends failed/model_refused.
  • After claude exits, its leftover descendants are killed, including daemons a commit may have started (gpg-agent, git fsmonitor--daemon); they restart on demand (M16).
  • A re-train that supersedes a base while a rebase of it runs: the rebase's new base arrives superseded, and its result carries parent_inactive (N4). Re-train first, or let the rebase finish.
  • Never resume or fork a headless session by hand; never call claude -p yourself; never SendMessage or TaskStop a gremlin (continue and kill cover it).

State

<run log without .md>-gremlins/, next to the run log: registry.json, ledger.jsonl, jobs/, briefs/, notes/, results/, logs/, raw/. Its .gitignore keeps out raw/ and lock files; everything else is committed with the run log at phase close. The Athanor reads notes/, results/ and logs/; raw/<job>.err and raw/<job>.sup.log only when a job failed.