9.2 KiB
9.2 KiB
Gremlins MVP: build report
- Date: 2026-10-16/17 · Orchestrator model: claude-opus-5-5 · Gremlin model: claude-opus-5-5 (all calls)
- Branch
gremlins-mvp(worktree.claude/worktrees/gremlins-mvp), frommainatb8affd8. Not pushed, not merged. - Spec:
2026-10-17-00-gremlins-mvp-spec.md. Tool: the prototype at.claude/worktrees/headless-forks/plugins/spire/skills/gremlins/gremlins.py, unmodified. - Wall time: about 21 minutes, 23:12:58 to 23:33:35, against a 90-minute timebox. Fallback: not needed; the prototype did not misbehave once.
How it ran
One base (B1) was trained on the spec, the verdict and the prototype source. It wrote no code. It wrote 70 lines of trunk notes, which pinned every cross-module seam: signatures, return shapes, the state-dir layout, the ledger and record keys, and its own decisions where the spec was open. Then eight gremlins were forked from B1, at most two running at a time (spawn --mode parallel --paths …, then wait --any / --all with Bash timeout 600000):
- Wave 1:
stream+launch, thenstore+call, each pair started as soon as a slot freed up. Committed inaff29d6. - Wave 2:
cli+entry.entryisgremlins.pyplus SKILL.md. Committed in9209e7dandfd75777. - Wave 3:
tests-unit(started inentry's freed slot, since it does not depend on cli), thentests-cli. Committed ind662790. - Integration fix:
79df026.
Every gremlin finished ok on its first attempt. No gremlin needed a follow-up turn, and none touched another gremlin's files. Each one wrote a note that listed every function with its box.
Gremlin metrics (from the prototype's results and ledger)
| Gremlin | Module | Wall s | Cost delta $ | Cache | Cache read | Lines written |
|---|---|---|---|---|---|---|
| base B1 (train) | trunk notes only | 407.8 | 1.9725 | n/a | 1,085,902 | 0 code (70 notes) |
| stream | grm/stream.py (+__init__.py) |
104.3 | 0.5175 | hit | 924,301 | 215 |
| launch | grm/launch.py |
66.7 | 0.4196 | hit | 783,210 | 182 |
| store | grm/store.py |
62.2 | 0.3815 | hit | 903,383 | 116 |
| call | grm/call.py |
80.0 | 0.4198 | hit | 908,838 | 86 |
| cli | grm/cli.py |
230.2 | 1.2211 | hit | 1,667,879 | 359 |
| entry | gremlins.py, SKILL.md |
52.2 | 0.3836 | hit | 916,241 | 14 + 54 |
| tests-unit | tests/test_{store,launch,stream,call}.py |
80.0 | 0.5441 | hit | 834,361 | 218 |
| tests-cli | tests/fake_claude.py, tests/test_cli.py |
197.3 | 1.1388 | hit | 1,982,799 | 275 |
- Base cost: $1.9725. The eight spawns together cost $5.0260. Build total (prototype
usage): $6.9985, 9 calls, 0 cold starts, 8 of 8 cache hits. - The live smoke of the MVP cost another $0.2204 (4 calls). That brings the grand total to about $7.22.
Final line counts against the spec's estimates
| Module | Lines | Estimate | Reason for the difference |
|---|---|---|---|
grm/store.py |
116 | 70 | docstrings; init_state and SUBDIRS added (trunk decision) |
grm/launch.py |
182 | 110 | the kept scrub constants and the tool lists; binary_version added |
grm/stream.py |
215 | 170 | iso/parse_iso moved here; TOKEN_KEYS; mismatch kept (4 lines, not used) |
grm/call.py |
86 | 60 | feeder and reader threads; the child is killed if on_start raises; a second signal escalates to kill() |
grm/cli.py |
359 | 200 | delta_header verbatim (~25); JSON argparse errors (~30); explicit job, ledger and base dicts (~40); status; docstrings |
gremlins.py |
14 | (in cli) | |
| Code total | 972 | 610 | The verdict estimated 600–800. No failure path was dropped, and nothing outside spec §4 was built. |
tests/ |
493 | about 300 | per-file boilerplate; the fake has 7 scenarios; B4 needs a subprocess and per-platform signalling; one extra test (below) |
SKILL.md |
54 | not in the spec | see Deviations |
Test and smoke results
- Unit tests:
uv run --no-project --python 3.12 python -m unittest discover -s tests, run from the skill dir: 24 tests, OK, 7.0 s. That is 14 module tests and 10 CLI tests, which cover every §6 case from B1 to B8. - Live smoke (MVP; real
claude.exe2.1.299; scratch git repo; modelclaude-opus-5-5):init:run.jsonwas written with the fingerprint, the argv andfp_hash, and a frozen preamble copy was made.train B1:ok, 5.0 s, $0.0969, session id taken from--session-id.spawn s1:ok,cache: hit(69,651 tokens read, 1,664 written), 36.5 s, cost delta $0.0431,base_state: warm. The note was written and the repo was left untouched.- The ledger had two lines, each with
model,models_used, six token columns,cost_delta_usd,cost_cumulative_usdandhit_at. status: B1warm,ttl_s3600,ttl_left_s3566.
- Parallel test (B3): two
spawns, p1 and p2, ran as separate Bashrun_in_backgroundcalls. Both started at 23:30:33 and ended at 23:30:44 and 23:30:45. Both completion notices arrived with the result JSON and exit 0, and both reportedcache: hit. The run ended with two new job files, two new whole ledger lines (four in total) and two notes. - Processes afterwards:
Get-CimInstance Win32_Processshows noclaude -p,pythonoruvprocess left. The onlyclaude.exeprocesses are the Desktop app and its own sessions. The prototype'slistshowed nothing running, andexpire --allretired its base B1, so no keep-warm ping can fire later. - Not run live: the B4
TaskStopon a backgroundedspawn(spec §5 step 4), a B7rebase, and the B8 gate on WSL. These paths are covered only by the fake-claude tests: CTRL_BREAK to the helper subprocess, and rebase argv and session id.
Deviations from the spec, each with its reason
SKILL.mdwas added, although it is not in the module table. Reason: B3 and B4 only work if the orchestrator is told to runspawnviarun_in_backgroundand to stop it withTaskStop, and the skill file is where it is told.templates/rebase.mdwas reworded ("a fork … becomes the new base${new_base}(phase …)" became "base${base}itself, resumed"). cli stopped passingnew_baseandphase. Reason: §3 said to use the templates as is, but the old text contradicts B7, which resumes without a fork and keeps the same base. The cli gremlin flagged it, and the fix is in79df026.spawn.md:${commit_rule}became the fixed line "Commit only if the brief below says so", and${mode}was removed. This is per spec §3; the fixed line is so that the rule is still visible.- Trunk-note decisions where the spec was open:
build_fingerprinttakesmcp_configand returns{fingerprint, argv, fp_hash}.- The argv builders take the stored run dict, so that a test's fake
argvis honoured. resume_argvhas no--session-id, because claude refuses it on a resume without a fork.- The fingerprint adds
allowed_toolsanddisallowed_tools, becausedontAskdenies every tool that is not allowed. resolve_binaryreturns a str, not the prototype's tuple.
models_usedis ordered by usage, largest first, not by name. Reason: by name, a haiku side-call could becomemodel(=models_used[0]).- The ledger line adds
hit_at. Reason: B6 derives warmth from the ledger, and the line'stsis the end of the call, not the time the cache was read. - Job ids:
- train is
<B>-train; it can be re-run only while there is no base file. - rebase is
<B>-rebase<n>; a failed rebase re-run overwrites its own job and raw files. - spawn takes
--job J, and an existing J is refused.
- train is
- The base's
cost_cumulative_usdis stored unrounded. Ledger and printed costs are still rounded once, at 6 decimals. Reason: a rounded basis made the deltas drift by 1e-6 (0.020001 instead of 0.02). StreamStateprefers the result'ssession_idover the init event's. Reason: spec §7, B7.- Exit codes are 0 / 1 / 2: 0 ok, 1 a call that ended
errororstopped, 2 refused.statusrefuses a run with norun.json. - No
SPIRE_GREMLIN_CHILDrefusal. Reason: no box needs it, and it would break the helper's own tests when they run inside a gremlin. - One extra test,
test_call.test_tee_and_parse. Reason: it is the only check that the prompt goes in on stdin as UTF-8 and that the stream is teed to the raw file and parsed.
Observations on the dogfooding
- The prototype raised
rebase_due: phase_boundaryon every wave-2 and wave-3 spawn, because--phasewas used as a wave label. The flag is advisory, so it was ignored. The delta header gave the forks the new commits instead. - The trunk notes were what made the parallel work fit together: four modules written blind against each other imported cleanly on the first try. The only integration fix was the template wording.
- Each fork read 0.8 to 2.0 M tokens from cache, at about $0.38 to $1.22 per module. The base's $1.97 was spent once.
Open items
- Live B4 check: a
TaskStopon a backgrounded MVPspawn. - Live B7 check: does a plain
--resumereport a cumulativetotal_cost_usd? The cost basis assumes it does. - The B8 gate on WSL.
- Cross-process ledger appends on Windows are not strictly atomic (CRT seek + write). This is accepted per the trunk notes. The live parallel run gave whole lines.