Releases: bhanneke/E2ER-project
Release list
v0.8.1
Stability + corpus extensions. Bug fixes from a full code review —
safety (Allium guardrails no longer bypassed without a
data_dictionary.json; SQLite Allium-approval workflow works; SSRF
hostname resolution), Lane-A robustness (strategist JSON guards,
mechanism-gate, resume-status, single-order cascade), Lane B/C wins
(storage citations; OpenAlex/S2 null crash; e2er run --acknowledge- unproven; FileToolHandler sandbox; OpenRouter content=""). Plus
user-driven additions: structured GitHub issue templates for data-source
and literature-provider requests, and LOCAL_DATA_DIR extensions
(comma-separated roots, recursive walk, PDFs staged into
workspace/literature/ with read_reference(path=…)).
Cross-lane
- Structured GitHub issue templates for the most common asks:
data_source_request(provider, auth, coverage, example RQ) and
literature_provider_request(capability, gap, auth). Both routed by
lane-*/provider-requestlabels. Generic feature requests still go
viafeature_request.md.
Lane A — Pipeline
- Fix: malformed strategist JSON no longer crashes the paper.
ceiling_checkandrun_self_attackdid a barejson.loadson LLM
output — truncated/invalid JSON raised and failed the whole run. They now
use the tolerantextract_jsonand skip malformedWorkOrder/finding
items instead of raising. - Fix: a missing mechanism-reviewer score can no longer be silently
accepted. The Rule-1 mechanism gate no-op'd when the mechanism score was
absent, letting a paper ACCEPT on the other reviewers' average. A missing
(but expected) mechanism score now forcesMAJOR_REVISION. - Fix: resume tolerates a bad/legacy persisted status.
PaperStatus( state.last_status)could raiseValueErrorand wedge a completed paper
into FAILED on resume; it's now coerced with a safe fallback. - Fix: tier-0 context builder handles explicit-null manifest fields
(datasets: null/research_question: null) instead ofTypeError.
Lane B — Literature
- Fix:
store_paperpersists citation counts.citationswas in the
ON CONFLICT DO UPDATEclause but missing from the INSERT column list, so
inserts dropped the count and conflict-updates zeroed it. Added to the
insert. LOCAL_DATA_DIRextensions. Accepts a comma-separated list of
roots, an opt-inLOCAL_DATA_DIR_RECURSIVE=trueto walk
subdirectories (paths underworkspace/data/are preserved), and now
also stages*.pdfintoworkspace/literature/. The bib-relevant
specialists' reference summary lists those local PDFs so they can be
read via the newread_reference(path=...). New
src/modules/local_corpus.pyconsolidates parsing/walking;
LocalBibLibraryuses it for.bibdiscovery across multiple roots.read_referenceaccepts a newpathargument (workspace-
relative) for the staged local PDFs — no download, no auth, sandboxed
under the workspace root.
Lane C — Data
- Fix (safety): guardrails no longer fully bypassed without a data
dictionary._query_alliumonly ranvalidate_allwhen a
data_dictionary.jsonwas present, so a production query with no
dictionary ran with ZERO validation. Now the structural rules (no
SELECT *, time-bound) and feasibility-first/approval gate always fire;
only the field-whitelist (Rule 2) is dictionary-gated (skipped with a
warning). - Fix: audit inserts generate app-side UUIDs.
log_query/
create_approval_requestrelied on a DB id default; SQLite has none, so
idwas NULL and the approval-request join silently never surfaced
pending production queries on the default SQLite DB. Now both generate a
uuid4()client-side — the Allium approval workflow works on SQLite.
Cross-lane
- Fix: cost-estimate labeling for the codex/gemini backends.
app.py
checkedcodex_cli/gemini_cli, but the real backend literals are
codex/gemini, so synthetic cost figures were mislabeled as real. - Fix:
literature_kb_enabledhonorsDATABASE_URL. It keyed off
legacypostgres_url/db_password, leaving the pgvector KB silently off
for the documentedDATABASE_URL=postgresql://…path. Now derived from
the resolved DB URL. - Fix (security): SSRF guard resolves hostnames.
_check_urlonly
blocked literal private IPs; a hostname (e.g.metadata.google.internal
→ 169.254.x, orlocalhost) slipped past. It now resolves the host and
blocks if any resolved address is private/loopback/link-local. - Fix:
e2er run --acknowledge-unprovenflag. The CLI hardcoded
acknowledge_unproven_tuple=True, silently disabling the $1 first-run
floor (and the README documented a flag that didn't exist). The flag now
exists (default off → floor enforced for metered backends); the $0
flat-rate CLI backends (claude_code/codex/gemini) auto-acknowledge. - Fix: single-order dispatch gets the cascade guard. The missing-
canonical-artifact check ran only inexecute_parallel; a lone specialist
could "succeed" without its artifact and starve downstream work. Extracted
assert_artifacts_written, now applied to both paths. - Fix:
FileToolHandlersandbox uses path containment, not a string
prefix (a sibling workspace with a prefix name could escape). - Fix: OpenRouter tool-only turns send
content=""instead ofnull
(some OpenAI-compatible servers rejectnullcontent + tool_calls).
v0.8.0
Pluggable data & literature providers. Specialists now discover data
sources in light of the research question — FRED and yfinance reach the
tool loop via list_data_sources + a unified fetch_data, and Allium sits
behind a Warehouse capability (its 5 guardrails unchanged). They also
pull the researcher's own reference library (local .bib, LOCAL_DATA_DIR,
and Zotero via the Web API) and read full-text PDFs (read_reference).
Both lanes are now registry-pluggable, so new providers are drop-in.
Cross-lane
scripts/live_check.py— live smoke harness. Exercises the real
data/literature provider paths (yfinance, FRED, Allium connectivity,
OpenAlex search,read_referenceon an OA PDF, Zotero library) against
live services, auto-skipping providers without credentials. No LLM calls
(free). Complementsmake smoke(offline/mocked) andmake smoke-paid
(full LLM run). Run:python scripts/live_check.py.
Lane C — Data
- Allium folded behind a
Warehousecapability (M3b of
docs/MODULARIZATION_PLAN.md). Allium is now a first-class registered
provider:AlliumWarehouseowns itscard(),tools()(→ALLIUM_TOOLS)
andhandler()(→DeferredAlliumToolHandler);_run_pipelineassembles
it by iteratingwarehouses(settings)instead of hardcoding, and the
catalog builds its card from the warehouse. Pure refactor — same condition
(Allium key present), same tools, the 5QueryValidatorguardrails and
approval flow are untouched, andhas_allium/data_module_enabledare
unchanged. Completes the Lane-C registry (series + warehouse). - Series data in the agent loop + RQ-aware discovery (M3a of
docs/MODULARIZATION_PLAN.md). FRED and yfinance are no longer
CLI-only — specialists reach them in the tool loop. NewSeriesFetcher
capability + data registry (providers.py,registry.py) mirror the
Lane-B pattern. Two new tools:list_data_sources(serves the registry
catalog so the agent picks the right source for the research question)
and a unifiedfetch_data(provider, method, params). Allium is unchanged
— it keeps its guardedquery_alliumtool and is advertised in the
catalog (the 5 guardrails are untouched). Series tools are always on
(yfinance needs no key); budgeted (_MAX_FETCHES=20). M3b will fold
Allium behind aWarehousecapability into the same registry.
Lane B — Literature
- Fix: literature search crashed on OpenAlex/S2 explicit nulls. A live
search returned 0 papers becauseopenalex._parseraised
'NoneType' object has no attribute 'get'on a result whose
primary_location.source(oropen_access/authorships) was an
explicitnull—.get(k, default)doesn't apply the default for a
present-but-null value. Both parsers now guard withor {}/or [].
Regression tests added (the mocked payloads previously only used
well-formed fields, so the bug only surfaced live). - Full-text
read_referencetool (M2.5 ofdocs/MODULARIZATION_PLAN.md).
Specialists can now read a reference's PDF in full to deepen the lit
review, not just its abstract. Newread_referenceliterature tool takes
apdf_url(surfaced in search/fetch results and on[PDF]-marked
reference-list entries, incl. Zotero attachments) or adoi(resolves an
open-access PDF). Downloads (auth'd for Zotero hrefs,/file/view→
/file), extracts text via pypdf (pdf.py), and returns it
truncated to ~20K chars. Tightly budgeted (_MAX_READS=6+ per-read char
cap) given the prior 522K-token literature blowup.fetch_bytesgained a
max_bytesoverride (PDFs exceed the 2 MB default). Newpypdfdep. New
ZoteroLibraryReferenceLibraryreads the researcher's Zotero library
via the Web API's native JSON (zotero.py), maps items to
PaperMetadata, and captures each item's primary PDF attachment href
(for the planned on-demandread_referencetool, M2.5). Config:
ZOTERO_API_KEY+ one ofZOTERO_USER_ID/ZOTERO_GROUP_ID; merged
into the reference summary after local.bib, deduped by (title, year).
Unset → no-op. Syncfetch_text_synchelper added for the (sync)
reference-library path. Degrades to[]on any Zotero error — can't
break paper creation. - Provider interface + registry (M1 of
docs/MODULARIZATION_PLAN.md).
Formalized the de-facto interface the source modules already shared into
capability sub-types —SearchSource(web discovery; OpenAlex, arXiv,
Semantic Scholar) andReferenceLibrary(the researcher's own corpus;
LocalBibLibraryoverLITERATURE_BIBTEX_FILE+LOCAL_DATA_DIR) — in
newproviders.py/registry.py.LiteratureToolHandlerand
_load_reference_summarynow iterate the registry instead of hardcoding
provider names. Pure refactor: the search (OpenAlex→arXiv) and DOI-fetch
(OpenAlex→S2) fallback chains are reproduced exactly; +13 tests, no
behaviour change. This is the seam Zotero (M2) and Citavi (M4) plug into.
v0.7.3
Fix the patch_revisor section-target resolution bug surfaced by
the v0.7.2 live re-validation on paper 7f4f2363. The drafter
got a paper all the way through to the revision phase (v0.7.0's
verify_numbers parser fix worked), but the patch_revisor emitted
edits targeting canonical section names (section:results,
section:mechanism) that didn't exist in the actual draft. The
merger reported "target region not found" with no hint and the
paper REJECTED on parser bugs, not real hallucinations — for the
second release in a row.
Lane A — Pipeline
- Merger emits "did you mean..." suggestions on section/table
not-found. Whenapply_editcan't resolve asection:or
table:target, the error message now appends the list of
available section titles or labelled tables in the document.
Example before/after:- Before:
target region 'section:results' not found in document - After:
target region 'section:results' not found in document (available sections: 'Introduction', 'Identification Strategy', 'Empirical Strategy', 'Discussion')
Two new public helpers:list_section_titles(text)and
list_table_labels(text). Suggestions are suppressed when the
list is empty (avoids the misleading
(available sections: )suffix on minimal LaTeX skeletons).
Universal targets (paper:full/abstract/references)
don't get suggestions.
- Before:
writing/scoped-revision.mdskill update. New section
("Before you compose any edits — list the draft's actual
targets") instructs the patch_revisor to grep the draft for
\section{...}and\label{tab:...}lines before composing
patches. Explains the case-insensitive substring matching the
merger uses, the common failure mode (canonical-name vs
actual-heading mismatch), and thepaper:fullfallback for
findings that don't have a dedicated section.
Test counts
- Mocked suite: 598 passed (was 590 in v0.7.2; +8 here).
- 8 new tests in
tests/pipeline/test_patch_merger.py:- 4 for
list_section_titlesandlist_table_labelshelpers. - 4 for the extended error: section suggestions, table
suggestions, no suggestion for non-section/table targets, no
misleading suffix when the list is empty.
- 4 for
v0.7.2
Closes the v0.7.1-noted follow-up: a CLI command to resume
paused / failed / zombie papers. Completes the status / cancel /
resume trio so the operator never has to drop down to curl.
Cross-lane
e2er resume <paper_id>— restart a paused or failed
paper from the terminal. Optional--max-cost Nraises the
cap atomically with the resume (sent through to the v0.5+
ResumeRequestbody). Surfaces the paper's title + previous
status + cap delta +last_errorbefore issuing the POST, so
the operator knows what they're restarting. Unlikestatus
andcancel, this command DOES auto-start uvicorn — the user
is asking the paper to start running again, so the server
needs to be up.- 200 → prints the new transient status (
resuming) +
dashboard URL, optionally tails to terminal via--tail - 400 → surfaces the validation detail (e.g. non-positive
cap) directly so the user can fix and retry - 409 → "already running" with a hint to
e2er cancelfirst - 404 → "paper not found"
- 200 → prints the new transient status (
- 9 new regression tests in
tests/test_cli_status.py
covering: no-cap-change happy path, cap-raise happy path,
completed-paper short-circuit, 400 / 409 / 503 / 404 error
paths,--tailintegration, the API-unreachable branch.
Test counts
- Mocked suite: 590 passed (was 581 in v0.7.1; +9 here).
v0.7.1
Two new lightweight CLI commands surfaced by the v0.7.0
fresh-install UX test: when e2er run's tailer times out (or
the user ^C's it), there was no scripted way to re-attach,
inspect the current state, or cancel a runaway paper without
opening the dashboard.
Cross-lane
e2er status <paper_id>— one-shot snapshot of a paper:
status, mode/methodology, cost meter (with the
cost_is_estimatemarker on CLI backends), specialist call
count, token total, workspace path, dashboard URL. Shows
last_errorverbatim when present so the user can diagnose
REJECTED / PAUSED / FAILED without parsing the events log.
With--tail, re-uses the same polling loope2er runuses
so the user can re-attach after ^C. Short-circuits on already-
terminal status (no wasted polls). Hits the local API by
default; respectsE2ER_API_URLfor remote inspection.e2er cancel <paper_id>— POSTs the/cancelendpoint
with a confirmation prompt (skippable via--yes). Surfaces
the title + current status + spend-so-far before the user
confirms so they don't cancel by accident. Terminal-status
short-circuit. Treats post-cancel 404 as success (the paper
finished while we were asking; that's what the user wanted).
Brief post-cancel poll so the user sees the CANCELLED
transition land before the shell returns.- Cost output now formats with two decimals. Pre-fix
e2er statusshowed$8.462921999999999; now$8.46. Float
noise was reaching the user-facing string when the API
returned high-precision cost totals. _poll_statusnow treatsrejectedas terminal. Pre-fix
thee2er runtailer kept polling forever on REJECTED papers
(a v0.5+ status it didn't know about). Observed during fresh-
install testing on paper 2ca473aa.
Test counts
- Mocked suite: 581 passed (was 554 in v0.7.0; +27 cli_status).
- 27 new tests in
tests/test_cli_status.pycovering
formatters, exit codes, the unreachable-API branch, the
confirmation prompt, and the post-cancel-404 race handling.
Known follow-up (v0.7.2 candidate)
e2er resume <paper_id>— natural complement tocancel.
PAUSED papers can be resumed viacurl POST /resumetoday;
a CLI command would close the same UX gap thatstatusand
cancelclose. Out of scope for v0.7.1.
v0.7.0
Better onboarding + a verify_numbers parser fix, bundled.
Surfaced by direct user feedback ("pip install e2er and then
what?") and by the v0.6.1 live run on paper f79b7cd9 that hit
two false-positive critical mismatches caused by parser bugs.
Cross-lane
- New
e2er initcommand — guided first-paper setup wizard.
Closes the post-pip install e2eronboarding gap. Walks the
user through 4 steps (LLM backend pick + prereq check, data
module on/off, optional BibTeX path, optional Postgres
DATABASE_URL), an optional GitHub-integration prompt, then
writes./.env(with confirm-overwrite), runse2er install-skills, and prints three concrete example research
questions to copy. Hand-rolled stdin wizard — no new
dependencies (noclick/prompt_toolkit). TTY-detected so
non-interactive invocations exit with a helpful one-line guide
instead of blocking oninput(). Secrets discipline: GitHub
PATs and API keys collected during the wizard are written to
.envas comments, never as live env vars. 24 new unit tests
intests/test_cli_init.py. README quickstart updated to lead
withe2er init.
Lane A — Pipeline
- Fix two
verify_numbersfalse-positives: ISO date strings
in column headers (2021-03-01) were being parsed as the bare
year2021, false-positive-mismatching against unrelated
source values; and LaTeX brace-protected thousands separators
(1{,}573.89— the form that survives math mode) were being
split into two bogus numbers (1and573.89). Both surfaced
on the v0.6.1 live-validation paperf79b7cd9, which was
REJECTED entirely on parser bugs rather than real
hallucinations. New_normalize_cell(cell)helper runs
before_NUMBER_REon each tabular cell: normalizes{,}→
,so the existing thousands branch picks the value up
intact, then strips ISO / slash / US date patterns so years
inside dates don't leak as numeric claims. Bare years outside
date context (e.g.Sample size & 2021) still extract — the
fix is targeted at dates, not all four-digit numbers. 5 new
regression tests intests/pipeline/test_verify_numbers.py.
Test counts
- Mocked suite: 554 passed (was 525 in v0.6.1; +24 wizard +5
verify_numbers fix).
v0.6.1
Hot-fix on v0.6.0 closing the known follow-up surfaced by the
v0.6.0 live run on paper 3bc58e8d.
Lane A — Pipeline
- Iterative-phase guard extended to drop the legacy
revisor
on iterations 2+, alongsidepaper_drafter. Both specialists
rewritepaper_draft.texfrom scratch every time they run, so
the same drift argument that motivated step 6's
paper_drafterguard applies torevisor. v0.6.0's live run
showed the strategist dispatchingrevisorduring iterative
phase even thoughpaper_drafterwas correctly skipped — the
guard only filtered one. v0.6.1 closes the same door for both. - Strategist system prompt updated to name
revisor
explicitly alongsidepaper_drafterin the iterative-phase
rule, and to point atpatch_revisor(dispatched automatically
by the runner's revision phase) as the legitimate path for
scoped revisions. Removes the v0.6.0 ambiguity where the prompt
said "userevisoronly when upstream artifacts are updated"
but the runner now expects norevisorcalls in iterative
phase at all. test_section_writer_not_dropped_on_iteration_2renamed to
test_legitimate_specialists_not_dropped_on_iteration_2and
updated to reflect the v0.6.1 contract (was assertingrevisor
survives the guard, now asserts only the legitimate specialists
do).- 4 new regression tests in
test_iterative_phase_guard.py
pinning the extended-guard contract.
Full mocked suite: 525 passed (was 521 in v0.6.0; +4 here).
v0.6.0
Targeted-revision discipline. Closes the three drift sources
identified in docs/V0.6_PLAN.md: full-rewrite revisor on
MAJOR_REVISION, parallel-revisor write race in self-attack, and
unconstrained paper_drafter re-dispatch in the iterative phase.
Validated end-to-end on paper 3bc58e8d (2026-05-22, 38 min,
$12.36 est., Sonnet via Claude Code CLI).
Lane A — Pipeline
- New
patch_revisorspecialist + deterministic merger.
Replaces the pre-v0.6revisorin every dispatch site. Writes
structured edits topaper_draft.tex.edits.json; the merger
(src/core/strategist/patch_merger.py) validates each edit's
targetagainst the work order'sFindinglist, applies
in-scope edits topaper_draft.tex, and emits
paper_draft.tex.applied.diffas a unified-diff audit
artifact. One edit type supported in v0.6:replace_text
withfind/replace/find_must_be_unique. Target
schema:section:<name>/table:<label>/references/
abstract/paper:full. Edits whose target isn't in the
findings are rejected before any text is touched. - Structured
Findingdataclass + three collectors. New
src/core/strategist/findings.pyintroduces the
Finding(source, source_detail, target, severity, problem, suggested_fix)frozen dataclass that every revision source
emits:collect_self_attack_findings,
collect_verify_numbers_findings,collect_review_findings.
combine_findingssorts severity-desc with source priority
(verify_numbers > self_attack > review on ties — numerical
mismatches are the most mechanical to fix). - MAJOR_REVISION wired through
patch_revisor. Replaces the
pre-v0.6 free-text-rationale path. Combines review findings +
(when present) verify_numbers findings, serialises them as a
JSON block in the work order'sfocus, dispatches
patch_revisor, callsmerge_patch_file. fully_applied →
COMPLETED; missing patch file or failed edits → REJECTED with
the first 3 failures named inlast_error. Edge case:
MAJOR_REVISION with no actionable findings short-circuits to
COMPLETED without dispatching (avoids wasted spend). - Self-attack critical findings wired through
patch_revisor.
Eliminates the pre-v0.6 parallel-revisor write race. Top-3
critical findings are batched into ONE patch_revisor call.
Patch failures at this phase are advisory (logged, do NOT
REJECT) — the downstream review phase catches what remains. verify_numbersauto-patch loop (proactive gate). Pre-v0.6
the gate was defensive: critical mismatch → REJECTED. v0.6
closes the detect → patch → re-detect loop: critical mismatch →
dispatchpatch_revisorwith the mismatch findings → re-run
verify_numberson the patched draft → REJECTED only if the
second pass still has criticals. Bounded by
_VERIFY_NUMBERS_AUTO_PATCH_BUDGET = 1(single attempt) so a
drafter that consistently disagrees with the source JSON
doesn't loop. The persistednumber_verification.json
reflects the post-patch state.- Iterative-phase guard against
paper_drafterre-dispatch.
Two-layer defence:- Soft: strategist's system prompt instructs it to use
section_writer(scoped to asection:<name>focus) on
iterations 2+, neverpaper_drafter. Validated on the live
run — strategist usedsection_writer3× in iter 2. - Hard:
_dispatchdropspaper_drafterwork orders when
self._iteration >= 2, logging a warning. Catches the
strategist if it ignores the soft instruction. iteration 0
(initial) and iteration 1 (first iterative) still allow
paper_drafterlegitimately.
- Soft: strategist's system prompt instructs it to use
patch_revisorloads three skills.writing/scoped-revision
(new — defines the patch-file shape with worked examples for
verify_numbers and self_attack findings),
writing/cite-numbers-by-source(v0.5 — same discipline as
the drafter),writing/personal-style,reasoning/anti-slop.- Five architecture invariants pinned. Each step has a
primary regression test;tests/pipeline/integration/test_v0_6_invariants.py
documents all five in one place and adds cross-step
assertions (legacyrevisornever dispatched by v0.6 runner
paths; both source types reachpatch_revisor's focus when
review + verify_numbers both have findings; merger
scope-enforcement holds across dispatch sites).
Test counts
- Mocked suite: 521 passed (was 422 in v0.5.0; +99 in v0.6).
- New test modules:
test_findings.py,test_patch_merger.py,
test_patch_revision_wiring.py,test_self_attack_patch_wiring.py,
test_verify_numbers_auto_patch.py,test_iterative_phase_guard.py,
test_v0_6_invariants.py.
Known follow-ups (deferred to v0.6.1)
- The legacy
revisorspecialist is no longer dispatched by v0.6
runner code paths, but the strategist may still freely dispatch
it from_run_iterative_phase. Surfaced by the 2026-05-22 live
run (one revisor call in iterative phase). Candidate fix:
extend the iterative-phase guard to also droprevisoron
iterations 2+, OR update the strategist prompt to discourage
it explicitly.
v0.5.0
Anti-hallucination & methodology-aware pipeline. Full design
record at docs/V0.5_PLAN.md. Motivated by v0.4.5 live tests on
papers a6182f08, cbe8048f, eea5379b, and validated end-to-end
against fresh live runs on 2026-05-20 (234a11ea, fd6bf64d) and
2026-05-21 (525fa03c) — see docs/V0.5_LIVE_VALIDATION.md.
Lane A — Pipeline
-
Programmatic anti-hallucination gate before review (new file
src/core/pipeline/verify_numbers.py, 357 lines). Scans every number
in\begin{tabular}blocks ofpaper_draft.texand matches each
against the flat numeric values fromsummary_statistics.json,
estimation_results.json,robustness_results.json, and
figure_spec.json. Tolerance 0.5% relative; integers ≥10 must be
exact; signs must match. Critical mismatches (relative error >10%
vs the closest source value) → statusREJECTEDand reviewers
never spawn. Persistsnumber_verification.jsonat workspace root
on every run. Live-test papera6182f08's "log realized variance
falls by 0.41 ($t=-3.9$ )" hallucination was caught by
technical_revieweronly after 6 reviewers had run; this gate
catches it deterministically, at $0, before any reviewer spends a
token. Graceful skip when no source JSON files are present (warn +
pass), so papers from before the analyst contract was tightened
don't regress. -
Methodology-aware phase routing.
PipelineRunner.__init__now
acceptsmethodology: str = "empirical", propagated from
papers.methodologythrough_run_pipelineandresume_paperin
the API. Formethodology == "theoretical",
_reviewers_for_methodology()dropsdata_reviewerfrom the
6-reviewer panel and_run_replication_phase()early-returns.
Live-test papercbe8048fburned ~$0.34 on adata_reviewerstub
over an empty contract plus ~$0.43 on a replication packager with
no replication artifacts — both wasted, both gone in v0.5. -
New status
PaperStatus.REJECTED, distinct fromFAILED.
FAILEDis reserved for crashes;REJECTEDmeans the pipeline
ran successfully and the quality gate (verify_numbers,
HARD_REJECT, MECHANISM_FAIL) returned a negative verdict.
Resumable: transitions back to IDEA / IN_PROGRESS / REVIEW /
REVISION / CANCELLED._run_revision_phase's HARD_REJECT and
MECHANISM_FAIL branches updated to emit REJECTED instead of
FAILED. New IN_PROGRESS → REJECTED transition for the
verify_numbers gate path. -
BudgetExceededError→PAUSED, resumable. Newexcept BudgetExceededErrorbranch inPipelineRunner.run(), alongside
the existingCircuitBreakerErrorhandler. Persists state, logs a
paused_budgetevent with{spent, cap}, returns a structured
{status: "paused", reason: "budget_exhausted", ...}payload.
The operator raises--max-costand POSTs
/api/papers/{id}/resume; existing resume-from-disk logic picks
up at the first incomplete phase. Previously a budget exhaustion
was indistinguishable from a crash. -
PAUSEDandREJECTEDrows now persistlast_erroron the
paperstable. Pre-v0.5, only FAILED and CANCELLED rows carried
the error/reason; PAUSED and REJECTED dropped it at the SQL layer,
leaving the dashboard withlast_error=NULLand no way to render
the budget breakdown, circuit-breaker specialist, or review-gate
rationale._update_statusnow treats PAUSED and REJECTED the
same way as FAILED and CANCELLED for error preservation.
Discovered while writing the v0.5 budget-pause regression test. -
POST /api/papers/{id}/resumeacceptsmax_cost_usdin the
request body. Pre-v0.5 the endpoint silently ignored the body and
read the cap from the DB row, so raising the cap on a budget-paused
paper required a manualUPDATE papers SET max_cost_usd = ...
beforehand (the workaround surfaced during the 2026-05-20 live
validation). The endpoint now accepts an optionalResumeRequest
body; a positivemax_cost_usdis validated and persisted on the
row atomically with the status reset, then passed to the runner.
Zero or negative values 400. Calls without a body preserve the
pre-v0.5 behaviour (use the existing row value). -
paper_drafter,section_writer,abstract_writer, and
revisorload a newwriting/cite-numbers-by-sourceskill that
teaches the cite-by-JSON-key discipline: every numeric value in
the paper must trace to a value insummary_statistics.json,
estimation_results.json,robustness_results.json, or
figure_spec.json. HTML-comment markers (<!-- src: file#key -->)
letverify_numbersmismatches name the exact source path the
drafter should have used. Reduces hallucination rate in the first
place; complements the post-hoc gate. Includes the "empty sidecar
→ no quantitative claims" rule so the design-without-estimates
pathway is explicit. -
Test-mock fix:
MockLLMBackend._detect_specialistnow matches
on the canonicalYou are the <Name> specialistrole line in the
system prompt rather than searching for any specialist name
substring. The old heuristic silently misrouted calls whenever a
skill referenced another specialist by name (e.g. the new
writing/cite-numbers-by-sourcementions "econometrics
specialist" → paper_drafter calls were routed to the econometrics
output → paper_draft.tex was never produced). Now matches one
occurrence per prompt with no skill-content interference. -
Machine-readable JSON sidecar contract for verify_numbers.
Pre-v0.5 every specialist was told to write EXACTLY ONE file, so
even when a skill described a JSON sidecar (e.g.data/figure-spec),
the system prompt overrode it and the JSON never appeared. The
2026-05-20 live runs confirmed this empirically: both papers wrote
number_verification.jsonwithskipped_reason="no source JSON files found"— the gate was effectively a no-op. v0.5 adds a
SPECIALIST_SIDECAR_ARTIFACTSregistry, asidecar_artifactsfield
onWorkOrder(auto-populated by_inject_context), and a
multi-file "Required Output" prompt block that lists every required
file with its role + JSON validity rules.data_analystnow emits
summary_statistics.jsonandfigure_spec.json;
econometrics_specialistnow emitsestimation_results.json
(with optionalrobustness_results.json). Two new schema skill
files (data/summary-statistics-schema,
econometrics/estimation-results-schema) teach the JSON shapes and
the "write{}instead of omitting when data was unavailable"
rule that distinguishes "honest empty" from "missing" for the gate.
v0.4.5
Bug pack rolling up findings from the v0.4.4 live test (paper eea5379b)
that completed end-to-end on a fresh pip install e2er. The pipeline
itself works; these are correctness + clarity fixes around it.
Lane C — Data
- Fix nested workspace path on
--save-to(Lane C, replication
correctness). The data_analyst subprocess runs with cwd at the paper's
workspace dir;_resolve_workspacethen resolved the relative default
workspace_root="workspaces"against THAT cwd, so the CSV landed at
workspaces/<id>/workspaces/<id>/data/. The model worked around this
by emitting a_candidate_csv_pathsfallback in estimation.py — a
prompt-engineered band-aid for a pipeline bug. Fix:_resolve_workspace
now prefers$E2ER_WORKSPACE_ROOT(claude_code injects the absolute
path) over the relative settings default. - Inject absolute workspace_root into the claude_code subprocess env
(E2ER_WORKSPACE_ROOT) and use an absolute path as the subprocess cwd.
Without both, the relativeworkspacesstring can re-resolve at any
nested call site.
Lane A — Pipeline
- Accept
pipeline_modeas an alias formodeinCreatePaperRequest.
e2er run --mode single_passreached the API aspipeline_mode,
which Pydantic silently dropped → server fell back to the default
"iterative"→ first-run log line falsely reported the wrong mode.
Also fixsrc/cli_run.pyto send the canonicalmodefield. - Reword the first-run cap log line. "override=True" read like the
server overrode the user's cap; it actually meant the user acknowledged
the unproven (model, methodology, mode) tuple so the $1 floor was
lifted to their requested cap. New format spells it out:
cap=$20.00 (user_ack_unproven=True, first_run_floor=$1.00).
Cross-lane
- Label CLI-backend costs as estimates. Anyone running on
claude_code/codex_cli/gemini_clisees Sonnet-rate synthetic
dollars even though the Max plan absorbs the actual cost. Startup log
now warns once when a flat-rate backend is selected; the/api/papers/<id>
usage payload carries acost_is_estimateflag so dashboards can
render the number with the right hedge. e2er migrateworks on pip-installed wheels. Old code pointed at
scripts/migrate.pywhich is excluded from the wheel. Moved to
src/db/migrate.py(importable, ships in the wheel), reads SQL files
viaimportlib.resources("sql")with a dev-checkout fallback.- Drop the stale
_SCRIPTS_DIRPATH entry on pip installs. Guarded
with.exists()so the resolved PATH doesn't carry a non-existent
site-packages/scripts/directory that confusedwhich-style probes
inside the claude_code sandbox.