feat(recovery): route failed write-less stages to recovery instead of futile retry

Adds the failure-ticket + recovery-routing mechanism: a deterministic
gate->capability table gates whether a stage has agency to fix its own
failure, opens a FailureTicketOpenedEvent when it doesn't, and routes to
a metadata role=recovery stage (per-stage budget, cap 2, not reset by
TransitionExecuted) instead of retrying in place. Extends salvage
decisions with a RECOVER option so the review-gate judge can also route
to recovery, unifies deterministic-gate and review-gate routing through
routeToRecovery(), and has ExecutionPlanCompiler synthesize a
write-capable recovery stage + edge for freestyle plans. Read-only tools
(file_read, list_dir) are now always available on any tool-granting
stage so a recovery stage can inspect the write-less stage's failure
without flooding context via shell ls -R.
This commit is contained in:
2026-07-08 10:28:53 +04:00
parent d6bada6f10
commit f51a8dada4
21 changed files with 325 additions and 29 deletions
+11 -1
View File
@@ -225,7 +225,11 @@ func (m Model) handleKey(k keyMsg) (tea.Model, tea.Cmd) {
// On the idle launcher, Tab cycles the launch target (chat → workflows → chat) in any
// edit mode, so it works whether or not the input is focused.
if m.displayState() == StateIdle && k.Type == keyTab {
m.cycleLauncherWf()
if k.Shift {
m.cycleLauncherWfBack()
} else {
m.cycleLauncherWf()
}
return m, nil
}
if m.editMode == ModeInsert {
@@ -1290,6 +1294,12 @@ func (m *Model) cycleLauncherWf() {
m.launcherWf = (m.launcherWf + 1) % (len(m.workflows) + 1)
}
// cycleLauncherWfBack is cycleLauncherWf in reverse, for Shift+Tab.
func (m *Model) cycleLauncherWfBack() {
n := len(m.workflows) + 1
m.launcherWf = (m.launcherWf - 1 + n) % n
}
// cycleRightPanel advances the in-session right panel: events → changes → off → events.
func (m *Model) cycleRightPanel() {
m.rightPanel = (m.rightPanel + 1) % 3