Forgecell improves the process that creates software. The coding harness changes the codebase for a ticket; the meta harness uses Molecule ledgers to propose a better Formula for future tickets. These are different jobs, with the same four primitives.
- Observe. Run a Molecule and preserve its issue, exact Formula snapshot, Atom outcomes, commands and limitations in the ledger. An error or blocked run can be useful evidence.
- Reason. Identify a recurring workflow problem or a narrowly supported lesson. Separate observations from the harness's interpretation. A successful exit is not proof of quality.
- Propose. Explain one small process change, why the ledger supports trying it, the expected benefit, and what would count against it. Attach the complete Formula diff.
- Review. A human approves or dismisses the proposal. Approval permits a recipe change; it does not validate the hypothesis. Keep the decision and both recipe versions.
- Evaluate. Run another suitable ticket using the approved recipe. Compare the intended behavior and actual outcome, including regressions, skipped checks and environmental differences.
- Iterate. Retain a useful lesson, revise an uncertain one, or propose reversing a harmful change. Each further Formula mutation needs a human decision. Insufficient evidence is a valid finding; the Lab should not manufacture a change to appear productive.
“Better” depends on the ticket: correctness, useful validation, time, cost, or fewer failed attempts can conflict. State the desired outcome and tradeoffs. Do not claim an optimum or causality from a single run. Comparing the same ticket twice may confirm that instructions were followed without showing that they generalize.
Every new proposal records:
- Process change: what the next Molecule will do differently, in plain language.
- Evidence and reasoning: ledger IDs, observed behavior, uncertainty and scope.
- Expected impact: a hypothesis, explicitly unverified.
- Evaluation: what to inspect on the next comparable run and signs of regression.
- Exact change: original and proposed YAML, hashes, and diff.
- Human decision: pending, approved or dismissed, with a review timestamp when decided.
The CLI displays the explanation before the YAML. Earlier suggestions remain readable; missing evaluation fields are shown as missing, not retroactively invented.
A useful example: “When sandbox IPC restrictions block the usual test runner, try the verified direct Node invocation and report both results separately.” This changes validation behavior. “Fix Unicode decoding in invoke.ts” belongs to the ticket, not to a Formula lesson. Renaming the Formula or adding a comment is not a process improvement.
forgecell learn <molecule-id...> reads finished ticket ledgers from the same approved
Formula snapshot and invokes an explicitly bound learn Atom. The meta harness returns
summary, rationale, expectedImpact, evaluation, and complete yaml as JSON.
Forgecell saves a pending proposal. forgecell suggestion <id> --approve applies it only
if the current Formula still matches its original snapshot; --dismiss preserves the recipe.
The runtime rejects proposals that only change metadata or unused fields. This is a structural check, not proof that prose describes a useful process improvement. A person must still reject ticket-specific code instructions, unsupported claims and unsafe changes. The example adapter instructs the meta harness to preserve command bindings and human gates; this is a prompt instruction, not an enforced restriction on the proposed diff. arbitrary BYO adapters remain responsible for their own sandbox and tool permissions.
Next-run evaluation is currently a manual review of ledgers. Forgecell does not yet calculate comparative results, automatically reverse a mutation, or post findings to GitHub.
The development CLI keeps each checkout's Lab under ~/.forgecell/labs/. Its local
files are the execution record: approved Formulas, Molecule ledgers, and suggestions
with human decisions. doctor --json reports the exact Lab directory. Keep it backed up
deliberately; outputs may contain repository information. A ticket records the requested
work. Curated Markdown findings in docs/dogfood/ document Forgecell's own research
without publishing raw private output.
GitHub issue summaries can be added deliberately; the Lab currently posts no comments.
Start with the first approved lesson and follow-up evidence.
The development Go CLI supports the same saved suggestion format as the TypeScript reference:
forgecell learn mol-123
forgecell suggestion suggestion-ID
forgecell suggestion suggestion-ID --approve
# Or dismiss without changing the recipe:
forgecell suggestion suggestion-ID --dismissAdd --json for an agent-readable result. learn never accepts an approval flag:
it saves a pending proposal with the process change, ledger evidence, reasoning,
expected impact and a comparable-run evaluation plan. Approval is a separate human
decision and does not establish that the change improved anything.
The Go onboarding proposal does not silently configure a meta harness. An explicit
learn Atom command is required. Current-source functionality offers proposal-only
opt-in; these flags are not included in published v0.2.0-preview.2:
forgecell init --meta-harness codex --json
# Or a BYO argv array; capability and containment remain unknown:
forgecell init --meta-command '["/absolute/meta-command","arg"]' --json
forgecell init --lab LAB_DIR --review PROPOSAL_ID
# After human review of exact YAML and binding limits:
forgecell init --lab LAB_DIR --approve PROPOSAL_ID
# Separate invocation; activation does not run learning:
forgecell learn MOLECULE_ID --lab LAB_DIRNative choices are codex, claude-code and cursor. Native opt-in requires observed
installation/authentication readiness and supported meta capability. Unsupported or
unknown support saves no proposal and supplies BYO configuration guidance. BYO commands
are never probed. Default init binds coding only. Opt-in proposes exactly one trailing
learn Atom with a separate command and bounded 900000 ms timeout. Saved review displays
argv, permission limits, evidence inputs and exact before/after YAML.
Re-init preserves pending proposals, interrupted approval recovery, approved coding and
learning bindings, learned instructions and custom Atom order. Opt-in does not migrate
an existing learn binding; coding rebinding preserves it.
Eligible inputs are finished ticket Molecule ledgers from the same intact, currently approved Formula snapshot, including Atom outcomes, verification evidence and limitations. Pending attempts, mixed snapshots, damaged evidence and uncertain publication are rejected before invoking the meta command. Learning saves only a pending suggestion; Formula mutation needs a separate human approval of that suggestion.
Native adapters request provider controls: Codex read-only sandbox with approval policy never, Claude built-in tools disabled and strict empty MCP configuration, Cursor sandbox enabled with ask mode. Installed enforcement remains unverified. Codex meta lacks ticket analysis's additional tool/MCP/plugin isolation; Claude lacks analysis safe-mode controls; Cursor MCP/plugin isolation is unverified, so ask mode is not verified read-only containment. Meta retains the supplied working directory rather than analysis's temporary directory. No-edit, supplied-evidence-only and binding/gate preservation prompts are declarations, not enforced diff limits. Arbitrary custom commands retain the permissions of the local process that invokes them. A prompt alone is not a sandbox.
Review recomputes the diff from the exact hashed YAML, rather than trusting stored diff text. A shared Formula-write lock prevents competing approvals. Approval refuses stale recipe bytes and persists an intent before replacement so an interrupted approval can finish its record. Dismissal does not replace the Formula. Learning rejects mixed snapshots, damaged evidence, and cosmetic changes to unused fields.
Nothing used to start learning, so the same failure could repeat until a person noticed.
After run and loop, Forgecell evaluates cheap, deterministic rules over the Lab's
recent finished ledgers. A rule fires when the newest finished Molecules of the current
approved Formula snapshot exhibit the same failure class the threshold number of times
in a row. Each class is a pure function of the finished ledger; no failure-class field
is stored:
process-timed-out: the Molecule's final coding harness attempt hit its timeout and verification did not pass afterwards. A timeout that a later attempt or a passing verification superseded does not count, and neither does a timeout that only an analysis or review attempt mentions. Ledgers from before attempt results were recorded are simply not counted.analysis-policy-violation: intake stopped because the unchanged policy validator rejected the analysis, including the per-class corrective retry stops.protected-regression-failed: verification ran the protected view and its required checks did not pass.
Intake and recovery also guard against two wasted runs; neither is a failure class
above. An analysis whose candidate plan fails Plan.Validate (for example a check with
category vet) is rejected inside the analysis loop with the policy-violation class
plan-validation and gets one corrective retry within the same three-retry budget; a
second invalid plan stops intake. Supplied check inputs, checks, setup and the Formula
baseline are validated before any analysis, so their errors stop intake without a
retry. An amended coding attempt that completes with the tree and artifacts of the
parent revision's paused state stops at the review gate with outcome reason
noChangeAfterAmendment: verification is not run, delivery and learning refuse it, and
the next step is a further amendment with new work or a checks-only amendment that
verifies the retained tree.
Intake also allows up to two supplemental evidence passes, so the analysis invocation
bound is the initial analysis, three corrective retries and two post-supplement analyses.
An evidence request may name path#Lstart-end (1-based, inclusive). The excerpt holds
exactly those lines of the base-commit blob, at most 64000 bytes and 1500 lines; its
evidence path names the lines actually returned and its SHA-256 covers the excerpt bytes.
A range past the end of the file or past either bound is clamped, and the clamp is
recorded in the collection outcome reason. Ranges get the same sensitive, generated and
binary refusals as whole files, and count toward the 16 named-file and 1000000-byte
bounds by excerpt size. A whole-file request for a .go file over 64000 bytes returns an
outline of its top-level func, type, const and var declarations with line ranges,
recorded as path#outline with the reason outline collected; file is N bytes; request line ranges (path#Lstart-end) for content. An oversized non-Go file is refused with the
same hint. A request after the second pass stops intake with Supplemental evidence passes exhausted; unmet requests: followed by each request and its latest outcome reason.
The streak is the newest run of finished Molecules of the current Formula snapshot,
ordered by finish time and then Molecule id, among the 128 most recently modified
ledgers. Every Molecule in it exhibits the class and is one that learn itself would
accept. What each kind of Molecule does to a streak:
- Counts: a finished Molecule that exhibits the class. The newest
thresholdof them become the request's evidence. - Ends it: a finished Molecule of the same snapshot that does not exhibit the class
(a success, a different class, a plain failure), or that
learnwould refuse (not an approved ticket Molecule, a damaged Formula snapshot, evidence it cannot classify). - Invisible, neither counts nor ends it: unfinished Molecules and Molecules waiting at a gate, Molecules of another Formula or snapshot, non-ticket Molecules, and ledgers that cannot be read. An unreadable ledger is skipped and reported in one diagnostic line instead of stopping the evaluation, so a streak can span a Molecule the Lab could not read.
The threshold is an optional Formula setting and defaults to 3:
learning:
recurrenceThreshold: 3 # integer from 1 to 50Formulas without it keep their exact bytes and SHA-256. The threshold in force is the one in the current approved Formula.
When a rule fires and a learn Atom is bound, Forgecell records one durable, deduplicated
pending learn request under learn-requests/ in the Lab, holding the class, the
triggering Molecule ids and the Formula SHA-256. When no learn Atom is bound, it only
prints one line that names the class and the Molecules and recommends
forgecell init --meta-harness. No model is called automatically. Evaluation, run,
loop and doctor never start the learn command.
doctor (also in --json as learnRequests), the end of run and the loop report show
pending requests and the suggestions saved from them, so they are not missed. Exit codes are
unchanged.
forgecell learn --pendingruns each pending request through the normal learn path over exactly that request's
Molecules. A success saves a pending suggestion and records its id on the request; a failed
or timed-out run is reported with a bounded, redacted message, the request stays pending and
the exit status is nonzero. learn --pending cannot be combined with Molecule ids and never
approves or dismisses anything. Approval, stale-Formula protection and every other
learn guard are unchanged. Running it automatically from the loop is left to a later slice.
While the Formula SHA-256 is unchanged, a class never fires again while a request or a suggestion from it is pending. After a suggestion is dismissed, or a learn run fails and saves nothing, the class fires again only after the threshold number of new occurrences since that event; a failed request is superseded by the fresh one, and the old request is retired first so a crash never leaves two pending requests for one class. A Formula change resets the counters and retires requests tied to the old Formula.
learn --pending writes a durable intent (with a random nonce) on the request before it
starts the learn command. If the process dies before the outcome is recorded, the next
learn --pending finds the intent, adopts a suggestion that run already saved for exactly
the same Molecules and Formula (no second model call), and only calls the learn command
again when there is none.
A request, suggestion or ledger that cannot be read is skipped and reported once; it never
stops the other failure classes from being evaluated. A class whose own suggestion cannot
be read does not fire until that is fixed. learn-requests/ stays bounded: the oldest
terminal requests (stale, superseded, or replaced by a newer one) beyond 20 and run locks
of finished requests are removed. Failure text from a learn run and any other untrusted
text is shown on one line with terminal escapes, control, bidirectional and other format
characters removed, and is cut by characters, not bytes.
Learning requires the current Lab's completed exact-byte Formula approval before the meta harness runs. Each suggestion retains that originating approval identity and both exact YAML hashes. Reactivating identical bytes under a different human decision invalidates earlier suggestions; editing YAML or changing approval during generation prevents a suggestion from being saved.
Approval persists its intent, updates the exact recipe and current Lab approval, and completes the saved decision durably under the shared Formula write lock. Interrupted approvals block execution and can be resumed with the same reviewed suggestion after inspecting stopped writers and conflicting state. Dismissal cannot undo an applying approval. A newer activation cannot be overwritten by retrying an older decision.
Historical suggestions remain inspection evidence. Comparing their variants with
evaluate requires a fresh --approve DIGEST covering the frozen plan, both exact
recipes, source and output destination. The preview performs no setup, checks,
model calls or output-directory writes. The report retains the new decision's
provenance. Rechecking retained output does not approve new model execution.
Formula approval remains separate from ticket scope and publication review.
The single-task evaluate above compares one baseline and one candidate recipe on one
frozen task. evaluate --cohort extends the same discipline to a reviewed cohort:
two Formula variants (for example identical apart from model) run over N frozen task
plans with K repetitions each.
forgecell evaluate --cohort COHORT.json --source REPO --out DIR # prints the digest
forgecell evaluate --cohort COHORT.json --source REPO --out DIR --approve DIGEST
forgecell evaluate --cohort COHORT.json --source REPO --out DIR --approve DIGEST --keep-workspacesThe cohort file is strict JSON, at most 64 KiB, with unknown fields rejected:
schemaVersion: 1; a hypothesis (≤ 2000 characters); a primaryOutcome of
hiddenAcceptancePassRate, requiredChecksPassRate or costPerAcceptedOutcome;
exactly two variants [{name, formula, formulaSha256}] whose name matches
^[a-z][a-z0-9-]{0,31}$, whose formula is a cohort-relative Formula file, and whose
pinned bytes differ (a cohort that pins the same Formula for both variants is refused);
1–50 tasks [{name, plan, planSha256}], each plan an existing evaluation Plan file;
repetitions 1–10; and an optional stop {maxAttempts?, maxCostUSD?}. Every
referenced file is pinned by SHA-256. On both fresh and resumed runs every input copied
under <out>/inputs/ is re-hashed against its pinned hash immediately before each
attempt and the attempt executes from those verified bytes; a mismatch refuses the run
and writes status: aborted rather than running a mutated copy.
One activation digest covers the cohort bytes, both Formula files and every plan file,
plus source and destination, reusing the existing exact-approval semantics. Nothing is
created or executed before approval; without --approve the command prints the digest
and the exact approve command.
For each task, for each repetition, variant A then variant B run interleaved (A first),
sequentially, each in its own isolated per-attempt Lab under
<out>/attempts/<task>/<variant>/rep-<n>/. A failed or timed-out attempt is recorded,
never silently retried, and the run continues. The one exception is the Molecule's own
recovery from a transient provider error (below), which applies to cohort attempts too.
An attempt cancelled by SIGINT or SIGTERM
is discarded rather than saved, so resume re-runs that coordinate instead of skipping it,
and the run is recorded as status: aborted. The stop rule is checked after each
attempt; maxCostUSD counts only reported cost (list-price estimates included and
labelled), never triggers when no cost is reported, and its stop reason names the cost
basis. A stop that leaves the two variants with unequal attempt counts records an
imbalance note, because rates over different n are not directly comparable.
--parallel N (1–4, default 1) runs up to N attempts at a time; the default runs them one
at a time exactly as above. Coordinates still start in the same order, so A and B of a
task and repetition tend to run together under the same machine load, and recorded
coordinates are still skipped on resume. The report keeps attempts in coordinate order,
so report.json and report.md do not depend on completion order. --parallel is not
part of the activation digest, so changing it never invalidates an approval or a resume.
When a stop rule fires, no new attempt starts and the in-flight ones finish and are
recorded; maxAttempts counts recorded and in-flight attempts before every coordinate,
so it is never exceeded, even on a resume with gaps. An abort error, a report-save error or
SIGINT/SIGTERM cancels the in-flight attempts, which are discarded unsaved (also when the
cancellation lands during the diff capture) so resume re-runs them. Each attempt.json
run with N above 1 records parallel: N, and whenever any recorded attempt ran that way,
including after a resume, the report notes that elapsed and harness durations include
contention from concurrent attempts.
Every Lab and evaluation go invocation shares one build and test cache: the Molecule's
candidate and protected views (Formula baseline checks included), evaluation verify(),
setup and preflight, base-flake re-runs and replay. The absolute GOCACHE and GOMODCACHE
are resolved once per process with go env under the caller's environment (with
GOTOOLCHAIN=local), so no invocation falls back to a cache under its private per-run
HOME, also when GOCACHE is unset in the caller's environment. Each scrubbed check
environment also sets GOPROXY=off, GOTOOLCHAIN=local and -trimpath in GOFLAGS. No
-mod is added: without one Go already uses readonly mode, or vendor mode for a module with
a vendor directory, and a forwarded -mod (-mod=vendor, -mod=mod) is kept. A
cohort attempt's memory-capped -p is merged into the same GOFLAGS afterwards. A reviewed
policy that forwards GOPROXY or GOTOOLCHAIN keeps the caller's value, and the check
record names the override. A module missing from the caller's module cache now fails a check
instead of being downloaded; when a failed check's output shows module lookup disabled by GOPROXY=off or a GOTOOLCHAIN=local toolchain error, its record carries a note naming
Forgecell's cache settings: warm the module cache (go mod download) or forward GOPROXY
rather than reading it as a candidate defect. Evaluation commands no longer inherit the caller's environment:
they get a private HOME, the caller's PATH, LANG and TMPDIR, the cache settings and
FORGECELL_EVAL_WORKSPACE.
Each verification checkout lives at a unique path. Without -trimpath the test binary
embeds that path and every package misses the cache; with it, Go serves (cached) results
for every package whose code, dependencies, and read files and environment are unchanged.
Go keys a test only on the environment variables and files it actually reads, so:
- the Molecule's views use one stable
TMPDIRper Lab,<temp root>/forgecell-lab-<first 16 hex of sha256(resolved Lab path)>(0700, shared by concurrent runs, whose files stay apart in the unique subdirectories Go creates under it), becauset.TempDir()readsTMPDIR. The temp root, resolved, is$FORGECELL_LAB_TMP_ROOT, else$TMPDIR, each only when set, usable and not inside your home, else the per-user default: the per-user temp dir on macOS (getconf DARWIN_USER_TEMP_DIR; if that fails, the system temp dir with a recorded note) and/tmpon Linux. Following the environment keeps a disk-backed$TMPDIRand keeps tests and nested runs inside a check view under that view'sTMPDIR, so they never leak directories into your real temp dir. It lies outside the Lab, which usually sits under your home, so a test that walks up from its temp dir and stops at the privateHOMEnever reaches your real home's.claudeor.codexconfig; a root inside your home is passed over (with a note), and a per-user default inside it is refused outright. Moving or symlinking a Lab elsewhere gives it a new path and a cold test cache. Go'sGOTMPDIRis its stablegotmpsubdirectory; each verification first removes the user's entries there older than 24h (old children ofgotmpincluded), andforgecell doctor --cleanlists and removes them too, with the same confirmation. Pruning touches only a directory that Forgecell created: creating it writes a.forgecell-check-tmpmarker and alabfile naming its Lab, and reusing your own directory at that path makes it 0700 and writes either again if it went missing; a directory without the marker is never pruned, a symbolic link, non-directory or another user's directory at the path is refused (the views then fall back to a privateTMPDIR, and the Molecule records a note, as they do when the directory vanishes mid-run), anddoctor --cleanalso requires the Lab to have aledgers/directory. The earlier location,<lab>/tmp, is no longer used; when it carries the markerdoctor --cleanlists it and removes it only after confirmation, never automatically. A markedforgecell-lab-*directory older than 24h whoselabfile names a path that no longer exists, or that has none, is an orphan: the start sweep and each verification remove it, anddoctor --cleanlists and removes orphans under both the per-user default root and the current one; HOMEstays private per view. A test that reads it (os.UserHomeDir) is a known cache miss; few do.
Hidden acceptance always runs fresh: every independent-acceptance check gets -count=1 in
GOFLAGS whatever its argv runs, the generated acceptance.sh passes -count=1 too, and
base-flake re-runs already do. When the captured change touches a path outside the root of
any go.mod module in the candidate tree (for example docs/), which that module's cache
cannot see, every view of that verification runs with -count=1. With several modules (a
root go.mod plus nested ones, or go.work modules) a change outside any one of them is
enough. Regression go test and go vet checks, candidate and
protected, may otherwise hit the cache. Each go check record states its cache state, derived
from the environment it actually ran with: cache: enabled, cache: disabled (fresh)
(-count in GOFLAGS or argv), cache: disabled (fresh: non-module change) or
disabled (go cache unavailable) without a go toolchain; a non-go check such as gofmt or
a shell script states none, even when its GOFLAGS carries -count. The ledger's verification summary reports per view how many go test packages
were served (cached) versus run, read from the ok <pkg> (cached) summary lines.
-trimpath differs from CI. CI runs without -trimpath, so a test that reads source
paths (runtime.Caller, file names in stack traces) sees
module-relative paths in Forgecell's checks and absolute paths in CI. A reviewed policy can
opt out by forwarding GOFLAGS containing -trimpath=false, which the merge keeps; that
check then misses the cache across checkouts.
Safety note. Go's cache does not track subprocess behaviour or files outside the module. A test whose result depends on external tools or state can be served a stale cached result. The full-suite run in GitHub CI stays the final gate.
By default each attempt's Lab, workspace and verification trees are deleted once its
record and verification observations are saved, to bound disk growth. Before deletion the
run writes <attempt>/changes.diff (the unified diff from the base commit to the captured
tree, capped at 5 MiB with truncation marked, its sha256 recorded in the attempt record)
and <attempt>/changes.json (the changed paths). The diff is captured with a fixed,
configuration-independent git invocation so it depends only on the pinned bytes; a capture
that fails is recorded as the attempt's changesError and never aborts the run. Every
worktree an attempt allocates is registered only in the attempt's own throwaway source (see
Replay tasks below; --source is only read and never gains a worktree), so deletion simply
removes the attempt's Lab, verification and source trees together; a cleanup failure
is recorded as the attempt's cleanupError. Both error kinds are summarized in the report
rather than swallowed. Report and any later recheck read the retained files, never the
deleted tree. --keep-workspaces retains the trees.
The side-by-side report is written to <out>/report.json and <out>/report.md. For
each variant it reports attempts and completions; hidden-acceptance pass count and rate;
the required-checks pass rate; scope violations, scope-change and amendment counts, and
timeouts; median and p90 of elapsed and harness time; reported cost total and per
accepted outcome with its basis and N of M coverage; effective models; and review-Atom
findings by severity when present. The required-checks pass is tri-state: it is reported
only when the plan actually ran a non-acceptance check, and is null ("n/a") when the
plan has none. RequiredChecksPass and its rate are nullable in report.json; aggregates
and per-task cells exclude n/a attempts and state their own measurable n. When the
primary outcome is requiredChecksPassRate and every attempt is n/a, the report states
the primary outcome is not measurable. Cost per accepted outcome divides the reported-cost
total only by the accepted attempts among the cost-reporting ones, and always prints its
N of M coverage. A per-task × variant table lists the outcomes. Counts and rates only,
with no significance claims, and every rate states its n (tasks and repetitions).
An attempt whose harness was served a model other than its pin (usage.modelMismatch)
is recorded with invalid: "model-mismatch" and is never correct. Invalid attempts stay in
the variant's attempt count, reported cost and cost coverage, because the money was spent,
but are excluded from completions, passes, failures, timeouts, scope and amendment counts,
required checks, every median and p90 and the cost per accepted outcome; each variant
reports invalid and invalidReasons, and the report adds one note naming how many were
excluded. When a variant's first attempt is invalid this way the run stops (variant <name> was not served its pinned model (<pinned> → <served>)), because the comparison
cannot be made; a mismatch on a later attempt is recorded and the run continues.
Transient provider errors. When the Claude CLI ends a coding attempt with
terminal_reason: "api_error" and a network-class message (ENOTFOUND, ECONNRESET,
ECONNREFUSED, ETIMEDOUT, EAI_AGAIN, socket hang up, Connection error.,
Request timed out., Can't reach the API server, went to sleep mid-response), an
overloaded provider, or an error whose own status is HTTP 502, 503, 504 or 529 (the
API Error: NNN prefix or a structured api_error_status), the harness attempt records
classification: "transient-provider-error" and the provider message, not a bare
exit 1. A 4xx status, or an invalid_request_error, authentication_error or
permission_error, is never transient, whatever numbers or words its message contains.
The Molecule then resumes automatically, at most twice per coding stage: it waits 30 s
before the first provider check and 120 s before each later one, checks that the provider
host resolves (the host of ANTHROPIC_BASE_URL, else api.anthropic.com; a host that does
not resolve is noted and the next wait follows, without using a resume, up to four checks
per stage), checks that the Formula approval is unchanged (a change stops the stage), and
starts a new coding attempt on the same approved plan digest from the retained workspace
tree, with the added instruction The previous attempt was interrupted by a provider network error; the workspace contains its partial work. Review it and complete every acceptance item. If the retained work already satisfies every acceptance item, report outcome completed, not no-change. No re-approval is needed. Each resumed attempt carries
resumed: "resumed after transient-provider-error (k of 2)", counting resumes that
started, and a ledger note, and every attempt keeps its own usage, so the reported cost sums
them. A transient attempt that wrote outside scope is not resumed. When the resumes or the
provider checks are used up, the Molecule fails with transient-provider-error retries exhausted after N automatic resumes and M provider checks; last error: ... and records the
stage's attempt IDs in transientExhausted. Model refusals, policy or permission errors,
timeouts, budget stops and a non-network api_error are never retried automatically. A
cohort attempt whose Molecule carries that marker on its last attempt is recorded with
invalid: "provider-unavailable": like model-mismatch it stays in the attempt count and
reported cost but is excluded from completions, passes, rates and medians, and unlike other
recorded failures it is resumable, so a resumed cohort run re-runs that coordinate. Any
other stop after a transient attempt (an interrupt during the wait, a Formula, extension or
capacity failure) is an ordinary failure.
Each attempt.json records costUsd from the reported usage (null when nothing reported a
cost), and, before the Lab is deleted, failedCheckOutput: for every failed Molecule
setup command (phase: setup) and check (phase: check), in the candidate view and its
protected run, the ID, exit code and the last 8 KiB of stdout and stderr, copied from the
verification history (already scrubbed of environment secret values) and passed through
the pattern-based credential redaction. The tails stay out of report.json, which a
resume must read. report.md has an ## Attempts section: one line per task, variant and
repetition giving the outcome (accepted, failed, incomplete or invalid:<reason>),
the served models, the cost and the elapsed time, followed by the last 20 stdout lines of
each failed command (a leading … marks a first line cut from a longer stream).
Hidden acceptance runs first. The evaluation lists the plan's acceptance check before
every other plan check in the readiness analysis (setup still runs before all checks, and
Formula baseline checks follow), so Molecule verification, which is unchanged — still
fail-fast and still all-required — always observes the hidden test before a regression
check can stop it, for example on a timing-sensitive test in the replayed historical
code. Each attempt records hiddenAcceptance: true or false from the evaluation's
independent verification, the same run that sets correct and runs every check (so
correct always implies hiddenAcceptance: true), or from the Molecule's candidate
verification when independent verification never ran; null when acceptance never ran
or did not complete (coding incomplete, setup failed, verification errored first, or the
check timed out, was interrupted or hit the output cap), and for an invalid attempt.
hiddenAcceptancePass counts attempts with hiddenAcceptance: true, independently of
every other check, and hiddenAcceptanceN counts the attempts where it ran; report.md
shows hidden-acceptance pass X of N-ran beside correct, which still requires
acceptance and every required check to pass. A resumed record written before the field
existed lacks the key; it is excluded from the hidden-acceptance rate and the report
notes it. When acceptance passed but a regression check failed, the attempt records
regressionFailedTests, the sorted, unique Go test names the failed checks' full
streams report (read before tailing: go test -json fail events with a test when the
stream carries them, otherwise --- FAIL: lines), and the ## Attempts section lists
them, so flakes in historical code are visible rather than read as the model's failure.
When a failed check's stored stream was itself cut, regressionFailedTestsTruncated is
set and the list is marked (output truncated).
Regression failures that are flaky at base are classified, so a test that fails in the
untouched historical code does not decide the comparison. The failed-test extraction also
records each test's package import path (the Package of a go test -json event, or the
FAIL\t<pkg> result line that follows a plain stream's --- FAIL: lines), and notes an
unattributed failure: a package failure no --- FAIL in that package explains, or any
panic: line, Go runtime abort (fatal error:, such as a deadlock) or signal: line (a
test binary killed by a signal), test timed out, [build failed] or [setup failed] (in
plain output or an event's Output), even beside a named failed test in the same package,
and a plain exit status N line with no --- FAIL before it in its package. After independent
verification, and before cleanup reclaims the per-attempt source, an attempt whose hidden
acceptance passed re-runs each failed regression test in a fresh worktree of that source at
the base commit (base only; no candidate change applied). The plan's setup commands run
there first, as in verification (each from its directory with its own timeout), under the
replay's scrubbed environment; a setup failure leaves every test unclassified. Each test then
runs exactly 3 times (no early stop): go test -count=1 -run '^<name>$' <pkg> (with
-json to read the result; each subtest level is anchored separately) from the check's
directory, under the same scrubbed environment, with a 5-minute timeout each. A run fails
only on the test's own fail event or a timeout after it started and before it passed; a
non-zero exit after its pass event, a missing test or package, a build failure or an
interruption is undecided. A test with at least one passing and one failing run is
flakyAtBase; one that failed all 3 is brokenAtBase (it may be the very bug the PR
fixes, so it excuses nothing); one that passed all 3 is neither; anything else is
unclassified. At most 10 tests are re-run per attempt, in package and test order; the
rest, and a test whose package is unknown, are unclassified. The attempt records
regressionFailures (package, test, flakyAtBase, brokenAtBase, baseFails,
basePasses, unclassified), keeping regressionFailedTests, and
correctExcludingBaseFlakes: correct, or, judged only from the evaluation's independent
verification (which runs every plan check without stopping at a failure), never from the
Molecule's fail-fast verification: that verification ran with every plan check observed and
complete, acceptance passed, every check that is not a go test check (go vet, gofmt and any
other) passed, and every failed go test check failed only on named tests, none
unattributed, each tagged flakyAtBase. correctExcludingBaseFlakes is always computed
from that independent verification. It runs whenever coding completed with a captured tree,
even when the Molecule's own fail-fast verification failed or errored on a check: the
attempt's verification keeps the refusal (so correct and the required checks keep their
strict, Molecule-gated meaning), and the full independent run is recorded as
evaluationVerification, which only hiddenAcceptance, the regression failure lists and
correctExcludingBaseFlakes read. It does not run when coding failed, was blocked, timed out
or asked for a scope change, or the attempt is invalid. When the independent verification
did not run or a check is unobserved, the flag is false: an unobserved check never counts as
passing. The Molecule's own failure evidence must agree as well: every failed check in
failedCheckOutput (candidate and protected views) must be a go test check that failed only
on named tests tagged flakyAtBase (not brokenAtBase or unclassified), with no
unattributed failure and a complete failure list, so a test the candidate broke
deterministically in the Molecule's run keeps the flag false even when the independent run
did not fail it; any other Molecule failure (a setup command, acceptance, go vet, gofmt or
another check) does too. Each variant reports a correctExcludingBaseFlakes count (records that predate the
field count only when correct), and the ## Attempts section marks each test
[flaky at base (k/3 failed)] or [broken at base] and lists the unclassified ones. The
re-runs add up to 10 × 3 × 5 minutes per attempt, the base worktree is removed even after a
cancellation, and a cancellation during the re-runs still discards the attempt.
The protected regression of an evaluation attempt exempts the task's own test files
(the plan's candidateTestFiles, which replay sets to the replayed PR's changed test files
and which hidden acceptance grades with their real versions): the evaluation sets them,
sorted, as the readiness plan's protectedExemptPaths, so
the protected view keeps the candidate's copies instead of restoring the base versions, and
a correct solution to a PR that changed an existing test is not failed by that old test.
Independent verification likewise applies the candidate's versions of those files, so its
regression checks see the same test sources as the protected run: the PR's own test files
are graded by hidden acceptance and run in their candidate versions everywhere else.
Every other existing test, and every other original check file, is still restored to base;
originalCheckFiles alone only means "keep the base version", so a hand-authored plan
without candidateTestFiles exempts nothing and files a recheck adds always keep base.
A plan with exemptions is never approved automatically (plan-has-protected-exemptions),
an amendment cannot add or change them, and normal ticket runs never set them.
An existing <out> whose recorded activation digest matches resumes and skips attempts
already recorded (a directory with a complete attempt record counts as done); a
mismatched digest is refused so a different cohort can never overwrite retained
evidence. Cohort --recheck — re-grading retained outputs without rerunning a model — is
deferred in this slice; the retained per-attempt evidence under <out>/attempts (the
record, verification observations and the captured changes.diff/changes.json) is
untouched and can be regraded by a follow-up, which reads the captured diff rather than
the deleted workspace tree. The single-task --inputs/--recheck mode is unchanged.
A cohort is only as useful as its tasks. evaluate replay builds frozen evaluation task
plans from our own merged PRs, so a model comparison runs against real history with
real, hidden acceptance tests:
forgecell evaluate replay --repo OWNER/REPO --pr 120 --pr 124 --source CHECKOUT --out NEW_DIRFor each PR it reads, read-only via gh api (the same pattern as
internal/intake/open_issues.go), the PR's merge_commit_sha, base.sha/base.ref,
body, changed files (/pulls/N/files, one page of at most 100) and the linked issue —
the first Closes #N/Fixes #N in the PR body, else the first #N in the title. The
issue body is read from /issues/N as the current body (taskSource: current-issue-body;
no body history in v1) and is treated as untrusted text.
baseCommit is computed from the merge commit's parents with local git in --source: a
single-parent (squash/rebase) merge uses merge^1; a two-parent merge uses the
merge-base of its two parents. The rule applied is recorded in approval.md.
To keep hidden acceptance material out of anything a reviewer might hand to a harness, each
eligible PR writes to two sibling directories. The human-facing <out>/tasks/pr-<N>/
holds only:
task.md, the issue title and body as untrusted text for human review.approval.md, a human-readable summary pinning every hidden artifact by SHA-256 and itshidden/pr-<N>/…path.
The machine-consumed inputs, which embed or are the hidden tests, live under
<out>/hidden/pr-<N>/ and are never shown to the harness during a run:
plan.json, an evaluationPlanwithallowedFilesthe PR's changed files (plus a renamed file's previous path),originalCheckFilesthe changed_test.gofiles,requireInitialFailure: true, andchecksofgo test ./...,go vet ./...and gofmt fromlabplus anacceptancecheck.issue.json, the linked issue as the plan's task (Plan.Task, consumed by the--cohortrunner).acceptance.sh(Plan.Acceptance), a self-contained script that copies the workspace to a throwaway tree, writes the changed test files (captured at the merge commit) into it and runs, in their packages, only the tests they define, so grading never rewrites tracked content and an unrelated (for example timing-sensitive) test in the same package can never decide acceptance. The command isgo test -buildvcs=false -run '^(<names>)$' <pkgs>, where the names are the top-levelTest/Fuzz/Examplefunctions defined in the hidden test files, sorted and each quoted withregexp.QuoteMeta. If that pattern would exceed 4,000 bytes the PR is skipped withhidden test name pattern too long.files/<repo-relative>, the raw hidden test files for review.
Assumption (recorded): the frozen
Planschema and the #150 cohort runner requirePlan.Task/Plan.Acceptanceto be clean plan-relative paths (no..) read from the plan's own directory. Keeping hidden content out oftasks/while staying directly consumable by the cohort runner therefore meansplan.json,issue.jsonandacceptance.share co-located underhidden/pr-<N>/(the cohort stub's taskplanpath points there), rather than placingplan.jsonintasks/with a..-relative reference.
Replay executes repository test code at build time. It does so only under a scrubbed
environment allowlist (PATH, a private HOME, TMPDIR, GOCACHE, GOMODCACHE,
GOFLAGS=-trimpath, GOTOOLCHAIN=local, GOPROXY=off, LANG) — never the inherited
environment — and in throwaway worktrees created with core.hooksPath=/dev/null. In a
worktree at baseCommit the hidden tests are applied (renamed-away old paths removed) and
run with go test -json and the same -run filter as acceptance.sh, so the initial
failure is reproduced by the task's own tests and the merge-pass validation below is not
failed by an unrelated test; the PR counts as reproducing the initial failure when at least one
hidden test reports a fail action (a test-failure) or when the failure is a
test-compile: the test package does not compile at base because the hidden tests call
identifiers the PR adds. To distinguish a test-compile from a real build error, when the base
run reports a build failure the builder runs go build -buildvcs=false <pkgs> in the same
worktree (before it is removed) under the same scrubbed env — go build ignores _test.go,
so a success, or a package with no non-test Go files at base (one the PR creates), means the
non-test code builds and only the tests fail to compile. Any other non-test build failure
remains the build failed at base: … skip. A missing toolchain or an incomplete run is a
distinct recorded skip, never a reproduced failure. For validity, the hidden tests are also
run in a worktree at the merge commit and must pass there.
The non-test build is run once per hidden-test package: the failure is only a test-compile when
every package either builds or reports no non-test Go files; any other per-package failure is a
real build error. A base build failure counts as test-compile only when the collected compiler
output actually contains compile-error lines from which at least one identifier is extracted — a
TestMain/init panic or os.Exit before any test runs stays a build failed at base skip.
A test-compile task gains an interface contract. The builder collects every compile error
(re-running go test -json -buildvcs=false -gcflags=-e <pkgs> — -e on the named packages only,
so the compiler does not stop at ten), parses path:line:col: message lines, and extracts the
identifiers the hidden tests need (undefined: Name, undefined: pkg.Name, x.Name undefined (type T …) → T.Name, too-many/not-enough-arguments and cannot-use → the call target with any
instantiation suffix stripped, and unknown field Name … type T → T.Name). If that collection
run does not complete (timeout, interruption or output overflow) the PR is skipped with setup error: compile-error collection did not complete. It then resolves those declarations at the
merge commit with go/parser+go/ast+go/printer over the merge tree's non-test .go
files — funcs and methods with the body dropped, the full type spec (including type parameters)
for a struct or interface, the field line for T.Name (promoted embedded fields included), every
method of that name for a v.M call on a local variable, and const/var specs (the value dropped
whenever it contains a function literal or is not a plain literal/identifier/selector/composite
expression). Package qualifiers resolve through the test files' imports by the imported package's
declared name (and dot imports), so a compiler-printed pkg.T resolves even when the test aliased
that import. Predeclared names (string, int, error, the builtins, …) and identifiers
qualified by an import outside the lab module (e.g. io.Reader) are excluded — they name
existing declarations, not identifiers the PR adds — so they never enter the contract nor the
cross-package method fallback. Unresolved names render as Name // declaration not found. If,
after rendering, no extracted identifier resolves to a declaration, the PR is skipped with build failed at base: no interface identifiers recognized: <build detail>. The result, sorted by
package path then name and capped at 400 lines (a larger contract skips the PR with interface contract too large), is appended to both issue.json (Body) and task.md under the heading
## Interface contract (from the merged change), one fenced go block per package (each fenced
with a backtick run longer than any inside the block). taskSource then becomes
current-issue-body+interface-contract. approval.md records the initial-failure kind, the
extracted identifiers and any unrecognized "Other compile errors at base" (the latter never
leak into the task).
A PR is skipped, with the reason recorded in <out>/skipped.json, when it has no _test.go
changes, no linked issue, more than 25 changed files, more than 2,000 changed lines, only
non-Go changes, hidden tests outside the lab module (v1 assumption), a symlinked test
file, an initial failure that does not reproduce, a non-test build error at base, an interface
contract larger than 400 lines, hidden tests that do not pass at the merge commit, or any
per-PR infrastructure failure (gh/git error, unsafe path, linked reference that is a pull
request). Per-PR failures never abort the run;
skipped.json and cohort.stub.json are always written.
To stop the comparison harness from reading the answer out of git history, the builder also
writes a non-bare, history-free checkout at <out>/source containing only the tasks'
base commits (each fetched shallow, with --update-shallow, under refs/replay/pr-<N>), so
no merge commit, PR branch or later history is present. It has a detached HEAD at the first
built task's base commit and a clean work tree, and origin set to
https://github.com/<repo>.git with remote.origin.fetch set to
+refs/forgecell-none/*:refs/forgecell-none/*, a refspec that matches nothing on GitHub (an
unset refspec would still fetch the remote HEAD). That refspec only prevents an accidental
bare git fetch origin; network isolation of the coding harness is the real control. Every
fetch uses --no-write-fetch-head, so no FETCH_HEAD records the full-history checkout's
path. A non-bare checkout is required because the molecule
runs git rev-parse --show-toplevel and git remote get-url origin on --source, which a
bare repository fails; evaluate --cohort refuses a bare --source before any attempt with
--source must be a non-bare git checkout (replay writes <out>/source). Cohorts built by
replay must use <out>/source as --source. <out>/cohort.stub.json lists every built
task pinned by SHA-256 and records source as that checkout; before forgecell evaluate --cohort, add exactly two Formula variants, remove the documentary source field (the
strict cohort decoder rejects unknown fields), rename it to cohort.json and pass --source <out>/source. When no task is built (or the checkout cannot be written, in which case the
partial checkout is removed), <out>/source is not created and the stub carries no source
hint and an empty tasks list.
A multi-task <out>/source holds every task's base commit, and a later PR's base contains an
earlier PR's solution. evaluate --cohort therefore never runs an attempt from --source
itself, which it only reads: for every attempt it creates a fresh throwaway source under
that attempt's directory holding only the plan's base commit (shallow, no replay refs,
detached HEAD, origin copied from --source, the same no-op fetch refspec, no
FETCH_HEAD), runs the molecule, diff capture and verification from it, and removes it after
the attempt. With --keep-workspaces the attempt's trees and its source are kept, but they
live in that attempt's own source, never shared, so a later attempt cannot see an earlier
attempt's solution.
Because the task models the issue as it was when work began, issue.json freezes
State: "OPEN" even when the source issue is now closed — intake's State == "OPEN" gate
would otherwise reject every replayed (closed) issue. The live issue state is recorded in
approval.md only. The frozen plan stores no manifest evidence: at run time evaluate's
configureReadiness cites, for each scope path that does not exist at base, the evidence ID
of the nearest-ancestor manifest (go.mod, package.json or pyproject.toml) in the
repository snapshot CollectForTicket collects from the source at the base commit, so
readiness accepts an added file (with no ancestor manifest the attempt fails readiness as
before). The builder writes nothing outside
<out>, and its tests use recorded gh fixtures with a tiny fixture repo — no network.