The toolkit counts something as verified only after it has run it. This page explains what that means for each stage, when a stage stops instead of continuing, and how checks are reported.
- Execution is the evidence. A fix read in the diff is present, not verified.
cc-verifyreports what ran, what passed, what failed, and what couldn't be verified, and why. - Unverified stays unverified. It is reported with its concrete reason and carried into the verdict and residual risk. "Environment unavailable" with no cause isn't an acceptable reason.
- Nothing is invented. Commands come from what the project defines (lockfiles, manifests, task runners, CI). If a command doesn't exist, it isn't run.
- Verification doesn't edit code unless you ask.
- A pass that couldn't run isn't a passed check. A missing delegated skill is reported as a degraded pass.
- Compacted output isn't evidence. A stage judges the whole diff and cites uncompacted output. When it can't obtain either, the affected part is unverified. See Complete evidence.
Tests that exercise worktree selection or executor dispatch clear inherited
HERDR_*, CODEX_*, CLAUDE_*, and ORCA_* variables, plus CODE_CYCLE_HOME,
XDG_CONFIG_HOME, and APPDATA. A test that needs one of those settings sets
it explicitly in its own scope. CODEX_HOME points to an empty temporary
directory during those modules so Codex auth checks cannot fall through to the
developer's home directory. This keeps a developer's host selection, credential
path, and telemetry location from changing unrelated test behavior.
Herdr is optional when running the suite: with HERDR_ENV unset, worktree
selection uses Git directly; dedicated Herdr tests opt into the flag and mock
the command.
The unit suite checks every native stage profile's credential rules, explicit
exceptions, canonical symlink targets, and each missing-denial failure in
probe_review_confinement. The installed Codex confinement probe and srt
prerequisite probe also test synthetic credential stores without reading host
secrets. Claude settings with inherited allowRead are refused because that
setting could cancel a denial.
On a host that supports native sandbox execution, run the opt-in E7 matrix:
CODE_CYCLE_LIVE_CREDENTIAL_PROBE=1 python3 -m unittest discover -s tests -p test_credential_reads.pyThis requires installed codex and srt. It exercises read/write, publishing,
registry and Docker-grant profile combinations on both executors, prints only
READABLE/denied states for synthetic stores, and fails on any unexpected
read. Publishing profiles must allow synthetic gh credentials and deny the five
other stores. No host credential contents are accessed. Preserve the output
and platform/version information as boundary evidence. macOS/Windows still need
platform validation before removing their Codex preparation fallback.
Do not opt in inside a native stage whose outer sandbox cannot support nested
Codex or srt. The test skips there; record that boundary as not_verified,
and keep unit results separate from real sandbox evidence. Static-only recovery
when the sandbox is unavailable does not establish credential confinement;
verification remains disabled under the existing recovery policy.
Writing Codex dispatches from linked worktrees need some of the worktree's Git metadata, which lives outside the workspace: Git stores objects, refs, and reflogs in the common directory, and the selected worktree's index, HEAD, and pseudorefs in its private directory. Read-only dispatches receive none of it.
No side effects. Sandbox setup and teardown leave .git exactly as it was,
apart from the Git changes the stage makes on purpose. Codex's Linux sandbox
mounts each filesystem rule on its destination and creates a missing
destination as an empty file, which outlives the dispatch: an empty
commondir makes Git treat an ordinary checkout as a broken linked worktree,
and an empty MERGE_HEAD reports a merge in progress (#229). So no rule names
a path that does not exist when the profile is built, and nothing is cleaned
up afterwards, because a sweep of .git cannot tell a synthetic file from one
Git or another dispatch created meanwhile.
Read-only common root. The common directory is read-only. Write access goes
only to its objects, refs, and logs directories and to the worktree's
private directory; those are also the only --add-dir roots. The read-only
root protects, without naming them, config, hooks, packed-refs,
commondir, gitdir, modules, other worktrees' pointers, and the main
worktree's HEAD, index, lock files, and merge, rebase, bisect, and sequencer
state. Rules then make read-only only what exists: the private commondir and
gitdir, config.worktree and its lock, the workspace's .git gitfile, and
the main worktree's logs/HEAD, refs/bisect, refs/worktree, and
refs/rewritten with their reflogs, which sit below the writable refs and
logs. Git follows commondir and gitdir to find configuration and hooks,
and the gitfile keeps a later dispatch's grants on this worktree, so all three
must also stay readable; the probe reads them. Lock files stay creatable inside
the writable directories, which is how Git updates refs.
What works and what does not. commit, fetch, push, and loose ref
updates work. Anything that must rewrite a root-level file fails inside the
sandbox: deleting a packed ref, pack-refs, garbage collection, and fetching
into a shallow clone, which updates shallow. Writing Codex publish dispatches
set GIT_CONFIG_PARAMETERS with gc.auto=0 and maintenance.auto=false so
automatic maintenance does not try. The common refs remain writable for
publication across branches; this filesystem boundary does not restrict ref
names, so stage instructions and GitHub branch protections remain part of the
publication boundary.
Routing away. A dangerous absent path cannot be protected without creating it, so before dispatch the runtime routes the stage to another eligible publisher, or stops that publishing operation with the reason when none is available, when:
extensions.worktreeConfigis on and the worktree'sconfig.worktreeorconfig.worktree.lockis absent;- the effective
core.hooksPath, read from local and worktree-specific configuration and resolved from the worktree's top level when relative, lies inside a writable Git directory. The worktree's privatehooksis not a trigger, because Git never reads it, and the default commonhookssits in the read-only root. A hooks path inside the writable source workspace stays outside these rules; - the stage would publish from the main worktree, whose index and HEAD live in the read-only common root. The cycle only grants Git metadata to linked worktrees, so this case arises only through a direct call.
Absent main-worktree refs/bisect, refs/worktree, refs/rewritten, and
their reflogs are accepted and left out.
Codex's own carve-outs. Codex itself protects .git, .codex, .agents,
and .aws below every writable root, and has been observed creating them as
empty directories in objects, refs, and logs. They come from the CLI
rather than from these rules, and Git ignores those dot-prefixed names in all
three directories.
Testing it. tests/test_codex_git_sandbox.py runs the real profile through
a fake codex that creates every missing rule destination, as the Linux
sandbox does, and compares .git before, during, and after the probe and the
dispatch. It also runs commit, fetch, push, and a loose ref update with
the common root made read-only by directory permissions. A live Codex CLI is
not available in CI. To check a supported CLI by hand on Linux, run a writing
publish stage from a linked worktree, then check that
find "$(git rev-parse --git-common-dir)" -newer <marker> lists nothing outside
objects, refs, logs, and the worktree's private directory, where <marker>
is a file touched just before the dispatch.
Before granting linked-worktree metadata, the runtime ignores Git directory,
worktree, index, and object-directory environment overrides. It also checks
that the workspace .git file, private gitdir and commondir pointers, and
Git's resolved paths agree and that the private directory is registered below
the selected common directory's worktrees directory.
Before dispatching any writing publish stage to Codex, the runtime runs a
deterministic local command with codex sandbox and the same filesystem rules.
It writes and verifies temporary markers in each granted root, checks that a
uniquely named marker cannot be created in the common root, worktrees, or
hooks, and that each existing protected file refuses writes, without a model
turn or network access. It never opens an absent protected name, and it removes
only markers that still carry its own token, after success, failure, timeout,
or interruption. A failed probe routes the stage to another eligible
publisher before dispatch and records the specific failure in the route reason.
Land issue #203 before issue #190, whose executor refactor reorganizes the
dispatch path this fix covers.
A host can change what an agent sees of a command's output. A command-rewriting
hook may compact git diff, test runs, or other output before the agent reads
it, and the only trace is a marker in the middle of the text. A review that
approves a shortened diff has not read the change it approved.
Measured on this repository with RTK 0.48, whose Claude Code hook turns
git diff into rtk git diff transparently (git diff b9dbc54 91ba1dd, the
PR #74 merge):
| Command | Lines |
|---|---|
git diff through the hook |
495, with markers such as ... (100 additions truncated) |
raw (rtk proxy git diff) |
764 |
git diff --output=<file> through the hook |
764 (the file is written by Git, unfiltered) |
In the same test, gh comment reads and exit codes were intact. The rule has two
layers:
-
The runtime supplies the diff. When
run_cycledispatchesreview,resolve, orrereview, it writesgit diff <base>...HEADitself, throughsubprocessand--output, which no host hook rewrites. The base is the first ofchange_bases_ofthat resolves, the same resolution the change signals use. The file goes in a fresh directory underevidence/, beside the telemetry database (CODE_CYCLE_HOME, or the user configuration directory). That is outside the workspace and Claude's disposable review clone. It is also outside the system temporary directory, which Codex'sworkspace-writesandbox may write. IfCODE_CYCLE_HOMEputs it inside the workspace,/tmp, or$TMPDIR, no file is written and the prompt says so. The prompt names the file, the base, merge base and head SHAs, the line count, and the SHA-256. The file is removed when the stage ends. When no base resolves, no file is written and the prompt says so; the stage never receives a partial file.How far the evidence is protected depends on the executor:
Executor Reads the file Can the stage change it? Codex, any stage Its sandbox reads the whole file system No; the operating system sandbox writes only its workspace and temporary directories Claude, non-writing stage Granted the directory with --add-dirNot prevented. Claude has no sandbox here, and an added directory is editable Claude, writing stage Not granted; falls back to the skill's rules Not granted, but not sandboxed either For every executor the file is read-only, and after the stage the runtime checks its SHA-256 again. A changed or missing file stops the cycle as
stage_not_completedbefore the stage's verdict is read. What this can't catch is a Claude stage that changes the file and restores it before it exits. For a review stage that matters, because the cycle acts on its verdict. No runtime can prove which bytes an agent read. For Codex the sandbox makes this case impossible. For Claude it is an accepted limit: Claude reviews remain trusted, with their capabilities unchanged and no secondary confirmation required. They are not runtime-verifiable against an edit to the evidence that is restored before the stage exits. -
The evidence skills state the rule. The eight evidence skills carry the shared
## Workspace tools and evidencesection, including its### Complete evidencesubsection, for manual runs and for evidence other than the diff. A truncation or summary marker, or output shorter than its own header counts, makes the output incomplete. Diffs are read from a file Git wrote, or through the host's raw mode. When complete output can't be obtained, the affected part is unverified, never checked.
This section decides whether output is complete. Evidence levels decides who produced it; the two don't overlap.
New reports use tests.version: 2 and tests.checks, replacing the legacy
top-level tests.evidence list. This is separate from the unchanged code-host
checks.items object. The enclosing result supplies head_sha.
{
"version": 2,
"conclusion": "verified",
"boundary": "not_required",
"checks": [{
"id": "configured-test", "command": "python3 -m unittest",
"executor": "codex", "profile": "deep_coder", "context": "project",
"required": true, "criteria": ["AC-1"], "level": "test",
"provenance": "project", "reason": "configured test command",
"head_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"result": "pass", "cause": "", "attempts": [],
"evidence": [{"exit_code": 0, "executed": 14}]
}]
}Every entry in a check's evidence and attempts arrays must be a JSON
object. A non-object entry (including a string) is malformed; the runtime
rejects the report and caps the conclusion at not_verified rather than
coercing text into evidence.
Freeze each check before execution. A result names the same check ID, command,
executor/profile and context. The runtime supplies frozen check identities to
later code stages. Copy that identity, including its owning executor/profile,
even when resolve or rereview is dispatched with another profile or executor.
A check requiring a different execution context gets a new ID and does not
replace the existing obligation. Required checks originate in project policy and
criterion links; an agent may promote, never demote. Record an unexecuted check
as result: unverified, with cause, evidence and actual recovery attempts;
do not invent an exit code. Results on another HEAD stay in history but cannot
prove the current change. An uncovered criterion remains open. Required failure
caps tests.conclusion at failed; required unverified or open criteria cap it
at not_verified. These caps never improve an agent conclusion.
Before implementation, the runtime reads the GitHub work-item body once per
cycle and parses one explicit criteria section. Accepted headings are exactly
Acceptance criteria and Criterios de aceptación, case insensitive, as a
Markdown heading (# through ######) or bold label. A trailing colon or
period is allowed inside a bold label or after it. The section is a flat list
at column zero using -, +, *, 1. or 1) markers and optional Markdown
checkboxes; another heading or a bold label ends it. Punctuated labels may
include prose on the same line, such as **Label.** text or **Label:** text.
Bold markup inside a list item does not end the section. Empty, missing,
duplicate, placeholder or unparseable sections produce deterministic needs_refinement
before any implementation, even with issue review off or difficulty 1. They
are insufficient specification, never a failed test. Existing issues are not
migrated. .github/ISSUE_TEMPLATE/work-item.md supplies the grammar for new work.
IDs are AC-n in order, scoped to the execution-plan revision and content hash.
Plane and Jira return not_evaluable with reason criteria reader unavailable.
Their cycles stop as BLOCKED with stop_reason: readiness_unconfirmed before
any agent dispatch, regardless of issue-review mode or difficulty. Editing the
work item cannot clear this stop: a provider reader must become available
(Plane reading belongs to #238-A; Jira needs its own reader). This differs from
parsed criteria needing refinement; neither unavailable nor unmet criteria
count as satisfied.
For one release, omitted tests.version or version 1 accepts tests.evidence.
The compatibility interval starts with the first published release supporting
version 2 and ends at the next published release. With runtime obligations,
the adapter matches exact commands and preserves missing required checks as
explicit unverified results; absence never creates a pass. Legacy standalone
reports retain their historical evidence classification. Version 2 evidence
is normalized into a private compatibility view for tests_basis,
boundary_verified, telemetry and stats; this view is not a second result
contract. Malformed or unsupported reports cap at not_verified.
Stages that omit tests or report tests.reason: no_code_change without outcome
fields record no new test outcome and preserve the previous cycle-level outcome.
Explicit reports (including checks, evidence, conclusion, passed or ran)
still receive the deterministic caps, even with no_code_change; a required
boundary run can still be recorded as missing for completed code work.
The native driver's final barrier prevents READY_FOR_MANUAL_MERGE while a
required check is unverified, criteria remain uncovered, or required security
verification was omitted. It stops once with HUMAN_INTERVENTION and
stop_reason: verification_unverified. Failed checks retain their failure;
blocked, omitted, missing-tool, inconclusive and environment causes remain
distinguishable metadata. CI_PASSED never turns an unverified local check into
pass. See Execution plan for persistence and receipt identity.
Resume with --from rereview --pr <n>. When the current HEAD, recoverable plan
and trusted receipt agree on a verification stop, the runtime dispatches only
cc-verify, once, in a detached disposable clone without remotes. It passes only
pending required checks and retains passing results. It does not dispatch the
rereview agent or republish a stage comment. A still-unavailable environment
stops again without a retry loop. A moved HEAD, changed criteria, unbound command
hash or unrecoverable plan takes ordinary reconciliation instead. This does not
add a CLI start point or change START_ORDER/CYCLE_STARTS.
Hotfix requires hotfix-reproduction and hotfix-regression, both required,
with distinct focused commands proposed by the implementer. The configured
general suite satisfies neither identity. A missing proposal stops with
needs_refinement; a defined check that cannot execute stays unverified.
The implementer commits the reproduction before the fix. Under the reproduction
check's history, it reports the defect failure's SHA, command, environment and
cause: defect, preserving its output artifact. Evidence carries test_sha256,
context_sha256 and output_sha256; the passing result on the fixed HEAD must
have the same test and environment fingerprints. An import or environment
failure is not a defect reproduction. Multiple results share one check ID;
only evidence for the current HEAD determines its current result. Changing the
test requires a new justified and revalidated proof, not replacing the old one.
Every recorded test outcome has one tests_basis token. The functional
conclusion is separate: it describes the agent's judgement, while the basis
says who produced the evidence that judgement rests on.
tests_basis |
Meaning | Example | Source |
|---|---|---|---|
claimed |
No executed command is attached | "tests": { "passed": true } |
The agent |
agent_reported |
The result includes a command, exit status, and count, as reported by the agent | pytest tests/test_x.py → exit 0, 14 run |
The agent; auditable in the comment, not proof |
runtime_observed |
The runtime observed the fact itself | Executor exit code, head SHA, or a command the runtime ran | The runtime |
externally_verified |
A system the agent does not control owns the fact | Checks on the pushed head SHA read by the runtime | The code host |
Evidence entries use level: static or level: test to describe the command;
these are command categories, not tests_basis values. cc-verify emits the
entries and one conclusion token: verified, verified_with_reservations,
not_verified, or failed. The conclusion is the agent's judgement and does
not raise its evidence level. Missing or contradictory evidence never becomes
a verified test outcome. Older telemetry rows without tests_basis are read
as claimed.
Some changes need one run where the code is actually used, not only a green
suite. cc-verify decides it with the security gate's shape:
boundary_required = deterministic_rule OR verifier_judgement
The rule fires on changed paths classified as api, auth, database, or
migrations, the same classes as the touches_* change signals
(stage_signals.boundary_rule_areas). The verifier may add CLI entry points,
subprocess and adapter code, provider integrations, and runtime configuration;
it cannot remove what the rule fired. cc-verify lists what counts as a
boundary run for each area, and boundary runs write only to the disposable
workspace.
A required boundary run appears as an evidence entry with level: boundary:
{
"conclusion": "verified",
"boundary": "required",
"evidence": [
{ "level": "test", "command": "python3 -m unittest tests.test_config", "exit_code": 0, "executed": 9 },
{ "level": "boundary", "command": "python3 -m app.cli sync --config config.yml --dry-run", "exit_code": 0, "executed": null }
]
}A non-zero boundary command fails the test outcome like any other command.
An expected failure, such as the malformed-configuration case, is recorded as
a command that checks the failure and exits 0 when it occurs. When the run is
required and no successful boundary entry exists, the runtime records
boundary_verified: false and caps a verified conclusion at
verified_with_reservations; the reason (environment, cost, or
out_of_scope) is listed under Not verified, where reviewers already read
unverified items and residual risk. When the driver observed the change, the
runtime applies the path rule itself, so a verifier that states
not_required for a migration still gets boundary_verified: false.
A stage that completed code work but reported no test outcome also records
boundary_verified: false when a run was required. Without a test report, a
review, a blocked stage, or a no_code_change resolution records nothing.
boundary: not_required records nothing new. The runtime never runs a
boundary check.
| Stage | Verifies |
|---|---|
cc-implement-issue |
The narrowest related tests first, then the repository's required verification through cc-verify |
cc-initial-review |
Reproduces actionable findings when the environment allows it. Findings are marked as observed at runtime or found by code inspection. |
cc-resolve-comments |
Reproduces each original finding, applies the fix, and confirms it no longer reproduces. Then cc-verify over the fixes and related regressions. |
cc-rereview |
Reproduces every finding claimed as fixed. Only those it could verify become resolved. |
Depth follows the change:
- Authorization: both the allowed and the denied case.
- API contracts: status, payload, relevant headers, and error responses.
- Frontend: the affected flow in a browser, with console and network checks, desktop and reduced viewports, and loading, empty, error, disabled, and success states.
- Persisted data: migration state is checked before tests run.
cc-verify and cc-run stop and ask instead of marking something "not verified" and continuing when:
- runtime configuration is missing (for example, only an
.env.exampleexists); - a required database or service is unavailable;
- a needed port is held by an unknown process;
- migrations are pending that could change the result;
- permission to run a needed command is missing.
When one of these could be cleared by creating something (a config from its template, a local database, a results directory), the skill describes exactly what it would create and asks first.
Before invoking cc-verify, the cycle skills preflight these conditions without changing state. If preflight fails, verification isn't attempted, and the reason is recorded. In a resolution thread that reads, for example:
Fix applied. Verification pending: environment unavailable (PostgreSQL on :5432 not running).
Stages that only verify or start services write their artefacts to a disposable workspace, never to the source tree. Reviews are read-only. See Role workspace policy.
cc-run starts services in dependency order and reports each one's process, PID, port, URL, and stop command. It never picks another port silently and never kills a process it didn't start.
Every review reports the checks the repository actually defines, by their real names, and every state that isn't a pass: pending, failed, cancelled, skipped, and missing. A check the repository normally runs that didn't run on this PR is a finding, not a pass. When a repository policy skips a suite (for example, end-to-end tests on draft PRs), the review states it. skipped is never reported as passed.
For GitHub the reviewers use:
gh pr view <n> --json isDraft,headRefName,baseRefName,labels,statusCheckRollup
gh pr checks <n> # output kept even on non-zero exit: it is evidenceA local pass isn't the change request's result. cc-implement-issue after it opens the PR, and cc-resolve-comments after it pushes a fix, wait for the checks of the head SHA they pushed, never an earlier run's, while any is queued or running. The wait is bounded by the repository's own timeout, or about 15 minutes when it states none. Their shared section Checks of the pushed head holds the rule.
| Checks of the pushed head | Result |
|---|---|
| every required check passed | the local result stands |
| failed within the stage's scope (its own code or tests) | the stage fixes it, pushes and waits again, at most two rounds unless the repository sets its own limit; these rounds aren't cycle iterations |
| failed outside the stage's scope (infrastructure, an unrelated flaky suite) | PARTIALLY_RESOLVED for a resolution, BLOCKED for an implementation, naming the check and why |
| still pending at the timeout | the local status stands, and every pending check is named; none is reported as passed |
Before pushing, the stage tests the way CI will when that's cheap: no reliance on the global Git identity, ~/.gitconfig, cached credentials, or anything else only the developer's machine has. The summary states which environment assumptions its local run shares with CI and which it doesn't.
The published summary lists the final head's checks by name and state with the SHA they ran on. The structured result carries them as checks: head_sha, the counts passed, failed (failed, cancelled, or expected but missing) and pending, and the named items.
When the runtime drives the cycle, each tests_passed value is stored beside
its tests_basis; the telemetry gate rejects a row that has one without the
other. A boolean passed without evidence is claimed. A well-formed evidence
list with at least one successful test entry and no failing command is
agent_reported. Missing or contradictory evidence fails closed. A stage that
reports ran: false records no test outcome. A justified no-code resolution
uses "tests": { "ran": false, "reason": "no_code_change" }, never
passed: true.
cc-stats groups outcomes by basis. It labels claimed as claimed and
agent_reported as reported; it uses “verified” for test outcomes only when
their basis is runtime_observed or externally_verified. Agent conclusion
tokens are shown separately from those evidence levels. It also shows the
boundary verification rate among the cycles that required a boundary run.
A verdict row also records checks_passed, checks_failed and checks_pending when the stage reported all three counts for its own head. Counts for a different head, or an incomplete set, record nothing. cc-stats sets each implementation and resolution head against its CI, so it can show how often a RESOLVED head was actually green. The check names stay in the PR comment. See Telemetry.
← Review lifecycle · ↑ Documentation index · Routing and models →