Skip to main content

Plan steps and checks

Most appraisal reads work after the run. This page covers the part that runs inside it: each plan step is checked when the model marks it done, and what the checks find can feed planning guidance and learned rules. See How appraisal works for the whole picture.

A step is checked the moment it is ticked off​

Task, project, and offline appraisals read work from outside the run. Step appraisal happens inside it, turn by turn: a plan step moving to completed is a claim the model makes about its own work, and — the same rule one tier up — a self-report is exactly the thing never to trust. At the board tier the owner is the check; at the todo tier there is no person, so the check is structural.

The deterministic half is a pure function of what the step actually did. The harness already traces every tool call, so the span between a step going in progress and being marked done is arithmetic — no model, no threshold, no tuned constant. Five findings:

FindingThe span says
landedthe common case, and it renders nothing — a line per honest step would be bulk carried in the transcript for the rest of the run in exchange for confirming what the model already believes
the null stepzero tool calls: the box was ticked and nothing was attempted
ended on failurethe last thing tried failed, and nothing after it succeeded — a failure among successes is recovery, and recovery is the model working
ended on refusalthe last thing tried was refused: the step was blocked, not botched — telling the model otherwise would send it to fix code that is working
check faileda check the step itself declared ran and did not pass — read before the others, because the last call may have succeeded while the claim did not

Ambiguity reads as no finding: a sibling call still in flight may be the work the span looks empty without, and a denial in the same batch cannot be attributed to this step over any other, so both silence the check. An absence is not evidence, and a reading that fires on honest work is a line the model learns to skip — which is how a check that protects nothing survives.

The finding is rendered onto the todo result; the response is the model's. The harness names what happened and never rewrites the plan, because the plan is the model's — accept, revise the step, revise the plan, or ask, and a step already revised once escalates rather than looping.

Model escalation is off by default. Enable [agent] step_escalation = true to consult a quarantined model when the deterministic signals leave ambiguity. --no-step-escalation disables it for a run; mecha eval forces it off. The loop caps escalation at five attempts per run. Two triggers decide whether to ask, not what the answer should be:

  • A span outlier — a step that took at least three times the plan's mean call count (and six calls outright, so a plan of tiny steps cannot escalate on noise). The question is whether the decomposition should change for the steps still ahead, or this step was simply harder — a threshold cannot tell those apart, and a model can.
  • An unverified claim — the step's own words read as checkable ("tests pass", "builds clean") but nothing in its span looks like a check. The eval rig's rule, one tier down: grade the artifact, never the claim.

The escalation is one quarantined call, live in the loop — it has to reach this run before more turns are spent on a bad plan, so it has no CLI surface of its own. Its free-text reasoning never re-enters the conversation: what comes back to the run is a fully templated nudge, because a model's paraphrase of text it just judged is the paraphrase risk arriving through the one channel that does reach context. The model decides one binary — carry on, or revise the plan — and nothing else.

Null steps and reopened steps​

mecha sessions health now reports completions, null steps, and steps reopened after completion. Reopening is distinct from completing a step without doing work; the plan can survive across runs, so a reopen need not follow a completion in the same run.

JSON fieldMeaning
step_completionsAll recorded step completions.
step_measuredCompletions with an unambiguous measured tool-call span.
step_nullsMeasured completions with no tool calls behind them.
step_reopensCompleted steps moved back into progress.
step_null_rateShare of runs with a null step, among runs with a measured completion.
step_reopen_rateShare of runs with a reopen, among runs that completed or reopened a step.

The rates are per-run shares, not ratios of event totals. Their denominators are included in JSON, and older records without the sensor do not count as clean measurements. These counters do not by themselves declare a run unsuccessful.

A step can say how to check it​

A plan item may carry three optional predictions beside its content: expect, one sentence saying what will be true when the step is done; check, a command whose exit code says whether it is; and expect_calls, how many tool calls the model thinks it will take. They are echoed under the step in every todo result and ride the carried block across a compaction, so the model meets its own prediction again after either. Once a step is completed its check is frozen: a different check on that write or any later one is reported back as a change, not taken.

Declared checks execute when a step is completed. The harness dispatches the frozen command after the original tool batch, through the usual approvals, hooks, sandbox and interlock. Check execution defaults on, with at most 16 checks per run; --no-step-checks disables it. Refused, unavailable or skipped checks remain unverified. A passing check establishes only what that command tested at that time. See planning feedback for how failures and other prediction mismatches feed learning.

When a completion update omits a check declared while the step was open, the harness restores and freezes that check and executes it through the usual guards. A check explicitly withdrawn while the step is still open stays withdrawn.

Planning feedback and goal context​

