Skip to content

Inherited failures: fail fast on a red Base branch, or fix it with a Base fix - #226

Merged
JacobStephens2 merged 15 commits into
mainfrom
issue-221
Oct 1, 2026
Merged

JacobStephens2 merged 15 commits into
mainfrom
issue-221

Conversation

@JacobStephens2

@JacobStephens2 JacobStephens2 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Closes #221

Summary

A red check that is also red on the Base branch is an Inherited failure: thirdshift recognises it itself, before any Repair, from check names and conclusions only.

Repair loop, after CI goes red on the head
  split failed checks by name against the Base branch commit the head last merged in
    Failed on both                        -> Inherited failure
    pending, passed or missing on Base    -> the branch's own
  if the branch has failures of its own
    CI-fix Repair for those; Inherited failures listed as "don't fix"
  else (only Inherited failures)
    if the Base branch moved              -> merge it again (a Base move)
    else if a Base fix is allowed and the Run has had none
      open Base fix issue for the same Base branch and checks?
        yes -> wait for it to close        (or start it again, if its Run from here ended)
        no  -> write the issue (base-fix, ready-for-agent), start a child Merge run into the Base branch
      then merge the Base branch in and watch CI again
    else
      Failed run: CI red on <check>, which also fails on <base> at <short sha>; fix <base> first

A Base fix is allowed by base-fix, or base.fix = true in the User config (Setup asks, when merging is on); no-base-fix forbids it for one Run. The Base fix itself sees no Inherited failures, starts no Base fix and sends no Run notification.

src/
├── ci.rs          # watch() splits FailedChecks into own / inherited
├── base_fix.rs    # the Run's one Base fix: its issue, the look for an open one, the wait, its report
├── child_run.rs   # a child thirdshift: a Ticket's Run or a Base fix (moved out of spec_run.rs)
├── run.rs         # the Repair loop decides: Repair, Base move, Base fix, or fail
├── spec_run.rs    # Tickets get base-fix into the Spec branch; the Spec PR into the Base branch
├── args.rs, config.rs, questions.rs   # base-fix / no-base-fix, base.fix, Setup's question
└── prompt.rs      # CI-fix Repair prompt lists Inherited failures as not to fix

The Spec review added one thing that fell between the Tickets: a Ticket's Run sends no Run notification, so its Base fix was reported nowhere once the Ticket landed. A Run now ends its progress lines with Base fix: <issue URL> merged, the Spec run reads that back from a Ticket's Run, and the Ticket's line says so in the Tickets checklist and the Spec run's Run notification:

-#21 landed with https://github.com/acme/widgets/pull/1
+#21 landed with https://github.com/acme/widgets/pull/1, after Base fix https://github.com/acme/widgets/issues/22 merged

It also shares what the Tickets had each written: one check list (ci::check_list) for the Repair prompt and the Base fix issue, the Inherited failure cause written once, BaseFixAsk instead of a bare bool for what a child Run is asked, and one name for the base-fix word.

Seam under test: the black-box one, the real binary with the fake gh, fake claude and the Resend stand-in (tests/support::Scenario).

Evidence

  • Before: a_ticket_that_landed_after_a_base_fix_says_so_in_the_notification_and_the_checklist fails: the notification's Tickets section holds only #21 landed with https://github.com/acme/widgets/pull/1.
    After: it passes: the line ends , after Base fix https://github.com/acme/widgets/issues/22 merged, in the notification and in the Spec PR's checklist.
  • a_ticket_whose_red_check_also_fails_on_the_spec_branch_shows_the_cause_in_the_checklist_and_the_notification now also reads the Spec run's Run notification for the Inherited failure cause (story 15).
  • cargo test: every suite passes (503 tests, 70 in tests/spec_run.rs, 15 in tests/base_fix.rs, 20 in tests/ci.rs). cargo clippy --all-targets and cargo fmt --check are clean.
  • The CI tests from before the Spec (Declined CI fix, Base move, Repair cap) pass unchanged apart from one doc comment.

Merge Danger

Door: two-way

Default behavior changes only for a Run whose every red check is also red on the Base branch: it now fails at once, naming the checks, instead of spending a CI-fix Repair and ending as a Declined CI fix. Base fixes are off unless asked for. Reverting restores the old loop; a Base fix issue or a thirdshift-base-fix-<n>.lock file left behind is inert.

Blast Radius: Runs

  • With base-fix on, a Run writes an issue, adds the base-fix and ready-for-agent labels to the repository if it lacks them, and merges a fix into the Base branch unattended.
  • Waiting on a Base fix issue another clone opened has no time limit.
  • Every Run that took a Base fix prints one more progress line, Base fix: <issue URL> …, before its outcome.

Unaddressed findings

