Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ All notable changes to this project are documented here. This project follows

### Added

- **Publication audit (#261).** After every publishing stage, whatever it
reported, `run_cycle.py` compares the cycle branch, the default branch and
the protected branches on `origin`, and the cycle's change requests, with
their state before the stage. A merged or closed change request, a
non-fast-forward move or deletion of the cycle branch, or any move or
deletion of the default or a protected branch stops the cycle with
`publication_violation` and the before/after evidence; an unreadable state
stops it with `publication_unverified`. Code hosts other than GitHub have
their refs audited and the rest reported `unverified`. Telemetry schema 24.
- **Manual rereview (#346).** `manual_rereview.py record` attaches a rereview
that ran outside the runtime to its pull request with a schema-2 receipt
marked `provenance: manual`, the authenticated operator, the confirmed head
Expand Down
18 changes: 14 additions & 4 deletions docs/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ Each outcome has one source row, listed in `telemetry.OUTCOME_FIELDS`:
| `cycle` | `boundary_verified` | the latest test report recorded it (schema 9) |
| `cycle` | `fallback_stages`, `contract_violations` | always: every dispatch of the run went through the recorder |
| `cycle` | `status`, `iterations` | always |
| `cycle` | `stop_reason` | the run closed through `run_cycle.py` (schema 7); `needs_refinement` and `readiness_unconfirmed` from schema 11; `interrupted` from schema 12; `ci_timeout`, `ci_unreadable`, `ci_not_passing`, and `head_moved` from schema 19; `manual_rereview_rejected` from schema 22; `worktree_dirty` and `worktree_diverged` when safe synchronization to an open pull request head is impossible |
| `cycle` | `stop_reason` | the run closed through `run_cycle.py` (schema 7); `needs_refinement` and `readiness_unconfirmed` from schema 11; `interrupted` from schema 12; `ci_timeout`, `ci_unreadable`, `ci_not_passing`, and `head_moved` from schema 19; `manual_rereview_rejected` from schema 22; `publication_violation` and `publication_unverified` from schema 24; `worktree_dirty` and `worktree_diverged` when safe synchronization to an open pull request head is impossible |
| `cycle` | `issue_review` | the cycle started at `implement` and was not continued: `off`, `skipped` (declared trivial and not security-sensitive), or `dispatched` (schema 11) |
| `cycle` | `continued` | the cycle continued an interrupted implementation with `--continue`; always `true` when present (schema 12) |
| `cycle` | `repeated_findings` | the ladder was evaluated at least once; the last value, so a survival the final rereview found is kept when no dispatch follows it (schema 7) |
Expand Down Expand Up @@ -707,9 +707,12 @@ than a completion, or only started work that finishes elsewhere),
needs a sensitive sandbox grant nobody answered; see *Sensitive sandbox grants*
in `routing.md`), `manual_rereview_rejected` (schema 22: the latest receipt is a
manual rereview a resume cannot close on; see *Manual rereview* in
`review-cycle.md`), `worktree_dirty`, or `worktree_diverged`. The last two stop
before changing files when a dirty tree or non-fast-forward prevents syncing an
open pull request head. A closing row from an earlier version has none, and
`review-cycle.md`), `publication_violation` (schema 24: a publication invariant
failed during a publishing stage, whoever caused it), `publication_unverified`
(schema 24: the audited refs or change-request state could not be read; see
*Publication audit* in `role-workspace-policy.md`), `worktree_dirty`, or
`worktree_diverged`. The last two stop before changing files when a dirty tree
or non-fast-forward prevents syncing an open pull request head. A closing row from an earlier version has none, and
`cc-stats` reads it as unknown. A stage row's `reroute_reason` may then be
`permission_required` or `permission_answered`.

Expand Down Expand Up @@ -767,6 +770,13 @@ its matching row. The additive table does not rewrite existing telemetry, and a
code rollback leaves unused rows. The `quota_resume_reason` cycle payload field
records when a continuation routes around a persisted exhaustion.