Plans can declare serves, a checkable expect, a shell check, and an expect_calls estimate. Completing a step runs its frozen check through the usual approvals, hooks and sandbox. At most 16 checks run per run. Refused, unavailable or skipped checks remain unverified; a passing check establishes only what that command tested at that time. Recordings that dispatched a harness check are excluded from trace replay and its probes until replay can reconstruct the check observations. Independent artifact-task grading remains available.

Confirmed goal references persist across turns and session resume. Appraisal associates events with their historical goal, can retain a related charter line, and keeps the owner's completion verdict alongside execution evidence.

goal_context retrieves up to four applicable goal-linked rules and two recent examples with passing checks. It preserves scope and provenance and runs only when requested by the agent. It returns nothing until those links exist: a reflection takes its goal from the plan (or the named goal) at the moment of the intervention, or else from the goal its run was anchored to (a delegated task, a trigger, a goal you confirmed), and an example needs a completed step whose check passed, so on an install where runs rarely write a plan the example list stays empty, and lessons come only from anchored or planned runs. An empty answer means no recorded evidence, not a proven approach. Failed checks, changes to frozen checks and verified task-criterion failures can supply bounded mismatch reflections to mecha reflect. Estimate overruns remain observations; they do not establish a behavioral lesson. Unknown or tainted mismatch evidence is excluded.

[agent]
step_checks = true # default; --no-step-checks disables execution
goal_guidance = false # opt in to fixed planning advice

With goal_guidance = true, plan updates receive advice based on confirmed-goal alignment, remaining work, verification gaps and ordered charter sensor readings — for a line whose age sensor watches drafts, questions or requests, whether any pending commitment it weighs is past its patience. The sensor numbers stay outside model prompts. Guidance is experimental and has not shown a benefit: the first Qwen 3.6 35B pilot tied at 36/36, and on harder tasks with a confirmed goal it did worse (see the full pilot record). Use --no-goal-guidance to disable it for a comparison run.

The headroom line. When the run has a compaction threshold and has sent at least one request, a plan write's result ends with one line such as context: 42k of 170k before compaction (25%), with a pace and an approximate number of turns left when those are known. It is a statement of fact with no instruction in it, and it appears only on plan writes, so it costs nothing on a turn that does not touch the plan.

Comparing guidance on real runs​

From a checkout, eval/appraisal-guidance.toml defines a synthetic pilot with 12 tasks, three seeds and two arms: 72 runs at the same 24-turn ceiling. Checks are enabled in both arms; only goal guidance differs. Both arms disable learned rules, skills, hooks, messages, fallback and step escalation. The charter, graph and draft queue are synthetic; each task starts with fresh workspace files and reset fixture state.

The manifest pins local / qwen3.6-35b-a3b in both arms. Keep the model, configuration and checkout revision fixed through the run. For another model, change both arms and choose a new experiment name before registration. From the repository root, using the binary built from that revision:

cargo build -p mecha-cli
./target/debug/mecha exp new eval/appraisal-guidance.toml
./target/debug/mecha exp run appraisal-guidance-qwen36-35b-20260909 --dry-run
./target/debug/mecha exp run appraisal-guidance-qwen36-35b-20260909
./target/debug/mecha exp export appraisal-guidance-qwen36-35b-20260909 > /tmp/appraisal-results.json
python3 scripts/appraisal-report.py /tmp/appraisal-results.json
./target/debug/mecha exp judge appraisal-guidance-qwen36-35b-20260909 --json

exp run --limit N bounds one invocation; repeat the run command to resume. Registration freezes the manifest, so use a new experiment name for a changed design. Archive the export with the checkout revision and model configuration; source fixture contents are not all covered by the condition hash.

The cases cover budgeted plans, charter conflicts, revised briefs, contradictory or missing evidence, artifact repair, misleading checks, adjacent verification, review pressure, distractors and scope control. Grading checks the saved JSON artifact and preserved inputs and drafts. Passing a model-written check or claiming completion cannot replace those checks. Tasks also require use of todo.

The report pairs task, seed and repetition, separates execution failures from graded failures, and gives each metric its own observed-pair count. Missing cost, owner actions or plan counters remain unknown; cost comparisons require complete usage records. Negative failure-rate differences favor guidance. The report is descriptive; exp judge retains the existing selection/holdout gate.

This pilot measures guidance with current-task evidence. It does not test learning across runs, semantic owner corrections across turns, or real owner-policy outcomes. Owner interventions are unmeasured in this single-run design. Inspect traces alongside the report before drawing conclusions or enabling guidance by default. The 2026-09-09 Qwen pilot tied on every task outcome, so the gate rejected promotion. It exposed completion-time check omissions and no confirmed-goal-anchor coverage; guidance remains opt-in. Results and limits are recorded in results/appraisal-guidance-qwen36-35b-20260909/README.md on the machine that ran it (run output is not kept in the repository).

