🤖 gremlins: SKILL.md, Athanor procedure, implementation skill, run-log template
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: gremlins
|
||||
description: 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.
|
||||
allowed-tools: 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.
|
||||
@@ -37,16 +37,16 @@ A plan holds decisions, contracts, invariants and the scope fence. Code goes in
|
||||
|
||||
- **Sequence by default.** Later changes depend on earlier ones, even when they look independent. Run in parallel only for truly disjoint work: separate files with no behavioural coupling, read-only exploration, separate test suites. If you have to think hard about independence, sequence.
|
||||
- **One owner per coherent change.** Do not split one logical change across agents; they fight over the boundary.
|
||||
- **Hand over as commits, not descriptions.** Each block commits and adds a short handoff note to the run log (template: `references/run-log.md`). The note is how flags reach the reviewer.
|
||||
- **Hand over as commits, not descriptions.** Each block commits, and its owner writes a short handoff note (template: `references/run-log.md`): a gremlin writes `notes/<block>.md` and the Athanor appends it to the run log; a fresh agent writes into the run log directly. The note is how flags reach the reviewer.
|
||||
- **Between blocks, a cheap gate, not a review.** Compile or typecheck what the block owns. Run the test suite once, at phase end, and fix the fallout in that last block.
|
||||
- **Decisions belong before the run.** Inside it, take the conservative default, flag it in the handoff note and continue. Do not wait on the user.
|
||||
- **Deviate with a reason.** Splitting a block, changing an owner model, adding a check: allowed, logged with why.
|
||||
|
||||
## Context: fresh, fork, or keep
|
||||
## Context: fresh, trained, or keep
|
||||
|
||||
Both extremes have failed. Many tiny fresh implementers relearn the code each time and fragment the plan. One implementer kept across many tasks bloats and drifts. Choose per block: how much shared understanding does it need, and how much room is left? Context figures in `references/models.md` are guesstimates, not limits.
|
||||
|
||||
One option is **trunk and forks**. At intake the builder reads the build plan and the code it touches (the trunk) and writes short trunk notes. Complex blocks in the trunk's area go to a fork of the builder, which inherits its model and (assumed) its context. Mechanical or distant blocks go to a fresh agent that reads the trunk notes. Which harness behaviour is verified and which is assumed: `references/spawning-claude-code.md`.
|
||||
The Athanor's option is a **trained implementer and gremlins**. One base per build, a headless session trained early: it reads the code the build touches and writes the trunk notes. One gremlin per complex block, spawned from the base with a delta brief, so each block starts from the base's reading. The base is rebased at phase boundaries or when the helper says so. Mechanical or distant blocks go to a fresh agent that reads the trunk notes. Mechanics and rules: `spire:gremlins`. It is Windows-tested; other platforms fail closed until tested, and there fresh agents build every block (a fresh agent then writes the trunk notes). Which harness behaviour is verified and which is assumed: `references/spawning-claude-code.md`.
|
||||
|
||||
## Phase review, fix, stop
|
||||
|
||||
@@ -66,6 +66,7 @@ One option is **trunk and forks**. At intake the builder reads the build plan an
|
||||
|
||||
- Read `references/spawning-claude-code.md` before dispatching. In short: sequential work runs in the foreground; resume agents only from the session root; leaf briefs say "do this yourself; do not dispatch further agents".
|
||||
- Every plan, handoff note and review stamps the exact model id the agent ran on.
|
||||
- Gremlins run on the base's full model id; `spawn` takes no model override.
|
||||
- Model traits are in `references/models.md`: dated observations, not law.
|
||||
|
||||
## Writing things down
|
||||
|
||||
@@ -17,20 +17,27 @@ Rules:
|
||||
- Plan: <path> · Build plan: <path>
|
||||
- Branch: <branch> · Start commit: <hash>
|
||||
- Trunk notes: <path>
|
||||
- Gremlins: <state dir> (or "none: <reason>")
|
||||
- Athanor: <model id> · Session journal: <path>
|
||||
```
|
||||
|
||||
A successor Athanor finds the state dir on the `Gremlins:` line and adopts it (`list`); it never re-runs `init`.
|
||||
|
||||
## Per phase
|
||||
|
||||
```markdown
|
||||
## Phase <N> — <title>
|
||||
|
||||
| Block | Owner (model id) | Commit | Tokens (if known) |
|
||||
|---|---|---|---|
|
||||
| Block | Owner (model id from modelUsage) | Session | Commit | in | out | cache write | cache read | cost Δ |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
```
|
||||
|
||||
Gremlin blocks: the row comes from `usage --block <id> --format md`. Fresh-agent blocks: owner = the model id the agent stamped, Session `—`, the usage cells `—`, and the harness's token total in the block's handoff note.
|
||||
|
||||
## Handoff note (one per block, written by the block's owner)
|
||||
|
||||
Same format for everyone. A gremlin writes it to `notes/<block>.md` in the state dir and the Athanor appends it here; a fresh agent writes it here directly.
|
||||
|
||||
```markdown
|
||||
### Block <N.M> — <title>
|
||||
|
||||
@@ -51,6 +58,8 @@ Rules:
|
||||
|
||||
- Gate: <command> — <result>
|
||||
- Tests: <command> — <result> (fallout fixed in block <N.M>)
|
||||
- Usage: the `usage --phase <N> --format md` table and its three lines (cold starts, cache-hit ratio, priced total)
|
||||
- Base state: from `list` (the active base, its warmth, rebase/re-train flags)
|
||||
- Flags for the reviewer: every deviation and every "flagged for review" line above, one per bullet, with its block.
|
||||
- Not run / untested: <anything the tests do not cover>
|
||||
- Status: provisional until review, fix pass and re-check close.
|
||||
@@ -58,6 +67,6 @@ Rules:
|
||||
|
||||
After the review, Igor (or the fixer) appends the review report path, the fix-pass commits, the re-check result, and the minors logged as follow-ups.
|
||||
|
||||
## Trunk notes (written by the Athanor at intake)
|
||||
## Trunk notes (written by the base at training; by a fresh agent where gremlins are unavailable)
|
||||
|
||||
Short, for the next agent that has not read the code: key files and seams, invariants the phase touches, every consumer of a changed semantic, the gate command, known traps. A page, not a design document.
|
||||
|
||||
Reference in New Issue
Block a user