Schema 24 adds the stop reasons `publication_violation` and
`publication_unverified` of the [publication audit](role-workspace-policy.md#publication-audit).
They are payload-only. The audit's evidence is not stored in the telemetry
rows. The cycle journal holds one `publication_audit` result per publishing
stage and one `failure` row per breach, with the ref and its before and after
SHA. The full evidence is in the cycle report.

The routing rules' choice is the `profile` on each `dispatch` row. A later
selector's suggestion can be compared with it, and with the outcome, by
`(cycle_id, stage_seq)`, without that selector being installed while the run is
Expand Down
1 change: 1 addition & 0 deletions docs/review-cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ The loop stops early with `HUMAN_INTERVENTION`, before another resolution is dis
| No progress | a rereview reports the same `head_sha` and the same open finding set as the review before it | `no_progress` |
| Repeated findings | one `REV-xxx` ID survived a claimed fix twice | `repeated_findings` |
| Manual rereview not accepted | a resume's latest receipt is a [manual rereview](#manual-rereview) it cannot close on | `manual_rereview_rejected` |
| Publication invariant failed | during any publishing stage, a change request was merged or closed, the cycle branch moved non-fast-forward or was deleted, or the default or a protected branch moved ([Publication audit](role-workspace-policy.md#publication-audit)) | `publication_violation` |

Before the loop, an [issue review](#issue-review-before-implementation) can stop a new cycle as `needs_refinement` or `readiness_unconfirmed` without implementing anything.

Expand Down
65 changes: 63 additions & 2 deletions docs/role-workspace-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ equal to the expected commit. Before a success comment or receipt, the stage
also reads the PR using `gh` and confirms that head. The runtime independently
compares the reported SHA, local HEAD and current PR head before recording a
successful functional verdict. A failure/progress comment must state that the
push failed and cannot count as a successful resolution. The general invariant
audit for merges, force pushes and unrelated refs remains separate (#261).
push failed and cannot count as a successful resolution. The invariant audit
for merges, force pushes and deleted refs is described in
[Publication audit](#publication-audit) below.
The completion probe normalizes supported GitHub PR references (a number,
`#number`, `owner/repo#number`, or a GitHub `/pull/number` URL) to the numeric
identifier before checking the remote head.
Expand All @@ -115,6 +116,66 @@ same upload after a failure; it creates no new commit. On resume, reuse an
existing success publication rather than duplicating it. Bitbucket and Orca
transport recovery are not verified by this GitHub native path.

## Publication audit

The cycle never merges, force-pushes or deletes refs. A stage's command table
(#142) narrows what it can run, but code that its verification executes and
non-Bash tools that its host pre-approves can still reach the code host. So
`run_cycle.py` checks the result after every publishing stage (`implement`,
`review`, `resolve`, `rereview`), on every executor, whatever status the stage
reported (`scripts/publication_audit.py`):

1. **Change requests.** Every change request of the cycle that was open before
the stage is still open, and any change request the stage created is open.
A merge or a close is a breach. The audit reads the change request that the
stage was given, or for `implement` the one it reports, and every change
request whose head is the cycle branch.
2. **Cycle branch.** Its remote head did not move, or moved fast-forward from
the head recorded before the stage. A non-fast-forward move or a deletion
is a breach. `implement` may create the branch when it was absent. A commit
the checkout does not have is fetched into `FETCH_HEAD` only, so the audit
leaves no ref behind.
3. **Default and protected branches.** They did not move, and none was deleted.
Any move is a breach, even a fast-forward or a legitimate push by someone
else.

The audited refs are the cycle branch (the change request's head branch when
the runtime knows it, otherwise the checkout's branch), the default branch
(`code_cycle.repository.default_branch` when configured, otherwise the branch
`origin`'s `HEAD` names), and the branches the code host reports as protected.
A remote that has no branch yet has no default branch to audit; that part is
reported `unverified`. They are read once
before the stage, on `origin` only, and the same set is compared after it. A
push to any other branch never fails the check (#197).

The runtime cannot tell who moved a ref during a stage. A breach stops the
cycle with `publication_violation` whoever caused it, and the stop line says
"changed outside the stage?" instead of blaming the stage. `publication_violation`
means that an invariant failed, not that the stage caused it. The audit runs
before the stage's report is read, so a breach wins over a `BLOCKED` or any
other `stage_not_completed`. No later stage is dispatched. The evidence goes to
the cycle report (`CycleReport.publication_audit`) and the cycle journal. It
holds the before and after SHA of each audited ref, the before and after state
of each change request, and for each breach its kind (`change_request_merged`,
`change_request_closed`, `non_fast_forward`, `protected_ref_moved`,
`ref_deleted`) with up to 20 commits added and removed when they are available.

A state that cannot be read is never read as empty. If `origin`'s refs, its
default branch, the protected branches or a change request cannot be read, the
cycle stops with `publication_unverified`. Before the stage, nothing is
dispatched. After it, the stage's report is not accepted.

On GitHub the change requests and protected branches are read through `gh api`.
Other code hosts, Bitbucket included, have no reader until #238. For those, the
cycle branch and the default branch are still audited through Git, and the
change-request state and protected branches are reported `unverified` in the
cycle report, never as passed. A `--local-only` run publishes nothing and is
not audited.

Reconciling a stage that published a verified result but reported `BLOCKED` is
#364. Until it exists, a `BLOCKED` result is never turned into a success, even
when the SHAs match.

---

[← Optional workspace tools](workspace-tools.md) · [↑ Documentation index](README.md) · [Host permissions →](host-permissions.md)
Loading
Loading