The pilot record​

Six pilots completed on 2026-09-09, all on the local Qwen 3.6 35B model. Each directory under results/ on the machine that ran it holds the method, the raw records and the gate's verdict (run output is not kept in the repository).

PilotControlTreatmentPaired outcome
Guidance, first design (appraisal-guidance)36/3636/36all tied
Guidance, harder tasks with a confirmed goal (appraisal-guidance-v2)20/2418/242 improved, 4 regressed, 18 tied
Guidance, privacy follow-up (appraisal-privacy)6/65/61 regressed, 5 tied
Attribution and learning (appraisal-attribution-v2)30/3630/36all tied; no learned rule was exposed
Sequential learning (appraisal-learning-v2)6/66/6tied; too few reflections to learn a rule
Learning from verified mismatches (appraisal-mismatch)10/129/12learning did not improve

The gate rejected promotion in every case. Two further designs did not produce a result: the first attribution pilot was stopped because post-run diagnostics were being mined as invented owner corrections, and the first learning design ran only as a dry run. The honest reading is that neither guidance nor the learning arm has shown a task-success benefit on this model, and guidance lost on the harder tasks. The dated entries in docs/HISTORY.md carry the detail.

Independent validation of planning lessons​

mecha run --mismatch-case PATH binds an owner-authored JSON fixture before execution. It contains the exact prompt, a confirmed goal, a map of starting files to their UTF-8 contents, an artifacts map of output paths to expected JSON values, and a preserve list of input paths that must remain unchanged. Keep this file outside the task workspace. Every starting file must be listed; the run refuses a different prompt, goal or starting state. Expected output is recorded as local metadata and is never sent to the model.

For example, a fixture can declare:

{
"prompt": "Add the numbers in input.json and write the sum to answer.json.",
"goal": "task:sum",
"files": {"input.json": "[2,3]\n"},
"artifacts": {"answer.json": {"sum": 5}},
"preserve": ["input.json"]
}

The first supported surface is fs_read, fs_write, fs_edit, fs_list and todo. Narrow the recording with --tool; disable MCP, hooks, skills, messages, fallback, outbox routing, step escalation and goal guidance. These conditions are checked before recording the fixture. Shell and external services are not supported by this artifact validator.

mecha validate --trigger mismatch and the learn --auto proposal gate can then compare rule sets on a clean recorded mismatch. Each arm repeats the whole registered task in a fresh workspace and checks its actual JSON output against the independent expectation. This is a task-outcome comparison, not a replay of the filesystem at an intermediate step or a validation of the original work estimate. Steer and denial probes keep their existing branching behavior.

Use the same disabled hooks, outbox and messages settings during validation. File writes still use the current approval policy; use --yes only when those fixture writes are authorized. A refusal, missing fixture, changed tool surface or uncertain provenance remains ungraded. Malformed or incorrect artifacts fail. Per-arm receipts, including fixture/prompt fingerprints and usage, are stored under learning/artifact-probes/; whole-task results do not certify the original step's tool scope. Existing probation and retirement rules still apply.

Experiments register these fixtures under [tasks.mismatch_cases.<task-id>], with matching [tasks.confirmed_goals] entries. Fixtures are part of the condition hash and force the supported file-tool surface in both arms. The registered example is eval/appraisal-mismatch.toml: six training tasks followed by six transfer tasks, with rule exposure measured separately from task success.

Learning from a verified task criterion​

An owner-bound --mismatch-case can opt into training diagnostics with criteria. Each entry names an artifact and JSON pointer already present in its expected outputs. After the run, mecha records whether that criterion passed, failed or could not be evaluated. Expected values and the model's output prose do not enter the reflection payload.

A criterion can include a context count constraint:

"criteria": {
"review_priority": {
"artifact": "answer.json",
"pointer": "/review_first",
"context": {
"source": "context.json",
"observed_pointer": "/outbox_waiting",
"limit_pointer": "/review_threshold",
"relation": "greater_than",
"charter_goal": "charter:review-pending"
}
}
}

The context source must be a pinned, preserved input. Counts and limits must be nonnegative integers; supported relations are greater_than, at_least, less_than and at_most. Missing or changed context is unknown. A charter goal is an optional owner-supplied association, not a live charter reading. Omit criteria on held-out evaluation tasks to withhold this diagnostic feedback.

A forecast overrun still appears in the planning record, including whether multiple steps completed together. It does not by itself establish wasted work or qualify for a new behavioral rule. Verified criterion/check failures and changed checks retain the provenance, minimum-evidence and validation gates.