v0.3.0
The stability sprint. Five-phase overhaul focused on making v3
actually stable for users beyond the maintainer. Adopted patterns
from Davidvandijcke/coarse (branching model, slash commands, headless
CLI backends, OIDC PyPI publishing). Test count: 221 → 290.
Cross-lane — Foundation & process
- dev/main branch model:
devis now the default integration branch;
mainis released-only and branch-protected (PR required, no force
push, no deletion). All feature work goes through PRs intodev. AGENTS.md(new) codifies the three-lane split (Pipeline / Lit /
Data), the public contracts each lane owns, hard rules (no live runs
fromdevwithout smoke-pass, cross-lane changes need explicit flag),
and the tag-driven release procedure..claude/hooks/session-brief.shruns at SessionStart to dump
branch, recent commits, CI status, and in-flight paper runs into
every new Claude Code session.scripts/release_audit.py+make release-audit: hard-gates
(version match betweenpyproject.tomlandsrc/__init__.py, clean
tree, CHANGELOG has entries, noTODO(release), pytest passes) + soft
gates (on-main, CI green). Required before tagging..github/workflows/release.yml(new) tag-driven (push:tags:v*).
Enforces tag↔pyproject↔__init__.py version triple-match, runs tests,
builds wheel, creates GH release. Publishes to PyPI via OIDC trusted
publishing — no API token secret.- Path-filtered per-lane CI (
ci-pipeline.yml,ci-lit.yml,
ci-data.yml): each lane runs only its own tests; fulltests.yml
runs on every dev/main merge. - CHANGELOG per-lane organisation: entries under
## Unreleaseduse
### Lane A,### Lane B,### Lane C,### Cross-lanesub-headings.
Lane A — Pipeline (Phase 2-5)
- Specialist circuit breaker (
src/core/strategist/runner.py+
state.py): tracks consecutive failures per non-tolerant specialist.
After 3 failures, the runner raisesCircuitBreakerError, marks the
paperPAUSED(new state), logs acircuit_breaker_trippedevent,
and returns cleanly. Tolerant specialists (reviewers, polish) are
exempt — their failure is non-blocking. Fixes the run #14 failure
mode where data_analyst was re-dispatched 3+ times when Allium was
unrecoverable, burning 13 specialists before manual cancel. - Resume from last completed stage (
POST /api/papers/{id}/resume):
re-enters the pipeline at the first phase whose canonical artifact is
missing. Eligible frompausedorfailed. Avoids re-running phases
that already succeeded after fixing a downstream issue. - Turn-budget signal in specialist prompts (
src/core/specialists/base.py):
the system prompt opens with a "Turn Budget" section telling the
model its max_turns and to write a first version of the canonical
artifact within the first half. Fixes the run #16 failure where
data_analyst saved write_file for the last turn and hit max_turns
mid-pagination. SPECIALIST_SKILLSconsolidation: previously two parallel dicts
(registry.pyandloader.py) that drifted whenever someone added a
skill to one but not the other. Now one source of truth in
registry.SPECIALIST_SKILLSwith full paths (data/cleaning); loader
resolves them.- Codex headless backend (
LLM_BACKEND=codex, src/modules/llm/codex.py)
for ChatGPT Plus/Pro subscriptions. Shells out tocodex exec— same
pattern as the existing Claude Code backend. Adapted from coarse. - Gemini headless backend (
LLM_BACKEND=gemini, src/modules/llm/gemini.py)
for Google AI Pro/Ultra. Probes--approval-modevs legacy--yolo
at startup. Adapted from coarse. - Skills bundled in the wheel (
pyproject.toml+skills/__init__.py):
all 49 skill .md files ship in the e2er wheel.e2er install-skills [--backend claude|codex|gemini|all] [--force]copies them to the
per-CLI skills dir (~/.{backend}/skills/). - Pre-run safety: PR-time contract tests for specialist artifacts
(every specialist inSPECIALIST_ARTIFACTShas registered skills,
every skill path resolves to a real .md file, reviewer/polish lists
stay aligned with registries) and integration smoke (theoretical
pipeline end-to-end via MockLLMBackend, FastAPI POST surface,
cascade-detection halt). POST /api/papers/{id}/resume(new endpoint, see above).GET /api/papers/{id}/failure-bundle(new): single-call
diagnostic returning paper status + last_error (untruncated), every
pipeline event with full payload, per-specialist drill-down
(untruncated error_msg), workspace artifact listing (present vs
missing), and the data_summary.md excerpt. Replaces the
4-endpoint scavenger hunt diagnosis used to require.- Slash commands (
.claude/commands/*.md):/pre-pr,
/diagnose-run,/run-paper,/release-audit.
Lane B — Literature (Phase 2)
- Provider contract tests (
tests/lit/contract/test_provider_shapes.py):
first dedicated Lane B tests. For OpenAlex, Semantic Scholar, and
arXiv, mock the documented response payloads and verify parsers
handle standard shape, empty results, and network errors without
crashing or raising into specialist code.
Lane C — Data (Phase 2-3, 5)
- Live OpenAPI contract tests (
tests/data/contract/test_allium_developer_schema.py):
validatesAlliumDeveloperProvidermethod kwargs against Allium's
published OpenAPI specs (snapshots cached in
tests/data/fixtures/). Catches required-param drift, list-vs-object
body-shape mismatches, and silently-ignored unknown params. Run
#14-#18's wrapper bugs would have been red CI checks instead of
burning real specialist invocations. - Nightly schema-drift workflow (
.github/workflows/schema-drift.yml):
re-fetches Allium's live OpenAPI at 03:30 UTC, diffs against the
cached fixtures, opens a labelled issue with the unified-diff
artifact if anything changed upstream. - Data-layer degradation breaker (
src/modules/data/allium_developer.py):
tracks a sliding window of recent call outcomes. If >50% of the last
6 calls errored, subsequent calls short-circuit with a structured
"data layer degraded" envelope BEFORE hitting the network. Stops a
specialist from draining its turn budget on dozens of 429 retries.
Self-clears on the next successful call. GET /api/papers/{id}/data-queries(new): every Allium-style
query the run submitted, with validation/approval status, executed
timestamps, row counts, plus a rolled-up summary. Replaces the
manualcat audit_log.csv | grepworkflow.
Fixed — Real bugs from the May 2026 NFT-marketplace live run
Root cause (the hard lesson): v3 made an architectural change v1/v2 didn't have — instead of delegating to the Claude Code CLI subprocess, it owns the tool-use loop directly via the Anthropic / OpenRouter SDKs (so AlliumToolHandler can intercept every tool call for guardrail validation). That introduced a class of bugs the unit-test suite never covered: the layer was never pressure-tested with realistic specialist output sizes. MockLLMBackend returns short canned outputs, so unit tests never saw the failure modes that hit on the first live run.
The May 2026 run lost ~$8 across two attempts before the diagnosis: data_architect writing data_dictionary.json as a single tool call exceeded max_tokens_per_call=16384, the model's output was truncated mid-write (finish_reason=length), the tool_loop correctly bailed (looping is futile — same wall every retry), the specialist was marked failed, and downstream specialists silently cascaded.
src/config.py:max_tokens_per_calldefault bumped 16384 → 32768. Both Sonnet 4.6 and Haiku 4.5 support 64K out; 32K is a safe floor for the largest single tool argument any specialist emits.src/core/specialists/base.py:_MAX_TURNS25 → 40. Independent issue from the same run — Sonnet specialists with Allium tools needed 29-38 turns to converge; 25 was tight enough thatidea_developerhit the cap.src/core/specialists/dispatcher.py: cascade detection added toexecute_parallel. After each batch, any non-tolerant specialist (anything not a reviewer / polish specialist) whose canonical artifact is missing now raisesRuntimeErrorimmediately — preventing downstream specialists from running on absent inputs and looping. Reviewer / polish specialists are still tolerant of partial failure (the aggregator handles gaps).src/api/app.py: invalid UUIDs on/papers/{id}and/api/papers/{id}now return 404 instead of 500. Previously a typo'd URL surfaced aspsycopg.InvalidTextRepresentation→ 500.
Added — Stress tests for the tool-loop layer
tests/test_tool_loop_stress.py — five tests covering the failure modes mocked unit tests miss:
- 30 KB JSON tool argument forwarded to handler intact (the NFT-paper repro).
- 100 KB tool result threaded back into the message history verbatim.
finish_reason="length"produces an actionable error referencing the setting to fix (so devs don't chasemax_turnslike I did).max_tokens_per_calldefault >= 32K (config-level floor).- 25-turn message accumulation with correct token-usage summing.
Plus tests/test_security_review_fixes.py regression test for the cascade-detection behaviour above (test_execute_parallel_raises_on_missing_canonical_artifact).
Added (P3 engineering hygiene)
- mypy in CI.
mypy src/runs afterruffin.github/workflows/tests.yml.
Config inpyproject.tomlis pragmatic (catches real bugs without grinding
on annotation completeness):no_implicit_optional,strict_equality,
warn_redundant_casts. Per-module strictness can be ratcheted up later. - Pre-commit hooks.
.pre-commit-config.yamlruns ruff (with --fix),
ruff-format, mypy, and standard hygiene hooks (trailing whitespace, large
file check, merge-conflict markers, detect-private-key) on every commit.
Install withmake hooks. Makefilegainstypecheckandhookstargets; help output updated.- CONTRIBUTING.md gains a "Pre-commit hooks (recommended)" section and a
"Local checks before pushing" cheat sheet.
Fixed (real bugs surfaced by mypy)
src/modules/literature/bibtex.pywas callingbibtexparser.load(f, parser=...)
which does not exist in bibtexparser v2 (project pins>=2.0.0b7).
Any user with aLITERATURE_BIBTEX_FILEset would have hit
AttributeError: module has no attribute 'load'at runtime. Migrated to
bibtexparser.parse_file()and the v2Entry.fields_dictAPI, with a
glue layer keeping_entry_to_metadata's dict interface unchanged.src/modules/literature/arxiv.py:el.textwas accessed on an
Optional[Element]without a None check — would raiseAttributeError
on author entries with missing<atom:name>tags.src/modules/github/client.py:Github.get_user()returns
NamedUser | AuthenticatedUser; onlyAuthenticatedUserhascreate_repo.
Cast added so type narrowing works without breaking tests that mock the
user with MagicMock.src/modules/github/push.pyandsrc/api/app.py:GitHubClient(token, user)
was constructed withOptional[str]arguments that the constructor types
asstr. Added explicit None check (returns early when github is configured
but token/username are missing) plus assert in push paths.src/api/app.py:for e in eventsshadowed theexcept Exception as e
variable on the line above, which Python 3 deletes after the except block
closes. Renamed toexc/evto remove the deleted-variable read that
mypy correctly flagged.src/modules/llm/base.pytool_looptypedtool_handler: ToolHandler,
but engine.py was calling it withtool_handler=Nonefor tool-less
strategist decisions. Widened the abstract signature (and both concrete
backends) toToolHandler | None, with explicit None handling that
surfaces a clear error if the model nonetheless requests a tool.src/modules/data/tools.py:resultwas reassigned fromValidationResult
todict[str, Any]in the same function, which mypy correctly flagged as
a type confusion. Renamed the second binding toquery_result.
Added
- Methodology selector — papers now accept a
methodologyfield at
creation time:empirical(default, unchanged),theoretical(formal
model only, no data/econometrics specialists),mixed(formal model +
empirical test). Surfaced in the dashboard form andPOST /api/papers. - New
theory_specialist(writesmodel_spec.md) ported from E2ER v2.
Dispatched by the strategist when methodology istheoreticalor
mixed. Skill bundle:base/economist,modeling/game-theory,
modeling/asset-pricing,math/proof-strategies,
reasoning/identification. sql/009_papers_methodology.sql— addsmethodologycolumn to the
paperstable with a CHECK constraint.tests/test_methodology.py— 12 tests pinning the registry, prompt
contract, manifest persistence, API validation, and specialist
invocation.- GitHub Actions CI: ruff lint + format + pytest on Python 3.11 and 3.12,
triggered on push tomainand all PRs. - Branch protection on
mainrequiring both pytest matrix jobs to pass
before PR merges. Makefilewithmake smoke(free,10s, all 155 mocked tests) and$0.50 Haiku end-to-end test).
make smoke-paid(- Issue templates (bug report, feature request) and PR template under
.github/. tests/test_pipeline_resilience.py— 18 tests guarding against upstream
result loss when downstream phases fail (crash injection per phase,
resume-without-redo, artifact persistence, GitHub push idempotence,
state-file atomicity, no-op replay).SECURITY.mdandCHANGELOG.md.
Changed
PipelineState.save()now writes atomically (tmp + rename) and keeps a
.bak. Previously a crash mid-write could corrupt the only state file
and lose all upstream progress on resume.PaperStatusandPipelineModenow inherit fromStrEnum, matching
the pattern used in E2ER v2.- Renamed
BudgetExceeded→BudgetExceededError(PEP 8 exception
naming). src/api/app.py:TemplateResponsecalls migrated to the
(request, name, context)signature for current Starlette.
Fixed
tests/test_regressions.py: removed hardcoded absolute path that
would have failed on every CI runner.src/modules/data/audit.pyandtools.py: missing top-level
from pathlib import Path(worked at runtime only viafrom __future__ import annotations).src/modules/literature/bibtex.py: removed unuseddoi_indexdead
code that hid an incomplete dedup intention.