Skip to content

The run folder and run.json

.obelize/runs/<run-id>/ is the evidence a run leaves behind; run.json is its index. tests/unit/test_run_folder_contract.py fails when this file, the models in src/obelize/models/ and .github/workflows/e2e.yml disagree.

Layout

.obelize/
  .gitignore                    `*`, so git ignores everything in this folder
  latest                        the id of the most recently completed run
  runs/
    20260917T142530Z-3f9a1c72/
      run.json                  metadata and the index (this file's subject)
      findings.json             every finding, with verdict, bail and caused_by
      plan.json                 the planned edits, whether or not they were written
                                (`plan` and `apply` only; a scan has no plan)
      patch.diff                unified diff, git-apply compatible
      packs/<provider>/<slug>/  one folder per pack the run used, in `packs[]`:
        pack.yaml                 the pack, copied verbatim
        pack.sha256               its hash
      REPORT.md                 the human-readable report
      snapshots/before/…        files as they were before the patch
      snapshots/after/…         files as they were after the patch
      verify/…                  command output logs, junit XML, verify.json
      model/model.json          what a model was asked, and the rows it was not
      model/proposals-<n>.json  one per consultation: context range, prompt hash,
                                token counts, the proposal and whether it passed
      undo.json                 written if the run was reverted
      journal.json              only while an apply is writing, or after one was
                                interrupted: the rows it meant to write

What each file may contain is in PRIVACY.md. Every mode writes run.json, findings.json, packs/ and REPORT.md; the rest only when they apply: plan.json and patch.diff on plan and apply, snapshots/ on apply for the files it wrote, verify/ when a verification command ran and after obelize verify, model/ when a model provider is configured, undo.json after obelize undo. An absent artefact does not make the folder malformed.

An apply puts its originals down first. After the tree gate and before any write, it creates the folder with a snapshots/before/ copy of every file the plan would write and journal.json (the run id and the plan's file_edits rows); a link or a refusal there stops the run with the repository untouched. On finishing it writes run.json, removes journal.json and the unused snapshots, then writes latest. After an interruption, obelize undo --run <id> works from the journal and skips a file the run never reached as hash_mismatch: it is already the original.

.obelize/ is always excluded from scanning, before any user configuration. scan and fix write .obelize/.gitignore unless something is already at that name, so a git add -A never picks up a run folder; obelize never edits your own .gitignore.

obelize verify and obelize undo update an existing folder, never create one, and never move latest. verify replaces run.json's verify (with no baseline: no pre-patch tree is left to measure), exit_code and timings.verify_ms, verify/after/ and verify/verify.json; it keeps verify/baseline/, and REPORT.md gains a block saying the verdict was replaced. A recorded baseline that did not pass leaves the folder untouched. undo adds undo.json and nothing else. The folder may come from someone else's checkout, so neither follows a link in it: both open directories from .obelize/ down without following links, refuse a link where a directory belongs, and write beside a name then rename over it.

A folder is read by the release that wrote it: a 0.1.0 folder fails the schemas of a later release, so undo a 0.1.0 apply with 0.1.0.

.obelize/latest

One run id and a trailing newline, so run_id=$(cat .obelize/latest) needs no trimming. A regular file, never a symlink (THREAT_MODEL.md). Written last, after run.json, so it always names a complete run. The last run to finish owns it; a script that cares passes --run <id>. Obelize never deletes a run folder or latest.

Run id

20260917T142530Z-3f9a1c72
^^^^^^^^^^^^^^^^ ^^^^^^^^
UTC, ISO 8601     8 random hex characters
basic format

Pinned as ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{8}$. Lexicographic order is chronological. No colons, which Windows filenames forbid. The suffix is random, not a hash or a secret: it separates runs started in the same second.

run.json

Top level

Modes says which of scan, plan (a fix without --apply) and apply fill the field. A field that does not apply is null; an array with nothing to report is empty. Neither is ever absent.

Field Type Modes Description
run_id string all The folder name.
obelize_version string all The writing obelize's __version__.
mode string all scan, plan or apply.
exit_code integer all The process exit code (CLI.md); known last, so run.json follows every other artefact.
argv array of strings all The arguments as received, excluding argv[0], redacted like command output (THREAT_MODEL.md): a credential of a shape it lists reads [REDACTED], one it does not list is kept, so review the folder before sharing it. --trust-repo-config is included.
python object all {version, implementation}, for example {"version": "3.12.9", "implementation": "CPython"}. No interpreter path: it names the machine and often the person.
platform object all {system, release, machine} from platform.
git_sha string or null all HEAD at the start of the run; null when the target is not the top of a git repository.
git_branch string or null all The current branch; null when detached or when the target is not the top of a git repository.
git_dirty boolean or null all Whether a tracked file anywhere in the repository had uncommitted changes at the start. false outside git, where the dirty-tree refusal cannot protect you; null when git could not say, which refuses an apply as tree_unknown.
packs array all One row per pack the run used, in id order; empty when none applied. See packs[].
config object all See config.
counts object all See counts.
file_edits array all Files written, empty on scan and plan. See file_edits[].
refused array all Planned writes that did not happen; empty on scan and plan. See refused[].
idempotent boolean or null apply See idempotent.
withheld array all One row per withheld finding. See withheld[].
verify object or null plan, apply See verify.
model object or null plan, apply null unless a model provider was configured. A scan never carries one. See model.
limitations array all What the run could not look at. See limitations[].
timings object all The only place a wall clock appears. See timings.

packs[]

Field Type Description
id string <provider>/<from>-to-<to> (PACK_SPEC.md), or inventory/<distribution> for a library listed with scan --library.
pack_version string The pack content's own semver.
sha256 string 64 lowercase hex characters, matching packs/<id>/pack.sha256 and the copy in pack.yaml. An inventory has neither: its hash is that of the modules it listed.
source string bundled, file, or inventory for a library with no pack. No path: it could be absolute and outside the repository.
blocked string or null runtime_unsupported when the project declares a Python the new distribution does not install on (read from pyproject.toml, setup.cfg, Poetry's key, a Pipfile's [requires] or a string python_requires= in setup.py), legacy_version_unsupported when the legacy distribution is pinned below the pack's floor, unpinned, or undeclared while the code uses it, or new_version_unsupported when a manifest pins the new distribution so that it admits no release the pack writes (pypdf==3.0.0): this pack reports its findings, its rows are withheld for that reason, and it proposes no migration. An unparsable declaration leaves it unblocked. A pack that names its newest legacy release does not block on an unpinned declaration unless the repository shows another release: code or text that names a class or module that release lacks is one (the newest-release decision). The message of a legacy_version_unsupported block adds that a pin moves the release and not the code, so a call to a name the pinned release removed is ported first.
assumed string or null What the pack took a legacy declaration for where it names no version, or one below the pack's floor that admits it: the pack's from.newest, such as 3.0.1, or, for a pack that names from.since, the range from.version, such as >=0.27.0,<1, where the code uses a name only that range defines. null when nothing was assumed: a declaration that already reaches the floor (PyPDF2>=3) is not, and neither is any run of a pack that names neither field. The terminal and REPORT.md say so too.

When several packs ran, each ran over what the ones before it wrote, in id order, so an edit's line is in the file as its own pack saw it, and a rule_id is <pack id>:<rule id>.

config

Field Type Description
source string file when .obelize.yml was read, defaults when it was absent.
include string The effective pattern, command line merged over file.
exclude array of strings The effective user exclusions; the always-excluded set is in CLI.md.
max_file_bytes integer The effective limit; a larger file appears in limitations.

counts

Integers summarising findings.json, never a substitute for it.

Field Description
files_selected Files the walker chose.
files_parsed Of those, the ones both parse gates accepted.
findings Total findings, all kinds.
eligible, auto, needs_review, unsupported, not_a_usage The status split, summing to findings.
warnings Edits carrying a warning code; outside the split, since a warning withholds nothing. 0 on a scan.

A scan reports the §10 scan-status split (eligible: nothing withheld it); plan and apply report the §3 verdict split (auto: rewritten). The unused name is 0.

file_edits[]

What was written; the plan is plan.json. Empty means nothing in the repository changed.

Field Type Description
path string Repository-relative, forward slashes on every platform.
before_sha256 string The file as read; its name under snapshots/before/.
after_sha256 string The file as written. obelize undo reverts a file only while its hash still equals this; obelize verify reports tree_changed otherwise.
hunks integer Hunks this file contributes to patch.diff.
rules array of strings The pack rule ids behind the edit, sorted. Empty exactly when proposals is not.
proposals array of integers The model/proposals-<n>.json behind the edit; at most one. Never beside rules: a model is asked only about a file the rules left alone.

idempotent

true when an apply wrote no file (file_edits empty); null on scan and plan. It describes bytes, not success: an apply where everything bailed is true too. e2e.yml pairs it with git diff to check that a second apply changes nothing.

refused[]

Planned writes that did not happen; a reviewer reads this first. limitations[] is what the run could not look at. Without this array a refused apply would read as a repository that needed no work.

Field Type Description
code string A SCAN_VOCABULARY.md §13 value.
path string or null Repository-relative. null exactly for tree_dirty and tree_unknown.
detail string One sentence a person can act on.

withheld[]

Field Type Description
path string Repository-relative.
line integer 1-based.
symbol string or null The resolved legacy qualified name; null on a parse_error row.
bail string A SCAN_VOCABULARY.md §4 code.
caused_by array of strings or null Set exactly when bail is file_not_fully_migrated (§6).

It repeats part of findings.json so that run.json alone, which the bug-report template asks for, says what was withheld and why.

verify

null on a scan; on plan and apply always set, even when nothing ran (a dry run records not_run / dry_run). commands is empty whenever status is not_run.

Field Type Description
status string pass, fail, inconclusive or not_run: the worst of the commands', fail > inconclusive > not_run > pass.
reason string or null From the vocabulary in CLI.md, which maps both to the exit code. null only on pass.
commands array One entry per command, in the order they ran. See verify.commands[].
baseline object or null status, reason and commands before the patch, one level deep. A failed baseline makes the run inconclusive / baseline_failed, never fail. A write refused after the baseline ran keeps it under not_run / no_changes_to_verify.
edited array The files the run wrote whose bytes were not the run's when obelize verify --current checked the tree as it stood, sorted; empty for any other verification.

verify.commands[]

One row per command that ran, under verify and under verify.baseline.

Field Type Description
command string The command as given, redacted the same way as argv; the process still ran the text you typed.
source string cli, user_allowlist or repo_config: its trust rung. A pack's verification.suggestions are never executed.
status string This command's verdict; only exit code 0 passes.
reason string or null Set exactly when it did not pass: command_failed, command_not_executable or timeout.
exit_code integer or null null when no process started; negative when a signal killed it on POSIX, and 3221225786 when the deadline ended its job on Windows. A command stopped at its deadline keeps its code, even 0, and reads timeout.
duration_ms integer
truncated boolean Whether the output was capped.
log string or null Path under verify/, relative to the run folder, capped and redacted per PRIVACY.md; null when nothing was printed.
junit string or null Path under verify/, when a junit XML was produced.
in_obelize_environment boolean Whether PATH found the program in obelize's own environment, which usually lacks the project's test dependencies; false for a program named by its path. A baseline command with this true that failed or timed out adds a hint, saying which, to the summary and REPORT.md.

Output is a file, never a field: a log can be a megabyte and run.json goes into bug reports. verify/baseline/ and verify/after/ each number their commands from 1 (1.log, any junit report beside it). verify/verify.json is the verify object alone.

model

null whenever model.provider is none, the default. The detail is in the model/ directory.

Field Type Description
provider string openai_compat, the only v0 value.
host string The host of base_url, never the full URL, whose path can carry a credential.
model string The model name sent.
proposals integer Edits proposed, not questions asked.
accepted integer Proposals that passed every guard check and were written, which needs --apply with --accept-model.
tokens_in, tokens_out integer As reported by the endpoint.

No API key appears anywhere in the run folder, and no raw prompt unless model.log_prompts is set. A written proposal stays a review item: its row stays in withheld[], counts do not move, and it earns no exit 0, because the guard proves an edit safe to apply, not correct.

limitations[]

What the run could not look at, so a small finding count never reads as a clean repository.

Field Type Description
code string models.LimitationCode, ten values: SCAN_VOCABULARY.md §11's seven plus file_too_large, input_does_not_parse and internal_error (§4).
path string or null Repository-relative, when one file is meant. null for unusable_name, whose detail quotes the name.
detail string One sentence a person can act on.

For a refused parse, detail names the gate and quotes the parser's message, never a line of the file (cst.ParserSyntaxError.context and SyntaxError.text stay out of the run folder). For an internal_error, detail names the exception's class and never its message, which can quote the file or a path. An unreadable manifest gets a fixed sentence, because configparser quotes the line.

timings

The only wall-clock values, so everything else compares byte for byte between two runs over one input.

Field Type Description
started_at, finished_at string ISO 8601, UTC, to the second, with a Z suffix.
total_ms integer Measured on a monotonic clock, not subtracted from the timestamps.
scan_ms, plan_ms, apply_ms, verify_ms, model_ms integer or null null for a phase that did not run; model_ms is set exactly when model is.

findings.json

Byte for byte what obelize scan --json prints. Nothing time-derived (no run id, timings or absolute path), so two scans of one input are identical.

Field Type Description
obelize_version string The version that produced it.
packs array One {id, version, sha256} per pack the run used; version is run.json's pack_version.
counts object files_selected, files_parsed, findings and the scan's status split, as in counts.
findings array Every finding, in document order.

A finding

Field Type Description
path string Repository-relative, forward slashes on every platform.
line integer 1-based.
column integer 0-based, as libcst reports it.
kind string SCAN_VOCABULARY.md §1.
confidence_reason string §2.
symbol string or null The resolved legacy qualified name; the distribution on a manifest finding; null on a parse_error. A mock.patch target is recorded unquoted.
evidence string or null Always null: source excerpts are kept out of evidence.
scan_status string §10.
bail string or null §4. Set exactly when scan_status is needs_review or unsupported.
caused_by array of strings or null §6. Set exactly when bail is file_not_fully_migrated; sorted, de-duplicated.

Document order is (path, line, column, kind, symbol), and src/obelize/models/scan.py refuses any other: libcst yields references in an order that varies between processes. A finding has no verdict or warning; those belong to an edit, in plan.json and REPORT.md.

plan.json

What the run would write, whether or not it did. Written by plan and apply; with file_edits[] it defines a dry run:

Run plan.json's files[] run.json's file_edits[]
a dry run everything it would write empty
an apply that wrote it all everything the same list
an apply that wrote some of it everything the subset that landed, with refused[] naming why the rest did not

No time-derived value and no counts: it lives only beside run.json.

Field Type Description
obelize_version string The version that produced it.
packs array As in findings.json.
files array One row per file whose bytes would change, in path order, shaped like file_edits[].
edits array Every edit row, written or withheld, sorted by (path, line).

A planned edit

Field Type Description
path, line string, integer Repository-relative, 1-based.
status string SCAN_VOCABULARY.md §8.
rule_id string or null <pack id>:<rule id>, the rule behind it; null where no rule claimed the row, and on model_proposed.
reason string or null §4. Set on needs_review and unsupported, and kept on model_proposed as what the rules refused.
caused_by array of strings or null Set exactly when reason is file_not_fully_migrated.
warnings array of strings §5, sorted.

A file the scan already refused has no row here, since no rule claimed it; so REPORT.md prints the findings beside the edits, and under Withheld what to change for each reason the table shows. It also holds what each pack says about the findings it flagged (Guidance) and what each pack does not handle (Limitations), read from the packs the run loaded: no field of findings.json, plan.json or run.json carries them. Under Limitations it also names the files that use the legacy API and that no rule writes, a notebook with the cells that do and a stub with none, which no field carries either. Under Dependency manifests it names the lock files beside a manifest pin that was written or would be, or above it, since obelize does not edit them; that is not recorded in a field either.

patch.diff

A unified diff of every file plan.json names, in path order; empty when nothing would change.

  • Split on \n only, as git does, so git apply restores exact bytes: a CRLF line keeps its \r, a bare-CR file is one line.
  • Paths quoted as git quotes them, with the a/ and b/ prefixes -p1 expects. A name with a space is not quoted; its --- and +++ lines end in a tab.
  • No index line: git apply and patch -p1 accept its absence, and a wrong one would fail git apply --3way.
  • Never used to write or revert. A write puts down whole content through fsutil.write; a revert restores snapshots/before/<sha256> while the file still hashes to after_sha256. A reverse-applied diff can fail halfway or hit a drifted file.

snapshots/

snapshots/before/<before_sha256> and snapshots/after/<after_sha256>, no extension, written by apply for the files it wrote. Content-addressed, so no bytes are stored twice; file_edits[] maps each hash to its path.

The model/ directory

Written only when model.provider is not none: model.json is the index, each proposals-<n>.json the detail.

model/model.json

Field Type Description
obelize_version string
summary object The model object.
consulted integer Questions asked: the number of proposals-<n>.json files.
skipped[] array of objects Withheld rows not put to a model: path, line and a skip reason, so the selection can be audited by what it left out.

model/proposals-<n>.json

One per consultation, numbered from 1 in asking order, whether or not anything was proposed.

Field Type Description
index integer The <n> in the file name.
path, line string, integer The row asked about. A proposal may edit no other file.
symbol string or null The legacy symbol at that line.
bail string What the rules refused with: a SCAN_VOCABULARY.md §4 code.
context_start_line, context_end_line integer The range sent, 1-based, inclusive.
prompt_sha256 string Of the exact request body, so a re-asked question shows without either prompt.
prompt string or null Those bytes; null unless model.log_prompts is true. A prompt is source code.
provider, model string Who was asked.
tokens_in, tokens_out integer As reported; 0 when nothing was reported.
outcome string A proposal outcome.
failure string or null A provider failure, set exactly when outcome is unanswered.
refusal string or null A guard refusal, set exactly when outcome is guard_refused.
detail string or null One line from whichever decided; the endpoint's words are redacted, then cut.
proposal object or null What came back, unvalidated (path, start_line, end_line, symbol, replaces, replacement, rationale, imports), so a proposal naming ~/.bashrc is refused by name, not lost in a parse error.

The four Phase 3 vocabularies

Closed sets in src/obelize/models/vocabulary.py, published here where a reader meets them.

A model was not asked (ConsultSkip)

skipped[].reason, in the order the checks run.

Value Meaning
not_needs_review The row is unsupported: the new SDK has no equivalent to propose.
not_a_source_file A dependency manifest, or a file this run could not write.
atomicity_only The bail is file_not_fully_migrated; the rows in its caused_by are asked about instead.
pack_refused A flag_only rule refused the surface; a model overriding it would move behaviour out of the pack's data.
consumed_elsewhere The bail is configure_consumed_elsewhere: the defect is in the other module.
context_too_large No range holding the whole binding group fits the budget. Nothing is truncated: the "inside what was sent" check would pass a proposal written against something else.

An adapter could not answer (ProviderFailure)

failure: pipeline stages in the order the bytes reach them, not a ranking.

Value Meaning
endpoint_unreachable Nothing answered: no route, no listener, a dropped connection, or the deadline passed.
endpoint_redirected A 3xx, refused rather than followed, so the payload and Authorization header never reach a host the run did not print.
endpoint_refused Any other status outside 200–299; its message travels with it, redacted and capped.
response_too_large The body passed the 1 MiB cap.
response_not_json The body is not JSON, typically a wrong base_url.
response_not_a_completion JSON with no choices[0].message.content string.
answer_not_json The content holds no JSON object under any of the three readings.
answer_not_a_proposal An object without the key it was asked for. A bare proposal at the top level is this too: the envelope separates "an edit" from "none".
proposal_does_not_hold EditProposal refuses it: a field it cannot hold, such as an empty path, a missing replaces or a lone surrogate, which no file or report can carry. Unanswered, not refused.

The guard refused a proposal (GuardRefusal)

refusal. Seventeen words in three groups; the first refusal in this order is recorded, because a hostile proposal trips several and a reviewer needs the first.

May we touch this file at all:

Value Meaning
path_outside_root Absolute, holding a .. component, or not a relative POSIX path.
path_not_python Not a .py file. Manifests are obelize's to write, never a model's.
path_not_the_consulted_file A repository .py path other than the one asked about.
file_changed_since_read The file changed since the plan read it, so the line numbers point elsewhere.

Is this an answer to the question:

Value Meaning
outside_the_context The replaced range is not inside the range sent.
site_not_replaced The range does not contain the line asked about.
symbol_mismatch It names another symbol, or the consultation named none to check.
replaced_text_differs The lines it says it replaces (replaces) are not the lines it names as they were sent, trailing whitespace, line endings and blank lines at the end aside: a range that slipped is not the range that was meant. Also refused when redaction joined lines of what was sent, so that no line number can be trusted.

Is what it produces Python, and does it bring anything in:

Value Meaning
replacement_too_large Over the size limits of a model's replacement: too many lines, too many bytes, or one statement of more than 500 tokens, which libcst's parser may not survive.
replacement_not_displayable A control, format or surrogate character other than tab or line break: the line a reviewer reads is not the line that runs.
replacement_carries_redaction It holds [REDACTED, the placeholder for a secret the model was shown instead of the value, and the lines it replaces hold none: written as it stands it would put the word in the file.
replacement_not_encodable Not writable in the file's own encoding.
output_does_not_compile compile() refuses the result: a syntax error, a nesting too deep to read, or await outside async, which libcst accepts. Asked first, because libcst's parser dies on a deeply nested expression.
output_does_not_parse libcst refuses a result compile() accepted, or the result nests deeper than obelize can walk.
replacement_runs_past_its_range It changes how the following lines read (a trailing backslash or open bracket, or an opened block or string), though it parses and compiles.
import_outside_the_target It imports a module the pack does not target, __import__ and importlib.import_module included, or lists such an import in imports.
name_outside_the_question It uses a name nothing licenses: not in the replaced lines, not bound by a target-module import or by itself, not an allowed builtin; never a double-underscore name. Catches imp = __import__, exec("import os"), open(...), a module the file already imported.

What became of a proposal (ProposalOutcome)

outcome, first match in this order: three about the answer, five about what the run did with it.

Value Meaning
unanswered The adapter raised; failure says why. The row stays needs_review; the next row is still asked.
nothing_proposed The endpoint answered {"proposal": null}, a normal answer.
guard_refused refusal says which guard refused it.
not_applied Accepted; the run had no --apply.
not_accepted Accepted; --accept-model was not given.
file_already_proposed Accepted, and an earlier proposal holds this file. The guard checks a whole file, so a file takes one model edit per run; the next run asks about the rest.
write_refused Accepted, and the apply was refused: the tree or path gate, asked again over the files a model proposed, or a file that changed between that gate and the write.
written On disk; its Edit row reads model_proposed.

undo.json

Written into an existing run folder by obelize undo only. Not a run; no id of its own. An undo that put no file back leaves an undo.json already there as it is, so a repeat of a finished undo does not replace the account of what was put back.

Field Type Description
run_id string The run whose file_edits[] and snapshots it used.
obelize_version string The __version__ that performed the undo.
exit_code integer 0 when every file was reverted or already held the original, 4 otherwise; derived from files[].
argv array of strings The arguments the undo was invoked with.
undone_at string UTC, to the second, with the Z.
files array One row per file the run recorded writing, in path order. No count beside it, which could disagree.

undo.json files[]

Field Type Description
path string Repository-relative, forward slashes on every platform.
outcome string reverted or skipped.
reason string or null Set exactly when skipped: one of CLI.md.
recorded_sha256 string The run's after_sha256, which the file must still have.
current_sha256 string or null What is there now; null when unreadable.
snapshot string The pre-migration copy under snapshots/before/, recorded even on a skip.

hash_mismatch is exactly current_sha256 set and unequal to recorded_sha256. There is no undo --force: the refusal names both hashes and where the original is.

Stability

Every format here is unstable through 0.x (CHANGELOG.md) and carries no version number; a change lands with this file, the model and the schema together. verify and undo validate run.json against the closed model that wrote it, so a folder from a different obelize may be refused rather than half-read.

run.schema.json, findings.schema.json, plan.schema.json, undo.schema.json, model.schema.json, proposal.schema.json and outdated.schema.json (the rows of obelize outdated --json, which no run folder holds) in src/obelize/schemas/ are generated from the models and committed; ci / schema-drift fails when one is stale. Every object is closed and every field required, because every writer writes every field. A digest, a run id, an instant or a name carries the regular expression its model checks it with as its pattern; the other checks, such as the name of an environment variable, stay in the models, so the models are authoritative.

What e2e asserts today

.github/workflows/e2e.yml runs the flow against examples/gemini-legacy-app and checks:

Assertion Step
.obelize/latest names a run folder containing run.json after fix --apply
run_id and obelize_version are non-empty strings after fix --apply
mode is apply after fix --apply
packs holds the one requested pack, and its sha256 is 64 hex characters after fix --apply
git_dirty is false after fix --apply
exit_code is 4 after fix --apply
file_edits is empty and idempotent is true after fix --apply
counts.auto is 0, withheld has fourteen rows, and exactly one of them is configure_consumed_elsewhere after fix --apply
verify.status is not_run and verify.reason is no_changes_to_verify after fix --apply
findings.json, plan.json, patch.diff, REPORT.md and the pack's pack.yaml and pack.sha256 all exist after fix --apply
idempotent is true and file_edits is empty after the second fix --apply

tests/unit/test_run_folder_contract.py checks that the workflow reads only fields specified here and reads every one this table claims.

The example gets no edit: summarize.py and conversation.py run on summarizer/config.py's configure, so that file is withheld as configure_consumed_elsewhere. A more automatic result is a rules change to argue, not a number to update. tests/e2e/ asserts the same in-process; the workflow runs a built wheel in a clean interpreter.