Standards

  • base_fix.rs re-fetches the Base branch's checks that ci::watch already read (Duplicated Code): the issue needs the Base branch's own check URLs, which FailedChecks does not carry; carrying them would widen the CI watcher's result for one caller that runs at most once per Run.
  • A Run forbidden a Base fix calls BaseFix::fix only to get its cause (locality, misleading name): the cause is now written once, but the decision stays in BaseFix, so the Repair loop asks one question and the cap, the ask and the cause stay behind one interface.
  • base_fix names an ask, a BaseFix and a report in main.rs and notification.rs (Mysterious Name): the child-Run bool is now a BaseFixAsk; the shadowing in main.rs follows how goal and email are resolved there.
  • The hidden argument to Kind mapping is in args.rs and its inverse in child_run.rs (Repeated Switches): two arms each, both on the constants in args.rs; a shared table would be more code than it saves.
  • child_run::Kind is imported bare, and Ended::Reached shares a name with run::Reached (Mysterious Name): readable at each use, and renaming touches four modules for no change in behavior.
  • issue, worktree, base, goal, base_fix, logs travel together, with three new too_many_arguments allows (Data Clumps): a real clump, but it predates the Spec (main had two); bundling it is its own refactor of run.rs and spec_run.rs.
  • The issue title is the key a shared Base fix is found by, and is parsed back apart (Primitive Obsession): the Spec asks for exactly that ("one whose title names the same Base branch and checks").
  • Taken { started, reached } is a pair of bools (Primitive Obsession): two independent facts, each read in one place.
  • The mark file's path is built in three places (Duplicated Code): mark_file names it once; the join with the common git directory is one line at each.
  • Test helpers checks_on_base / checks_on_origin / inherited_failure repeat across test files (Duplicated Code): per-file helpers are the convention in tests/, and their signatures differ with each file's scenario.
  • "Base branch moved while CI ran" is counted in both CI arms (Duplicated Code): two lines, with different things to do after each.
  • "launch repository" elsewhere in worktree.rs and branch.rs: fixed in the lines this Spec wrote; the rest is on main already, outside this diff.

Spec

  • A failed Base fix run again by hand (thirdshift merge <Base fix issue>) is not marked as a Base fix, so it sees its target check as an Inherited failure (story 27): the Spec defines the Base fix as the marked child Run, and the next Run to meet the open issue starts it again as one. Treating any issue labelled base-fix as a Base fix is worth its own issue.
  • Waiting on a Base fix issue from another clone has no limit (story 25): the Spec asks for the wait ("wait for it to close"), the cap of one Base fix per Run holds, and the README says how to end it.
  • A Base fix left open by a Run from this Launch directory is started again, which the Spec did not ask for: decided in the review of Runs share one Base fix #224, since waiting on an issue nobody is working on would hang; each Run still takes at most one.
  • gh label create for labels the repository lacks was not asked for: without it gh issue create --label fails on a repository that has never had a Base fix.
  • Mark files stay in .git after a failed Base fix: deliberate, they are how the next Run tells the Base fix is no longer running; one is removed when its Base fix merges.
  • No gh issue list --search (Testing Decisions): the open issues are listed by label and their titles matched by thirdshift, because GitHub's search index lags behind an issue just written, which is when Tickets look.
  • Tickets share a Base fix through a lock in the Launch directory's git directory and the issue, not in-process: Tickets' Runs are child processes; story 30 is met and tested.

Tickets

🤖 Generated with Claude Code

JacobStephens2 and others added 14 commits October 1, 2026 01:28
A failed check that also failed, under the same name, on the Base branch
commit the head last merged in is an Inherited failure (ADR 0008). The CI
watcher now splits the failed checks into the branch's own and Inherited
ones. A CI-fix Repair is given only the branch's own, with the Inherited
ones listed as not to fix. When only Inherited failures are left, the Run
merges the Base branch again if it moved, and otherwise fails naming the
checks, the Base branch and its commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tively, test a still-red moved Base branch

A check is an Inherited failure only if every check of its name on the Base
branch commit failed. Commit statuses and a Base branch that moved but is
still red are now covered through the binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inherited failures fail the Run without a Repair
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ks, name the child Run's kind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Base fix, when the base-fix flag is given
base.fix in the User config lets every Run start a Base fix with no flag;
no-base-fix forbids it for one Run, and contradicts base-fix. Setup asks
about Base fixes only with every Run a Merge run, and always writes the
setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… writes base.fix at its default with merging off, share the Spec run test setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base fix in the User config and Setup
…ch directory at a time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… left open, share the Launch directory lock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README.md: keep issue-221's wording that base-fix, no-base-fix or the User
config's base.fix holds for each Ticket's Run, with issue-224's shared Base
fix and its "One Base fix per Base branch" section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…list and Run notification, share the check list and the Inherited failure cause

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 marked this pull request as ready for review October 1, 2026 03:21
@JacobStephens2
JacobStephens2 merged commit dafff52 into main Oct 1, 2026
13 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-221 branch October 1, 2026 03:26
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.

Inherited failures: fail fast on a red Base branch, or fix it with a Base fix

1 participant