oc_chat
Loading...
Searching...
No Matches
executors.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/round/executors.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_round_*_executor.py"
6"""
7Per-phase executors for the eight round.* operation types.
8
9Purpose
10-------
11Implement the idempotent-first executors that drive each round-close
12phase: open, merge, test, lint, consolidate, ff_main, cleanup, abort.
13
14Responsibilities
15----------------
16- Perform the git side effect for a phase via the injectable GitShim.
17- Check "already done" before doing work so every step is resumable.
18- Write exactly one operation-log row per phase per plan via the seam.
19- Surface operator-actionable stops as RoundEscalation.
20
21Diagnostics
22-----------
23Domain: OC_CHAT.MAIN
24Levels:
25 L2 - phase lifecycle
26 L3 - idempotency and git details
27 L4 - deep tracing
28
29Contracts
30---------
31- Executors never write round_job_steps; the state machine owns that.
32- Executors are pure with respect to the registry except for the audit row.
33- A no-op (already-done) executor returns ``status='skipped'``.
34
35Dependencies
36------------
37- oc_diagnostics (structured debug logging via _dbg)
38- oc_chat.registry.api (Registry facade)
39- oc_chat.registry.round.git_shim (GitShim - the injectable git seam for each phase's side effect)
40- oc_chat.registry.round.conflict (ConflictPolicy and ConflictStrategy - auto-resolve or abort on a merge conflict)
41- oc_chat.registry.round.escalation (RoundEscalation, make_escalation and resume_command_for - operator-actionable stops)
42- oc_chat.registry.canonical (canonical_json - the test and lint input hashes)
43- oc_chat.registry.errors (ManualMergeRequiredError and RoundInvariantError)
44- oc_chat.registry.roadmap.audit (verify_roadmap_files_unchanged - the roadmap hand-edit check at cleanup)
45"""
46from __future__ import annotations
47
48from dataclasses import dataclass, field
49import hashlib
50from typing import Callable, Sequence
51
52from oc_diagnostics import _dbg
53
54from oc_chat.registry.api import Registry
55from oc_chat.registry.canonical import canonical_json
56from oc_chat.registry.errors import (
57 ManualMergeRequiredError,
58 RoundInvariantError,
59)
60from oc_chat.registry.round.conflict import ConflictPolicy, ConflictStrategy
62 EscalationCode,
63 RoundEscalation,
64 make_escalation,
65 resume_command_for,
66)
67from oc_chat.registry.round.git_shim import GitShim
68from oc_chat.registry.roadmap.audit import verify_roadmap_files_unchanged
69
70
71_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
72
73
74@dataclass(frozen=True)
76 """A plan to merge in a round, with its branch and worktree."""
77
78 plan_id: str
79 branch: str
80 worktree: str
81 merge_order: int
82
83
84@dataclass(frozen=True)
86 """Outcome of a full-suite test run."""
87
88 passed: bool
89 count_before: int | None = None
90 count_after: int | None = None
91 duration_seconds: float | None = None
92 artifact_paths: tuple[str, ...] = field(default_factory=tuple)
93 plan_results: tuple[dict[str, object], ...] = field(default_factory=tuple)
94
95
96@dataclass(frozen=True)
98 """Outcome of an ``oct lint --profile strict`` run."""
99
100 passed: bool
101 errors: int = 0
102 warnings: int = 0
103 artifact_paths: tuple[str, ...] = field(default_factory=tuple)
104
105
106@dataclass(frozen=True)
108 """Result of executing one round-close phase."""
109
110 step_name: str
111 status: str
112 post_step_head_sha: str | None = None
113 input_hash: str | None = None
114 result_summary: str | None = None
115 error_summary: str | None = None
116 artifact_paths: tuple[str, ...] = field(default_factory=tuple)
117
118
119TestRunner = Callable[["RoundContext"], TestRunResult]
120LintRunner = Callable[["RoundContext"], LintRunResult]
121RoadmapBumper = Callable[["RoundContext"], str | None]
122
123
124@dataclass
126 """Shared inputs for the round-close executors."""
127
128 registry: Registry
129 git: GitShim
130 round_no: int
131 job_id: str
132 actor_slot: str
133 staging_branch: str
134 plan_specs: list[PlanSpec]
135 actor_type: str = "orchestrator_agent"
136 main_branch: str = "main"
137 base_ref: str = "main"
138 conflict_policy: ConflictPolicy = field(default_factory=ConflictPolicy)
139 test_runner: TestRunner | None = None
140 lint_runner: LintRunner | None = None
141 roadmap_bumper: RoadmapBumper | None = None
142 roadmap_dir: str | None = None
143 allow_untracked_roadmap_edits: bool = False
144 app_version: str | None = None
145 telemetry: dict[str, object] | None = None
146
147
148def _staging_head(ctx: RoundContext) -> str:
149 return ctx.git.rev_parse(ctx.staging_branch)
150
151
152def _hash_inputs(*parts: object) -> str:
153 return hashlib.sha256(canonical_json(list(parts)).encode("utf-8")).hexdigest()
154
155
156def run_open(ctx: RoundContext) -> StepOutcome:
157 """Open the round: one ``round.open`` audit row per plan (SF-001)."""
158
159 func = "run_open"
160 round_obj = ctx.registry.backend.get_round(ctx.round_no)
161 if round_obj is not None and str(round_obj.get("status")) in {"closed", "aborted"}:
162 raise RoundEscalation(
163 make_escalation(
164 EscalationCode.ROUND_STATE,
165 round_no=ctx.round_no,
166 phase="open",
167 message=f"round {ctx.round_no} is {round_obj.get('status')}",
168 )
169 )
170 for spec in sorted(ctx.plan_specs, key=lambda s: s.merge_order):
171 existing = ctx.registry.backend.get_round_plan(ctx.round_no, spec.plan_id)
172 if existing is not None:
173 _dbg("OC_CHAT.MAIN", func, f"plan {spec.plan_id} already open", 3)
174 continue
175 ctx.registry.round_open(
176 round_no=ctx.round_no,
177 orchestrator_slot=ctx.actor_slot,
178 plan_id=spec.plan_id,
179 merge_order=spec.merge_order,
180 actor_slot=ctx.actor_slot,
181 actor_type=ctx.actor_type,
182 )
183 if not ctx.plan_specs and round_obj is None:
184 ctx.registry.round_open(
185 round_no=ctx.round_no,
186 orchestrator_slot=ctx.actor_slot,
187 actor_slot=ctx.actor_slot,
188 actor_type=ctx.actor_type,
189 )
190 _dbg("OC_CHAT.MAIN", func, f"round {ctx.round_no} open", 2)
191 return StepOutcome(
192 step_name="open",
193 status="done",
194 result_summary=f"opened round {ctx.round_no} with {len(ctx.plan_specs)} plans",
195 )
196
197
198def run_merge(ctx: RoundContext, spec: PlanSpec) -> StepOutcome:
199 """Merge one plan branch into staging (idempotent on merge_commit_sha)."""
200
201 func = "run_merge"
202 plan_row = ctx.registry.backend.get_round_plan(ctx.round_no, spec.plan_id)
203 if plan_row is not None and plan_row.get("merge_commit_sha"):
204 _dbg("OC_CHAT.MAIN", func, f"plan {spec.plan_id} merged already", 2)
205 return StepOutcome(
206 step_name=f"merge:{spec.plan_id}",
207 status="skipped",
208 post_step_head_sha=_staging_head(ctx),
209 result_summary=f"{spec.plan_id} already merged",
210 )
211 ctx.git.checkout(ctx.staging_branch)
212 base_sha = ctx.git.rev_parse(ctx.staging_branch)
213 result = ctx.git.merge(
214 spec.branch,
215 no_ff=True,
216 message=f"[round-{ctx.round_no}] merge {spec.plan_id}",
217 )
218 if not result.ok:
219 self_resolved = _try_resolve_conflict(ctx, spec, result.conflicted_paths)
220 if not self_resolved:
221 ctx.git.merge_abort()
222 raise RoundEscalation(
223 make_escalation(
224 EscalationCode.MERGE_CONFLICT,
225 round_no=ctx.round_no,
226 phase="merge",
227 message=(
228 f"merge of {spec.plan_id} conflicted on "
229 f"{', '.join(result.conflicted_paths) or 'unknown'}"
230 ),
231 resume_command=resume_command_for(ctx.round_no),
232 details={
233 "plan_id": spec.plan_id,
234 "conflicted_paths": list(result.conflicted_paths),
235 },
236 )
237 )
238 head_sha = _staging_head(ctx)
239 else:
240 head_sha = result.head_sha
241 ctx.registry.round_merge(
242 round_no=ctx.round_no,
243 plan_id=spec.plan_id,
244 merge_order=spec.merge_order,
245 merge_commit_sha=head_sha,
246 base_sha=base_sha,
247 staging_branch=ctx.staging_branch,
248 merge_base_sha=base_sha,
249 actor_slot=ctx.actor_slot,
250 actor_type=ctx.actor_type,
251 )
252 _dbg("OC_CHAT.MAIN", func, f"merged {spec.plan_id} -> {head_sha[:8]}", 2)
253 return StepOutcome(
254 step_name=f"merge:{spec.plan_id}",
255 status="done",
256 post_step_head_sha=head_sha,
257 result_summary=f"merged {spec.plan_id}",
258 )
259
260
262 ctx: RoundContext,
263 spec: PlanSpec,
264 conflicted_paths: Sequence[str],
265) -> bool:
266 func = "_try_resolve_conflict"
267 decision = ctx.conflict_policy.classify(conflicted_paths)
268 if decision.strategy is ConflictStrategy.ABORT:
269 if decision.blocking_paths:
270 _dbg("OC_CHAT.MAIN", func, "blocking conflict", 2)
271 return False
272 if not decision.resolvable_paths:
273 return False
274 for path in decision.resolvable_paths:
275 ctx.git.checkout_path("current", path)
276 try:
277 ctx.git.run("commit", "--no-edit")
278 except Exception as exc: # pragma: no cover - defensive
280 round_no=ctx.round_no,
281 plan_id=spec.plan_id,
282 conflicted_paths=tuple(conflicted_paths),
283 ) from exc
284 _dbg("OC_CHAT.MAIN", func, f"auto-resolved {len(decision.resolvable_paths)}", 2)
285 return True
286
287
288def run_test(ctx: RoundContext, prior_input_hash: str | None = None) -> StepOutcome:
289 """Run the full suite; idempotent on the staging+args input hash."""
290
291 func = "run_test"
292 staging_sha = _staging_head(ctx)
293 plan_ids = sorted(spec.plan_id for spec in ctx.plan_specs)
294 input_hash = _hash_inputs("test", staging_sha, plan_ids)
295 if prior_input_hash is not None and prior_input_hash == input_hash:
296 _dbg("OC_CHAT.MAIN", func, "test input unchanged; skip", 2)
297 return StepOutcome(
298 step_name="test",
299 status="skipped",
300 post_step_head_sha=staging_sha,
301 input_hash=input_hash,
302 result_summary="tests already green for this staging head",
303 )
304 if ctx.test_runner is None:
306 round_no=ctx.round_no,
307 message="no test_runner configured for round.test",
308 )
309 result = ctx.test_runner(ctx)
310 if not result.passed:
311 raise RoundEscalation(
312 make_escalation(
313 EscalationCode.TEST_FAILED,
314 round_no=ctx.round_no,
315 phase="test",
316 message="full test suite failed on staging",
317 resume_command=resume_command_for(ctx.round_no),
318 details={"artifact_paths": list(result.artifact_paths)},
319 )
320 )
321 ctx.registry.round_test(
322 round_no=ctx.round_no,
323 test_count_before=result.count_before,
324 test_count_after=result.count_after,
325 input_hash=input_hash,
326 result="pass",
327 duration_seconds=result.duration_seconds,
328 plan_results=list(result.plan_results),
329 artifact_paths=list(result.artifact_paths),
330 actor_slot=ctx.actor_slot,
331 actor_type=ctx.actor_type,
332 )
333 _dbg("OC_CHAT.MAIN", func, "tests passed", 2)
334 return StepOutcome(
335 step_name="test",
336 status="done",
337 post_step_head_sha=staging_sha,
338 input_hash=input_hash,
339 result_summary=f"tests passed ({result.count_after})",
340 artifact_paths=tuple(result.artifact_paths),
341 )
342
343
344def run_lint(ctx: RoundContext, prior_input_hash: str | None = None) -> StepOutcome:
345 """Run strict lint over staging; idempotent on the staging+args hash."""
346
347 func = "run_lint"
348 staging_sha = _staging_head(ctx)
349 input_hash = _hash_inputs("lint", staging_sha)
350 if prior_input_hash is not None and prior_input_hash == input_hash:
351 _dbg("OC_CHAT.MAIN", func, "lint input unchanged; skip", 2)
352 return StepOutcome(
353 step_name="lint",
354 status="skipped",
355 post_step_head_sha=staging_sha,
356 input_hash=input_hash,
357 result_summary="lint already clean for this staging head",
358 )
359 if ctx.lint_runner is None:
361 round_no=ctx.round_no,
362 message="no lint_runner configured for round.lint",
363 )
364 result = ctx.lint_runner(ctx)
365 if not result.passed:
366 raise RoundEscalation(
367 make_escalation(
368 EscalationCode.LINT_FAILED,
369 round_no=ctx.round_no,
370 phase="lint",
371 message=f"strict lint failed: {result.errors} errors",
372 resume_command=resume_command_for(ctx.round_no),
373 details={
374 "errors": result.errors,
375 "warnings": result.warnings,
376 "artifact_paths": list(result.artifact_paths),
377 },
378 )
379 )
380 ctx.registry.round_lint(
381 round_no=ctx.round_no,
382 staging_sha=staging_sha,
383 errors=result.errors,
384 warnings=result.warnings,
385 input_hash=input_hash,
386 result="pass",
387 artifact_paths=list(result.artifact_paths),
388 actor_slot=ctx.actor_slot,
389 actor_type=ctx.actor_type,
390 )
391 _dbg("OC_CHAT.MAIN", func, "lint passed", 2)
392 return StepOutcome(
393 step_name="lint",
394 status="done",
395 post_step_head_sha=staging_sha,
396 input_hash=input_hash,
397 result_summary=f"lint clean ({result.warnings} warnings)",
398 artifact_paths=tuple(result.artifact_paths),
399 )
400
401
402def run_consolidate(ctx: RoundContext) -> StepOutcome:
403 """Record the staging HEAD as final_commit_sha; flip to committed."""
404
405 func = "run_consolidate"
406 round_obj = ctx.registry.backend.get_round(ctx.round_no)
407 final_sha = _staging_head(ctx)
408 if round_obj is not None and str(round_obj.get("status")) == "committed":
409 _dbg("OC_CHAT.MAIN", func, "already committed; skip", 2)
410 return StepOutcome(
411 step_name="consolidate",
412 status="skipped",
413 post_step_head_sha=final_sha,
414 result_summary="round already committed",
415 )
416 ctx.registry.round_consolidate(
417 round_no=ctx.round_no,
418 final_commit_sha=final_sha,
419 actor_slot=ctx.actor_slot,
420 actor_type=ctx.actor_type,
421 )
422 _dbg("OC_CHAT.MAIN", func, f"committed {final_sha[:8]}", 2)
423 return StepOutcome(
424 step_name="consolidate",
425 status="done",
426 post_step_head_sha=final_sha,
427 result_summary=f"consolidated at {final_sha[:8]}",
428 )
429
430
431def run_ff_main(ctx: RoundContext) -> StepOutcome:
432 """Fast-forward main to the round's final commit (rejects non-FF)."""
433
434 func = "run_ff_main"
435 round_obj = ctx.registry.backend.get_round(ctx.round_no)
436 if round_obj is None or not round_obj.get("final_commit_sha"):
438 round_no=ctx.round_no,
439 message="ff_main requires a recorded final_commit_sha",
440 )
441 target = str(round_obj["final_commit_sha"])
442 current_main = ctx.git.rev_parse(ctx.main_branch)
443 if current_main == target:
444 _dbg("OC_CHAT.MAIN", func, "main already at target; skip", 2)
445 return StepOutcome(
446 step_name="ff_main",
447 status="skipped",
448 post_step_head_sha=target,
449 result_summary="main already fast-forwarded",
450 )
451 if not ctx.git.is_ancestor(current_main, target):
453 round_no=ctx.round_no,
454 message=f"main {current_main[:8]} is not an ancestor of {target[:8]}",
455 )
456 new_head = ctx.git.fast_forward(ctx.main_branch, target)
457 ctx.registry.round_ff_main(
458 round_no=ctx.round_no,
459 from_sha=current_main,
460 to_sha=new_head,
461 actor_slot=ctx.actor_slot,
462 actor_type=ctx.actor_type,
463 )
464 ctx.git.checkout(ctx.staging_branch)
465 _dbg("OC_CHAT.MAIN", func, f"main -> {new_head[:8]}", 2)
466 return StepOutcome(
467 step_name="ff_main",
468 status="done",
469 post_step_head_sha=_staging_head(ctx),
470 result_summary=f"main fast-forwarded to {new_head[:8]}",
471 )
472
473
474def run_cleanup(ctx: RoundContext) -> StepOutcome:
475 """Delete plan branches/worktrees, bump roadmap, close round when clean."""
476
477 func = "run_cleanup"
478 round_obj = ctx.registry.backend.get_round(ctx.round_no)
479 if round_obj is not None and str(round_obj.get("status")) == "closed":
480 _dbg("OC_CHAT.MAIN", func, "round already closed; skip", 2)
481 return StepOutcome(
482 step_name="cleanup",
483 status="skipped",
484 post_step_head_sha=_staging_head(ctx),
485 result_summary="round already closed",
486 )
488 branches_deleted: list[str] = []
489 worktrees_removed: list[str] = []
490 failures: list[str] = []
491 for spec in ctx.plan_specs:
492 if spec.worktree:
493 if ctx.git.worktree_remove(spec.worktree, force=True):
494 worktrees_removed.append(spec.worktree)
495 elif _worktree_present(ctx, spec.worktree):
496 failures.append(f"worktree {spec.worktree}")
497 if spec.branch and ctx.git.branch_exists(spec.branch):
498 if ctx.git.branch_delete(spec.branch, force=True):
499 branches_deleted.append(spec.branch)
500 else:
501 failures.append(f"branch {spec.branch}")
502 roadmap_version: str | None = None
503 roadmap_failed = False
504 if not failures and ctx.roadmap_bumper is not None:
505 try:
506 roadmap_version = ctx.roadmap_bumper(ctx)
507 except Exception as exc: # noqa: BLE001 - surfaced as escalation
508 roadmap_failed = True
509 failures.append(f"roadmap bump: {exc}")
510 if failures:
511 ctx.registry.round_cleanup(
512 round_no=ctx.round_no,
513 outcome="failed",
514 branches_deleted=branches_deleted,
515 worktrees_removed=worktrees_removed,
516 error_summary="; ".join(failures),
517 actor_slot=ctx.actor_slot,
518 actor_type=ctx.actor_type,
519 )
520 code = (
521 EscalationCode.ROADMAP_BUMP_FAILED
522 if roadmap_failed
523 else EscalationCode.CLEANUP_FAILED
524 )
525 raise RoundEscalation(
526 make_escalation(
527 code,
528 round_no=ctx.round_no,
529 phase="cleanup",
530 message="; ".join(failures),
531 resume_command=f"oct agent round cleanup {ctx.round_no}",
532 details={
533 "branches_deleted": branches_deleted,
534 "worktrees_removed": worktrees_removed,
535 },
536 )
537 )
538 ctx.registry.round_cleanup(
539 round_no=ctx.round_no,
540 outcome="clean",
541 branches_deleted=branches_deleted,
542 worktrees_removed=worktrees_removed,
543 roadmap_version=roadmap_version,
544 app_version=ctx.app_version,
545 actor_slot=ctx.actor_slot,
546 actor_type=ctx.actor_type,
547 )
548 _dbg("OC_CHAT.MAIN", func, f"closed round {ctx.round_no}", 2)
549 return StepOutcome(
550 step_name="cleanup",
551 status="done",
552 post_step_head_sha=_staging_head(ctx),
553 result_summary=(
554 f"deleted {len(branches_deleted)} branches, "
555 f"{len(worktrees_removed)} worktrees"
556 ),
557 )
558
559
560def _audit_roadmap_hand_edits(ctx: RoundContext) -> None:
561 if ctx.roadmap_dir is None:
562 return
563 result = verify_roadmap_files_unchanged(ctx.registry, ctx.roadmap_dir)
564 if result.ok:
565 return
566 drift = [item.to_dict() for item in result.drift]
567 if ctx.allow_untracked_roadmap_edits:
568 ctx.registry.execute_domain_command(
569 "audit.override",
570 ctx.actor_slot,
571 {
572 "version": 1,
573 "kind": "roadmap_untracked_edit",
574 "round_no": ctx.round_no,
575 "roadmap_dir": ctx.roadmap_dir,
576 "drift": drift,
577 },
578 actor_type=ctx.actor_type,
579 related_round=ctx.round_no,
580 )
581 return
582 first = result.drift[0]
583 raise RoundEscalation(
584 make_escalation(
585 EscalationCode.ROADMAP_UNTRACKED_EDIT,
586 round_no=ctx.round_no,
587 phase="cleanup",
588 message=(
589 f"untracked ROADMAP edit in {first.source_file}: "
590 f"{first.kind} {first.item_id}"
591 ),
592 resume_command=f"oct agent round cleanup {ctx.round_no}",
593 details={
594 "roadmap_dir": ctx.roadmap_dir,
595 "files": result.files,
596 "drift": drift,
597 },
598 )
599 )
600
601
602def _worktree_present(ctx: RoundContext, worktree: str) -> bool:
603 result = ctx.git.run("worktree", "list", "--porcelain")
604 return worktree.replace("\\", "/") in result.stdout.replace("\\", "/")
605
606
607def run_abort(ctx: RoundContext, *, reason: str, drop_staging: bool = True) -> StepOutcome:
608 """Abort the round: return plans to ready_for_merge and drop staging."""
609
610 func = "run_abort"
611 ctx.registry.round_abort(
612 round_no=ctx.round_no,
613 reason=reason,
614 actor_slot=ctx.actor_slot,
615 actor_type=ctx.actor_type,
616 )
618 if drop_staging and ctx.git.branch_exists(ctx.staging_branch):
619 ctx.git.checkout(ctx.base_ref)
620 ctx.git.branch_delete(ctx.staging_branch, force=True)
621 _dbg("OC_CHAT.MAIN", func, f"aborted round {ctx.round_no}", 2)
622 return StepOutcome(
623 step_name="abort",
624 status="done",
625 result_summary=f"aborted: {reason}",
626 )
627
628
629def run_rollback(ctx: RoundContext, *, reason: str) -> StepOutcome:
630 """Roll back a diverged round: drop staging without touching plans."""
631
632 func = "run_rollback"
633 ctx.registry.round_abort(
634 round_no=ctx.round_no,
635 reason=reason,
636 actor_slot=ctx.actor_slot,
637 actor_type=ctx.actor_type,
638 )
639 if ctx.git.branch_exists(ctx.staging_branch):
640 ctx.git.checkout(ctx.base_ref)
641 ctx.git.branch_delete(ctx.staging_branch, force=True)
642 _dbg("OC_CHAT.MAIN", func, f"rolled back round {ctx.round_no}", 2)
643 return StepOutcome(
644 step_name="rollback",
645 status="done",
646 result_summary=f"rolled back: {reason}",
647 )
648
649
650def _reset_plans_to_ready(ctx: RoundContext) -> None:
651 func = "_reset_plans_to_ready"
652 connect_func = getattr(ctx.registry.backend, "connect")
653 with connect_func() as conn:
654 rows = conn.execute(
655 "SELECT p.plan_id AS plan_id, p.status AS status "
656 "FROM round_plans rp JOIN plans p ON p.plan_id = rp.plan_id "
657 "WHERE rp.round_no = ?",
658 (ctx.round_no,),
659 ).fetchall()
660 for row in rows:
661 current = str(row["status"])
662 if current == "ready_for_merge":
663 continue
664 ctx.registry.plan_set_status(
665 plan_id=str(row["plan_id"]),
666 from_status=current,
667 to_status="ready_for_merge",
668 source="round_lifecycle",
669 note=f"restored from aborted round {ctx.round_no}",
670 actor_slot=ctx.actor_slot,
671 actor_type=ctx.actor_type,
672 )
673 _dbg("OC_CHAT.MAIN", func, f"reset {len(rows)} plans", 3)
str _hash_inputs(*object parts)
Definition executors.py:152
None _audit_roadmap_hand_edits(RoundContext ctx)
Definition executors.py:560
StepOutcome run_cleanup(RoundContext ctx)
Definition executors.py:474
bool _try_resolve_conflict(RoundContext ctx, PlanSpec spec, Sequence[str] conflicted_paths)
Definition executors.py:265
StepOutcome run_lint(RoundContext ctx, str|None prior_input_hash=None)
Definition executors.py:344
StepOutcome run_abort(RoundContext ctx, *, str reason, bool drop_staging=True)
Definition executors.py:607
str _staging_head(RoundContext ctx)
Definition executors.py:148
None _reset_plans_to_ready(RoundContext ctx)
Definition executors.py:650
StepOutcome run_test(RoundContext ctx, str|None prior_input_hash=None)
Definition executors.py:288
StepOutcome run_open(RoundContext ctx)
Definition executors.py:156
StepOutcome run_ff_main(RoundContext ctx)
Definition executors.py:431
StepOutcome run_consolidate(RoundContext ctx)
Definition executors.py:402
bool _worktree_present(RoundContext ctx, str worktree)
Definition executors.py:602
StepOutcome run_merge(RoundContext ctx, PlanSpec spec)
Definition executors.py:198
StepOutcome run_rollback(RoundContext ctx, *, str reason)
Definition executors.py:629