Skip to content

feat(controlplane): fence and record CI repair rounds (CI repair, U2) - #14

Merged
Steel-tech merged 5 commits into
feat/ci-watchfrom
feat/ci-repair-ledger
Sep 28, 2026
Merged

Steel-tech merged 5 commits into
feat/ci-watchfrom
feat/ci-repair-ledger

Conversation

@Steel-tech

Copy link
Copy Markdown
Contributor

Stacked on #13. Base is feat/ci-watch; merge #13 first and GitHub retargets this to main.

What

U2 of the CI repair plan: the control plane's ledger of repair rounds. A round is a fix pushed to the attempt's own branch after CI went red. Like every publish side effect, it is authorized before the push and recorded after it, both under the lease token.

Rounds get their own table (migration 005_publish_ci_repairs.sql). publish_records keeps one row per step by design, and a second push is a new fact, not a replay.

Rules the ledger enforces

Rule Refusal code
The frozen definition must declare publish.ci.on_fail ci_repair_not_declared
proof must be recorded first publish_step_out_of_order
No round once CI is recorded green ci_repair_after_green
Rounds are numbered 1, 2, … in order ci_repair_out_of_order
No more rounds than the frozen budget ci_repair_budget_exhausted
A round repairs exactly the head jig last pushed (proof ref for round 1, the previous head_after after that), so a person's push in between is refused, not built on ci_repair_head_mismatch
The round must push a new head; failed check names are capped (20 × 200 bytes) invalid_ci_repair_*
Replaying identical values returns the stored round; different values conflict ci_repair_conflict
Lease and branch fencing, same as publish steps lease_not_owner, publish_branch_mismatch

R6, when a job counts as accepted: CI may not be recorded green on any head a round found red. This is refused when the ci row is written, with publish_ci_on_repaired_head, and CompleteAttempt repeats the check as a backstop. The green head does not have to be the last one jig pushed, so a person's fix after the rounds run out is still accepted by the publish retry.

Other changes:

  • The worker client gains AuthorizeCIRepair, RecordCIRepair and AttemptCIRepairs.
  • attemptWaitsForCI becomes attemptDefinition, which returns the parsed spec, because both rules read it.

⚠️ Still inert

Nothing in the worker calls the new endpoints until the round loop lands (U5). Behaviour on main is unchanged: red CI ends the attempt accepted_unpublished.

Verification

  • just check is green: format, vet, package boundary, definitions, full tests, build.
  • The store tests cover the chain through two rounds, every refusal at both the authorize and record halves, foreign and expired leases, replays and conflicts, and R6 both when the ci row is written and in the backstop.
  • A test runs the client against the real HTTP handler: authorize, record, replay as Completed, over-budget refusal over the wire, and listing rounds.
  • Mutation-checked: I broke 22 rules and routes one at a time (15 ledger rules, 2 route paths, then 5 more after review), and every break made the tests fail.
  • Independent review found one serious bug, fixed in 1c695fd: with R6 checked only at CompleteAttempt, a bad ci row could leave a job permanently unable to be accepted, because the row can't be replaced and no further rounds are allowed. It also found two test gaps, both fixed. Checked and clean: _txlock=immediate serializes the read-then-insert, and the primary key catches races.

Plan updates

  • R6 now requires the ci row check when the row is written; KTD7 notes the control plane enforces the head chain.
  • U2 is updated to match the build.
  • U5 records two traps: the last pushed head has to come from AttemptCIRepairs, because RetryPublish doesn't return rounds; and an authorization marked Completed does not compare head_before.

🤖 Generated with Claude Code

Steel-tech and others added 5 commits September 27, 2026 20:29
A definition with publish.ci.on_fail may push fixes to its own branch
after CI goes red. Each fix is a round, authorized before the push and
recorded after it under the lease token, in a new publish_ci_repairs
table (migration 005): publish_records keeps one row per step by design,
and a second push is a new fact, not a replay.

The ledger enforces what makes a round trustworthy: the frozen
definition must declare on_fail, proof must be recorded, CI must not yet
be green, rounds are numbered in order within the frozen budget, and a
round repairs exactly the head jig last pushed, so a person's push in
between is refused rather than built on. Identical replays return the
stored round; different values conflict.

CompleteAttempt now refuses accepted when the green ci record names a
head any round found red. It does not require the last pushed head, so
a person's fix after the rounds run out stays acceptable on retry.

The worker client gains the three calls; nothing in the worker calls
them until the round loop (U5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rded

A ci record is permanent, and no round is allowed once it exists, so a
green record naming a head a repair round found red left the job unable
ever to be accepted: CompleteAttempt refused it, and neither the ci row
nor the rounds could move. RecordPublishStep now refuses that record, and
CompleteAttempt keeps the rule as a backstop. Tests now cover two rounds
and a replay that changes only head_before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 64479e81-958b-4a3b-b939-a5cb72f52383

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Steel-tech
Steel-tech merged commit 0fab5c3 into feat/ci-watch Sep 28, 2026
5 checks passed
Steel-tech added a commit that referenced this pull request Sep 28, 2026
Land CI repair U2 + U3 on main (merged into stacked bases by #14/#15)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant