Skip to content

feat: Phase 3 Five-Alarm staged recovery - #4

Merged
9thLevelSoftware merged 3 commits into
mainfrom
feat/phase-3-five-alarm
Aug 5, 2026
Merged

feat: Phase 3 Five-Alarm staged recovery#4
9thLevelSoftware merged 3 commits into
mainfrom
feat/phase-3-five-alarm

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

Implements Phase 3: Five-Alarm staged recovery (design §8.2).

Behavior

  1. Gate — Escalation only after a current containment failure; historical risk alone is refused.
  2. Stage 1 — Intensified Firebreak: higher attempt budget, stricter wording.
  3. Stage 2 — Preserve Stage 1 candidate; select a different authorized model.
  4. Stage 3 — Clean-room: task/policy/failure summary only — no previous implementation code.
  5. Stage 4 — Verify all candidates; apply smallest verified; retain rejects for rollback.

Surface

  • FiveAlarmPlan state machine + audit timeline on RunRecord.five_alarm
  • RunOrchestrator::run_five_alarm
  • CLI: tif five-alarm --plan / tif five-alarm --run <id> [--apply]
  • Docs: ROADMAP (Phase 3 Done), CHANGELOG, protocol/README

Safety

  • Fail-safe preserved; multi-reviewer mock tests; historical-risk gate test

Test plan

  • cargo fmt --all
  • cargo clippy --workspace --all-features -- -D warnings
  • cargo test --workspace --all-features (131 tests)

Implement design §8.2 Five-Alarm state machine: gate on current
containment failure, intensified Firebreak, alternate model clean-room
(no prior implementation code), verify/rank all candidates, apply
smallest verified, retain rejects. CLI `tif five-alarm --run` +
orchestrator integration; tests per stage; ROADMAP/CHANGELOG.
Copilot AI lite review requested due to automatic review settings August 5, 2026 04:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f03f72d17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +841 to +844
|| run
.firebreak
.as_ref()
.is_some_and(|fb| !fb.applied && (!fb.success || !fb.candidate_ready));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep failed Firebreaks from satisfying the gate

When a run is already Contained and a previous manual Firebreak failed or produced no ready candidate, this clause makes current_failure true even though the current score/assessment still says the implementation is contained. That bypasses the Five-Alarm gate (“current containment failure required”) and can let tif five-alarm --run escalate and potentially auto-apply a recovery candidate for a contained run; gate this Firebreak outcome on an actual out-of-control score/assessment instead of treating any failed Firebreak as a containment failure.

Useful? React with 👍 / 👎.

Comment on lines +1005 to +1009
last_err_msg = e.to_string();
run.events.push(format!(
"five-alarm stage1 attempt {attempt}/{max_attempts} error: {e}"
));
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Try an alternate reviewer after Stage 1 errors

If the selected Stage 1 reviewer errors before returning a BackendGenerateResult (for example an uncompiled provider, missing credential, or context-build failure), this branch records only the message and retries the same reviewer; used_reviewer_ids remains empty, so Stage 2 select_excluding can select that same failing reviewer again instead of the configured alternate. In mixed reviewer pools this prevents Five-Alarm from reaching the different authorized model even though one is available, so record the attempted reviewer before continuing or exclude it on Stage 2 after repeated errors.

Useful? React with 👍 / 👎.

Comment on lines +1543 to +1546
if apply_outcome.requires_approval && apply_outcome.candidate_ready {
plan.stage = FiveAlarmStage::Complete;
plan.applied = false;
plan.original_preserved = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Five-Alarm state in sync after approval

For approval-required winners, the plan is persisted as Complete with applied = false, but the generic approve_firebreak path later applies the candidate without updating run.five_alarm or the winner candidate flags. Any run that requires approval will therefore keep an audit timeline saying the Five-Alarm candidate was only pending/not applied even after tif approve succeeds; defer marking the plan complete or update the stored Five-Alarm plan in the approval path.

Useful? React with 👍 / 👎.

@9thLevelSoftware
9thLevelSoftware merged commit e3de96a into main Aug 5, 2026
8 checks passed
.clone()
.filter(|s| Some(&s.id) == winner.isolation_session_id.as_ref())
})
.or_else(|| run.isolation_session.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The previous three filter callbacks politely decline the wrong session, and then the last or_else quietly hands the apply whatever session was lying around — the stage-3 clean-room tree, the run's last-known session, anything. You fixed up candidate_path afterwards, but session.id is what the audit/rollback story will read back, and you'll be telling future-you it applied fa-stage1-fa-primary-a1 when it actually painted the clean-room tree onto the worktree.

🩹 The Fix: Drop the final or_else(|| run.isolation_session.clone()) and let the Some(session) else { … } branch return the "winner selected but isolation session missing" abort you already wrote. If you genuinely want a fallback, only use it when the candidate path matches the winner's candidate_path byte-for-byte.

Suggested change
.or_else(|| run.isolation_session.clone());
.or_else(|| run.isolation_session.clone())
.filter(|s| {
winner
.candidate_path
.as_ref()
.is_some_and(|p| Some(&s.candidate_path.as_ref().unwrap_or(&PathBuf::new())) == Some(p))
});

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

impl Default for FiveAlarmRunOptions {
fn default() -> Self {
Self {
authorize_apply: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The default for FiveAlarmRunOptions is authorize_apply: true, user_approved: false. So FiveAlarmRunOptions::default() is secretly "skip the approval gate and apply me". Every test that does … ::default() (and there are several) is silently green-lighting an apply — and any future caller who forgets to set the bool gets the same free pass. This is the typedef cousin of DANGEROUS: true.

🩹 The Fix: Flip the default. authorize_apply: false is the safe baseline; let the CLI explicitly OR it with cfg.approval.auto_apply_firebreak (which it already does), and have test harnesses use the same opt-in.

Suggested change
authorize_apply: true,
authorize_apply: false,

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}

// Also enforce FireLevel gate for consistency.
let _ = FireLevel::escalate_to_five_alarm(true)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: let _ = FireLevel::escalate_to_five_alarm(true)?; — you already verified current_containment_failure is true thirty lines above, and now you're calling the same gate with a literal true and discarding the result. This is guarding the door with a mirror.

🩹 The Fix: Delete the line. The gate is enforced by FiveAlarmPlan::begin_escalation; calling it again with true adds nothing but a ? and a riddle for the next reader.

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Ok(plan)
}

fn push_timeline(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: push_timeline (private) and push_timeline_pub (public) are the same function with two names. The private one exists for the sole purpose of calling the public one. That's not encapsulation, that's a conjoined twin.

🩹 The Fix: Pick one name. Either inline push_timeline_pub into the two begin_escalation call sites and delete the private wrapper, or (cleaner) make push_timeline_pub private and rename it push_timeline. The orchestrator can call the same method as the gate — there's no API reason for the public/private split.

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


run.five_alarm = Some(plan.clone());
self.finalize_firebreak(config, run, apply_outcome.clone())?;
run.five_alarm = Some(plan.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: run.five_alarm = Some(plan.clone()) — and again one line after finalize_firebreak returns. The setter that wraps the work and then the setter that wraps the setter. finalize_firebreak doesn't touch five_alarm, so the second assignment is a no-op wearing a costume.

🩹 The Fix: Delete line 1609. The set on 1607 is the one that actually persists.

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review Roast 🔥

Verdict: Request changes | Recommendation: Address the session-id fallback and the authorize_apply default before merge; Ponytail nits are optional.

Overview

Severity Count
🚨 critical 0
⚠️ warning 2
💡 suggestion 0
🤏 nitpick 3
Issue Details (click to expand)
File Line Roast
crates/tif-core/src/orchestrator.rs 1489 Last or_else returns the latest session even when it doesn't match the winner — session.id ends up wrong in audit/rollback
crates/tif-core/src/firebreak.rs 1249 FiveAlarmRunOptions::default() sets authorize_apply: true — callers (and tests) silently bypass the approval gate
crates/tif-core/src/firebreak.rs 1124 let _ = FireLevel::escalate_to_five_alarm(true)?; is a redundant gate check with a literal true — dead code
crates/tif-core/src/firebreak.rs 1143 Private push_timeline just calls public push_timeline_pub — two names for the same function
crates/tif-core/src/orchestrator.rs 1609 run.five_alarm = Some(plan.clone()) set twice; second assignment is a no-op

🏆 Best part: The clean-room invariants are enforced in three places (context builder, marker assertion, and orchestrator's explicit prior_implementation_code = None). That's the kind of belt-and-suspenders I want around a "no prior patch content" rule — defense in depth, not paranoia.

💀 Worst part: The session.resolve chain in five_alarm_stage4. Three filter callbacks that correctly reject mismatched sessions, then a final or_else that shrugs and returns whatever the run last knew about. The candidate_path is patched up afterward, but session.id (and therefore the isolation_session_id that lands in the audit log and rollback pointer) is now lying about which tree was applied. That's the bug that bites a year from now when someone tries to roll back and finds the wrong tree.

📊 Overall: Like a fire drill that everyone runs perfectly except the last person who walks past the exit. The state machine, the gate, the clean-room mode, the multi-reviewer selection — all genuinely good. But the apply path has a "eh, whatever session" escape hatch that turns the audit trail into a fiction.

Files Reviewed (12 files)
  • CHANGELOG.md — 0 issues
  • README.md — 0 issues
  • crates/tif-core/src/firebreak.rs — 4 issues
  • crates/tif-core/src/lib.rs — 0 issues
  • crates/tif-core/src/orchestrator.rs — 2 issues
  • crates/tif-core/src/providers/context.rs — 0 issues
  • crates/tif-core/src/providers/mod.rs — 0 issues
  • crates/tif-core/src/reviewer.rs — 0 issues
  • crates/tif/src/cli.rs — 0 issues
  • crates/tif/src/main.rs — 0 issues
  • docs/ROADMAP.md — 0 issues
  • docs/protocol/v1.md — 0 issues

Correctness / Safety Findings

  • warning: crates/tif-core/src/orchestrator.rs:L1489: session fallback returns arbitrary latest session when winner's id doesn't match. candidate_path is patched, but session.id (audit/rollback key) ends up wrong. Required fix: drop the unconstrained final or_else; let the abort branch handle the missing session.
  • warning: crates/tif-core/src/firebreak.rs:L1249: FiveAlarmRunOptions::default() has authorize_apply: true with user_approved: false. Tests and future callers silently apply without approval. Required fix: default authorize_apply: false; let the CLI OR it with cfg.approval.auto_apply_firebreak (already done).
  • minor: crates/tif-core/src/firebreak.rs:L1124: let _ = FireLevel::escalate_to_five_alarm(true)?; is always-true dead code. Required fix: delete the line; the gate is already enforced by FiveAlarmPlan::begin_escalation.
  • minor: crates/tif-core/src/firebreak.rs:L1143: push_timeline (private) is a thin wrapper over push_timeline_pub. Required fix: pick one name and delete the other.
  • minor: crates/tif-core/src/orchestrator.rs:L1609: redundant run.five_alarm = Some(plan.clone()) after finalize_firebreak. Required fix: delete line 1609.

Ponytail Review

  • crates/tif-core/src/firebreak.rs:L1143: delete private push_timeline wrapper. Replace with direct calls to push_timeline_pub at lines 1119, 1126, 1134.
  • crates/tif-core/src/firebreak.rs:L1124: delete dead let _ = FireLevel::escalate_to_five_alarm(true)?; line. The gate is already enforced by begin_escalation.
  • crates/tif-core/src/orchestrator.rs:L1609: delete duplicate run.five_alarm = Some(plan.clone()) (line 1607 already persists it; finalize_firebreak doesn't clear it).
  • crates/tif/src/main.rs:L920 + crates/tif-core/src/orchestrator.rs:L876: reuse the CLI's Closed → OutOfControl reopen logic instead of duplicating it in run_five_alarm. Keep one source of truth.

Ponytail net: ~15 lines.

Suggested Minimal Patch

  1. firebreak.rs L1249: flip FiveAlarmRunOptions::default() to authorize_apply: false. Touches tests that use ::default(); pass authorize_apply: true explicitly where they want auto-apply.
  2. orchestrator.rs L1489: remove the unconstrained final or_else(|| run.isolation_session.clone()); let the existing Some(session) else { … } abort branch handle a missing winner session.
  3. firebreak.rs L1143: delete the private push_timeline wrapper; call push_timeline_pub directly from the three sites in begin_escalation.
  4. firebreak.rs L1124: delete the dead FireLevel::escalate_to_five_alarm(true)? line.
  5. orchestrator.rs L1609: delete the duplicate run.five_alarm = Some(plan.clone()) after finalize_firebreak.

Final Merge Guidance

Do not merge until the two warnings are fixed (the session-id fallback and the authorize_apply default). The Ponytail nits are optional cleanup.

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 78.1K · Output: 14.3K · Cached: 1.8M

@9thLevelSoftware
9thLevelSoftware deleted the feat/phase-3-five-alarm branch August 5, 2026 20: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.

2 participants