engine.py file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:2 fix(muse): use rev-parse for muse-only branch status · · Sep 22, 2026
1 """Governance Hygiene Agent orchestration (§9A-5, §GSW write-path order, §GSB reconcile)."""
2
3 from __future__ import annotations
4
5 import json
6 import re
7 from dataclasses import dataclass, replace
8 from datetime import date, datetime, timezone
9 from pathlib import Path
10
11 from adapters.base import VcsAdapter
12 from adapters.config import OverseerConfig
13 from adapters.errors import ReadError, WriteError
14 from adapters.runner import CommandRunner, quote_arg
15 from cli.atomic import WriteFailure, atomic_write_text
16 from tools.governance_hygiene.drift import detect_drift
17 from tools.governance_hygiene.patch import (
18 build_handover_patches,
19 build_roadmap_patches,
20 extract_paste_ready_block,
21 )
22 from tools.governance_hygiene.reads import ReadFailure, perform_verified_reads
23 from tools.governance_hygiene.realign import execute_realign_guard, plan_realign
24 from tools.governance_gates import scan_governance_gates
25 from tools.governance_gates.format import format_pending_gate_lines
26 from tools.cost_awareness.format import format_cost_awareness_lines
27 from tools.cost_awareness.surface import build_cost_awareness_report
28 from tools.verification_evidence_gate import (
29 build_verification_evidence_gate,
30 format_verification_evidence_gate_line,
31 )
32 from tools.independent_second_reviewer import (
33 build_independent_second_reviewer_gate,
34 format_independent_second_reviewer_gate_line,
35 )
36 from tools.adversarial_freeze import (
37 build_adversarial_freeze_gate,
38 format_adversarial_freeze_gate_line,
39 )
40 from tools.governance_hygiene.types import DriftReport, GovernanceSyncResult, PatchPlan, VerifiedReads
41 from tools.workspace import workspace_relay_footer_state
42
43 GOVERNANCE_SYNC_MARKER = "last_governance_sync"
44
45
46 def _emit_governance_gate_footer(
47 config: OverseerConfig,
48 repo_root: Path,
49 *,
50 handover_text: str,
51 roadmap_text: str,
52 emit,
53 kit_root: Path | None = None,
54 ) -> tuple[int | None, str]:
55 """Append §KH1.9 gate reminders, §PC.7 spend-awareness, and §MR.8 workspace_relay.
56
57 Returns ``(exit_code_or_None, workspace_relay_state)``.
58 """
59 if config.governance_gates.remind:
60 if "governance-sync" in config.governance_gates.surfaces:
61 result = scan_governance_gates(
62 config,
63 repo_root,
64 handover_text=handover_text,
65 roadmap_text=roadmap_text,
66 )
67 emit("")
68 for line in format_pending_gate_lines(result):
69 emit(line)
70
71 if (
72 config.cost_awareness.enabled
73 and "governance-sync" in config.cost_awareness.surfaces
74 and kit_root is not None
75 ):
76 cost_report = build_cost_awareness_report(
77 config,
78 repo_root,
79 kit_root=kit_root,
80 handover_text=handover_text,
81 roadmap_text=roadmap_text,
82 )
83 if cost_report.exit_code == 31:
84 emit("")
85 emit(cost_report.violation or "routing policy file missing or unreadable")
86 relay_state = workspace_relay_footer_state(config, repo_root)
87 emit("")
88 emit(f"workspace_relay: {relay_state}")
89 return 31, relay_state
90 emit("")
91 for line in format_cost_awareness_lines(cost_report):
92 emit(line)
93
94 verification_gate = build_verification_evidence_gate(
95 config,
96 repo_root,
97 handover_text=handover_text,
98 roadmap_text=roadmap_text,
99 )
100 ve_line = format_verification_evidence_gate_line(verification_gate)
101 if ve_line:
102 emit("")
103 emit(ve_line)
104 if (
105 not verification_gate.ok
106 and verification_gate.mode == "require"
107 and verification_gate.token == "missing_verification_evidence"
108 ):
109 relay_state = workspace_relay_footer_state(config, repo_root)
110 emit("")
111 emit(f"workspace_relay: {relay_state}")
112 return 2, relay_state
113
114 isr_gate = build_independent_second_reviewer_gate(
115 config,
116 repo_root,
117 handover_text=handover_text,
118 roadmap_text=roadmap_text,
119 )
120 isr_line = format_independent_second_reviewer_gate_line(isr_gate)
121 if isr_line:
122 emit("")
123 emit(isr_line)
124 if isr_gate.remediation:
125 emit(isr_gate.remediation)
126 if (
127 not isr_gate.ok
128 and isr_gate.mode == "require"
129 and isr_gate.token == "missing_independent_second_review"
130 ):
131 relay_state = workspace_relay_footer_state(config, repo_root)
132 emit("")
133 emit(f"workspace_relay: {relay_state}")
134 return 2, relay_state
135
136 aff_gate = build_adversarial_freeze_gate(
137 config,
138 repo_root,
139 handover_text=handover_text,
140 roadmap_text=roadmap_text,
141 )
142 aff_line = format_adversarial_freeze_gate_line(aff_gate)
143 if aff_line:
144 emit("")
145 emit(aff_line)
146 if (
147 not aff_gate.ok
148 and aff_gate.mode == "require"
149 and aff_gate.token == "missing_adversarial_freeze"
150 ):
151 relay_state = workspace_relay_footer_state(config, repo_root)
152 emit("")
153 emit(f"workspace_relay: {relay_state}")
154 return 2, relay_state
155
156 relay_state = workspace_relay_footer_state(config, repo_root)
157 emit("")
158 emit(f"workspace_relay: {relay_state}")
159 if relay_state not in {"not_configured", "ok"}:
160 emit(
161 "multi-repo SD-17 incomplete until ok workspace check-next exits 0 "
162 "(refresh relay tips; no peer writes from this command)"
163 )
164 return None, relay_state
165
166
167 def run_governance_sync(
168 config: OverseerConfig,
169 repo_root: Path,
170 adapter: VcsAdapter,
171 runner: CommandRunner,
172 *,
173 dry_run: bool = True,
174 lane: str | None = None,
175 all_lanes: bool = False,
176 emit,
177 kit_root: Path | None = None,
178 ) -> GovernanceSyncResult:
179 """Execute governance-sync; default dry-run is inert (§7)."""
180 if all_lanes and lane is not None:
181 return GovernanceSyncResult(
182 exit_code=2,
183 dry_run=dry_run,
184 reads=None,
185 drift=None,
186 plan=None,
187 committed=False,
188 commit_sha=None,
189 messages=("cannot use --lane with --all-lanes",),
190 )
191
192 if all_lanes:
193 return _run_all_lanes(
194 config,
195 repo_root,
196 adapter,
197 runner,
198 dry_run=dry_run,
199 emit=emit,
200 kit_root=kit_root,
201 )
202
203 lane_name = lane
204 if config.docs.lanes is not None and lane is None:
205 lane_name = config.docs.default_lane
206 return _run_single_lane(
207 config,
208 repo_root,
209 adapter,
210 runner,
211 lane=lane_name,
212 dry_run=dry_run,
213 skip_missing=False,
214 emit=emit,
215 kit_root=kit_root,
216 )
217
218
219 def _run_all_lanes(
220 config: OverseerConfig,
221 repo_root: Path,
222 adapter: VcsAdapter,
223 runner: CommandRunner,
224 *,
225 dry_run: bool,
226 emit,
227 kit_root: Path | None = None,
228 ) -> GovernanceSyncResult:
229 """Sync every configured lane; skip lanes with missing doc files (§K8)."""
230 if config.docs.lanes is None:
231 lane_names: tuple[str | None, ...] = (None,)
232 else:
233 lane_names = tuple(sorted(config.docs.lanes.keys()))
234
235 last_result: GovernanceSyncResult | None = None
236 for lane_name in lane_names:
237 name_label = lane_name if lane_name is not None else "default"
238 emit(f"lane: {name_label}")
239 result = _run_single_lane(
240 config,
241 repo_root,
242 adapter,
243 runner,
244 lane=lane_name,
245 dry_run=dry_run,
246 skip_missing=True,
247 emit=emit,
248 kit_root=kit_root,
249 )
250 last_result = result
251 if result.exit_code == 4:
252 emit(f"skipping lane {name_label}: missing governance doc(s)")
253 continue
254 if result.exit_code != 0:
255 return result
256 if last_result is None:
257 return GovernanceSyncResult(
258 exit_code=0,
259 dry_run=dry_run,
260 reads=None,
261 drift=None,
262 plan=None,
263 committed=False,
264 commit_sha=None,
265 messages=("no lanes configured",),
266 )
267 return last_result
268
269
270 def _run_single_lane(
271 config: OverseerConfig,
272 repo_root: Path,
273 adapter: VcsAdapter,
274 runner: CommandRunner,
275 *,
276 lane: str | None,
277 dry_run: bool,
278 skip_missing: bool,
279 emit,
280 kit_root: Path | None = None,
281 ) -> GovernanceSyncResult:
282 """Sync one handover + roadmap pair."""
283 from adapters.config import resolve_lane_docs
284 from cli.docs_paths import lane_living_doc_abs
285
286 try:
287 lane_docs = resolve_lane_docs(config, lane)
288 except Exception as exc:
289 from adapters.errors import ConfigError
290
291 if isinstance(exc, ConfigError):
292 return GovernanceSyncResult(
293 exit_code=2,
294 dry_run=dry_run,
295 reads=None,
296 drift=None,
297 plan=None,
298 committed=False,
299 commit_sha=None,
300 messages=(str(exc),),
301 )
302 raise
303
304 handover_path = lane_living_doc_abs(repo_root, config, lane_docs, lane_docs.handover)
305 roadmap_path = lane_living_doc_abs(repo_root, config, lane_docs, lane_docs.roadmap)
306
307 for path in (handover_path, roadmap_path):
308 if not path.is_file():
309 if skip_missing:
310 return GovernanceSyncResult(
311 exit_code=4,
312 dry_run=dry_run,
313 reads=None,
314 drift=None,
315 plan=None,
316 committed=False,
317 commit_sha=None,
318 messages=(f"missing governance doc: {path.name}",),
319 )
320 return GovernanceSyncResult(
321 exit_code=4,
322 dry_run=dry_run,
323 reads=None,
324 drift=None,
325 plan=None,
326 committed=False,
327 commit_sha=None,
328 messages=(f"missing governance doc: {path.name}",),
329 )
330
331 reads = perform_verified_reads(config, adapter, runner, repo_root=repo_root)
332 if isinstance(reads, ReadFailure):
333 emit(f"read failed [{reads.regime}]: {reads.command}")
334 emit(reads.message)
335 return GovernanceSyncResult(
336 exit_code=2,
337 dry_run=dry_run,
338 reads=None,
339 drift=None,
340 plan=None,
341 committed=False,
342 commit_sha=None,
343 messages=(reads.message,),
344 error_command=reads.command,
345 )
346
347 handover_text = handover_path.read_text(encoding="utf-8")
348 roadmap_text = roadmap_path.read_text(encoding="utf-8")
349 drift = detect_drift(reads, handover_text, roadmap_text)
350
351 if drift.any_unreadable:
352 emit("drift detection unreadable — fail-closed")
353 return GovernanceSyncResult(
354 exit_code=2,
355 dry_run=dry_run,
356 reads=reads,
357 drift=drift,
358 plan=None,
359 committed=False,
360 commit_sha=None,
361 messages=("unreadable drift field",),
362 )
363
364 if drift.fully_aligned:
365 emit("governance-sync: aligned (D1–D3)")
366 _write_sync_marker(repo_root, reads)
367 footer_code, workspace_relay = _emit_governance_gate_footer(
368 config,
369 repo_root,
370 handover_text=handover_text,
371 roadmap_text=roadmap_text,
372 emit=emit,
373 kit_root=kit_root,
374 )
375 return GovernanceSyncResult(
376 exit_code=footer_code or 0,
377 dry_run=dry_run,
378 reads=reads,
379 drift=drift,
380 plan=None,
381 committed=False,
382 commit_sha=None,
383 messages=("aligned",),
384 workspace_relay=workspace_relay,
385 )
386
387 realign_planned, _ = plan_realign(config, adapter, reads, drift)
388 realign_summary, realign_error = execute_realign_guard(
389 config,
390 adapter,
391 reads,
392 drift,
393 dry_run=True,
394 )
395 if realign_error:
396 emit(f"realign dry-run failed: {realign_error}")
397 return GovernanceSyncResult(
398 exit_code=2,
399 dry_run=dry_run,
400 reads=reads,
401 drift=drift,
402 plan=None,
403 committed=False,
404 commit_sha=None,
405 messages=(realign_error,),
406 error_command=realign_error,
407 )
408
409 # §GSP.6.1: roadmap patches first so handover NEXT regen sees D3-reconciled queue.
410 patched_roadmap, roadmap_sections = build_roadmap_patches(
411 roadmap_text,
412 reads,
413 drift,
414 )
415 patched_handover, handover_sections, next_regen_token = build_handover_patches(
416 handover_text,
417 reads,
418 drift,
419 realign_summary=realign_summary,
420 config=config,
421 roadmap_text=patched_roadmap,
422 repo_root=repo_root,
423 )
424 sections = handover_sections + roadmap_sections
425
426 feature_branch = _feature_branch_name(config)
427 main_sha = reads.r1_github_main_sha or reads.r3_canonical_main_sha or reads.r2_anchor_sha
428 commit_message = _commit_message(main_sha, drift, sections, realign_summary)
429 pr_url = _build_pr_url(runner, repo_root, config, feature_branch)
430
431 plan = PatchPlan(
432 handover_text=patched_handover,
433 roadmap_text=patched_roadmap,
434 patched_sections=sections,
435 realign_planned=realign_planned,
436 realign_reason=realign_summary,
437 feature_branch=feature_branch,
438 commit_message=commit_message,
439 pr_url=pr_url,
440 )
441
442 emit(f"drift: D1={drift.d1_handover_vs_git} D2={drift.d2_anchor_vs_canonical} D3={drift.d3_queue_vs_merged}")
443 emit(f"planned sections: {', '.join(sections)}")
444 emit(next_regen_token)
445 if realign_summary:
446 emit(realign_summary)
447 if dry_run and "land-b" in next_regen_token:
448 # §PMHF.3.4 rule 4: dry-run shows the planned land-b body.
449 land_b_block = extract_paste_ready_block(patched_handover)
450 if land_b_block:
451 emit(land_b_block)
452 if dry_run:
453 d1_d2_aligned = (
454 drift.d1_handover_vs_git == "aligned"
455 and drift.d2_anchor_vs_canonical == "aligned"
456 )
457 if d1_d2_aligned:
458 _write_sync_marker(repo_root, reads)
459 emit(
460 "dry-run: no governance-doc writes, commits, or realign apply "
461 "(may stamp local .overseer/last_governance_sync when D1/D2 aligned)"
462 )
463 else:
464 emit("dry-run: no writes, commits, or realign apply")
465 if pr_url:
466 emit(f"docs-only PR URL (operator-gated): {pr_url}")
467 footer_code, workspace_relay = _emit_governance_gate_footer(
468 config,
469 repo_root,
470 handover_text=handover_text,
471 roadmap_text=roadmap_text,
472 emit=emit,
473 kit_root=kit_root,
474 )
475 return GovernanceSyncResult(
476 exit_code=footer_code or 0,
477 dry_run=True,
478 reads=reads,
479 drift=drift,
480 plan=plan,
481 committed=False,
482 commit_sha=None,
483 messages=("dry-run plan",),
484 workspace_relay=workspace_relay,
485 )
486
487 return _apply_plan(
488 config=config,
489 repo_root=repo_root,
490 adapter=adapter,
491 runner=runner,
492 reads=reads,
493 drift=drift,
494 plan=plan,
495 handover_path=handover_path,
496 roadmap_path=roadmap_path,
497 emit=emit,
498 kit_root=kit_root,
499 handover_text=handover_text,
500 roadmap_text=roadmap_text,
501 )
502
503
504 def _apply_plan(
505 *,
506 config: OverseerConfig,
507 repo_root: Path,
508 adapter: VcsAdapter,
509 runner: CommandRunner,
510 reads: VerifiedReads,
511 drift: DriftReport,
512 plan: PatchPlan,
513 handover_path: Path,
514 roadmap_path: Path,
515 emit,
516 kit_root: Path | None = None,
517 handover_text: str = "",
518 roadmap_text: str = "",
519 ) -> GovernanceSyncResult:
520 """Apply patches, commit, and push on a feature branch.
521
522 Frozen §GSW.3.1 order: capture original branch state → realign on the
523 original branch → ensure feature branch (dual-HEAD under
524 ``muse+git-mirror``, §GSW.5.1) → write doc patches → commit → sync
525 marker (only after successful commit, §GSW.3.4) → push. Any failure
526 after the feature-branch switch restores docs + marker + original
527 branch (§GSW.4.2).
528
529 §GSB amends step C only: a C0 reconcile of the dated sync-branch name
530 (fast-forward ancestor tips / deterministic ``-N`` uniquify) runs before
531 the C1 dual-HEAD ensure, so a same-day second ``--write`` never checks
532 out a stale tip into the shared working tree (§GSB.3.1–§GSB.3.4).
533 """
534 original_handover = handover_path.read_text(encoding="utf-8")
535 original_roadmap = roadmap_path.read_text(encoding="utf-8")
536 marker_path = repo_root / ".overseer" / GOVERNANCE_SYNC_MARKER
537 prior_marker = (
538 marker_path.read_text(encoding="utf-8") if marker_path.is_file() else None
539 )
540
541 def _failure(exit_code: int, message: str, error_command: str | None) -> GovernanceSyncResult:
542 return GovernanceSyncResult(
543 exit_code=exit_code,
544 dry_run=False,
545 reads=reads,
546 drift=drift,
547 plan=plan,
548 committed=False,
549 commit_sha=None,
550 messages=(message,),
551 error_command=error_command,
552 )
553
554 # Step A (§GSW.4.1): capture branch identity before any mutation.
555 branch_state, capture_command = _capture_branch_state(config, adapter, runner, repo_root)
556 if branch_state is None:
557 emit(f"branch capture failed: {capture_command}")
558 return _failure(2, f"branch capture failed: {capture_command}", capture_command)
559
560 # Step B (§GSW.3.1): realign guard on the original branch — before any
561 # feature-branch switch and before any doc write.
562 realign_summary, realign_error = execute_realign_guard(
563 config,
564 adapter,
565 reads,
566 drift,
567 dry_run=False,
568 )
569 if realign_error:
570 emit(f"realign failed: {realign_error}")
571 return _failure(2, realign_error, realign_error)
572
573 def _rollback(*, restore_docs: bool) -> None:
574 _rollback_apply(
575 config=config,
576 repo_root=repo_root,
577 adapter=adapter,
578 runner=runner,
579 handover_path=handover_path,
580 roadmap_path=roadmap_path,
581 original_handover=original_handover,
582 original_roadmap=original_roadmap,
583 marker_path=marker_path,
584 prior_marker=prior_marker,
585 branch_state=branch_state,
586 restore_docs=restore_docs,
587 emit=emit,
588 )
589
590 # Step C0 (§GSB.3.2–§GSB.3.3): reconcile the dated sync-branch name on
591 # every applicable history before any checkout of that name. Ancestor or
592 # equal tips → fast-forward without checkout-as-FF; any diverged history
593 # → deterministic -N uniquify of the shared name.
594 reconciled, reconcile_error = _reconcile_feature_branch(
595 adapter, runner, repo_root, config, plan.feature_branch, branch_state
596 )
597 if reconcile_error is not None:
598 _rollback(restore_docs=False)
599 emit(reconcile_error)
600 return _failure(2, reconcile_error, reconcile_error)
601 if reconciled != plan.feature_branch:
602 # §GSB.3.3: PatchPlan is frozen — replace it so the success-path
603 # commit, push, and result plan all observe the reconciled branch
604 # and a pr_url rebuilt for that branch.
605 plan = replace(
606 plan,
607 feature_branch=reconciled,
608 pr_url=_build_pr_url(runner, repo_root, config, reconciled),
609 )
610 emit(f"governance-sync branch uniquified: {reconciled}")
611
612 # Step C1 (§GSW.5): feature branch must exist and hold current HEAD(s)
613 # before any handover/roadmap patch write.
614 checkout = _ensure_feature_branch(
615 adapter, runner, repo_root, config, plan.feature_branch, branch_state
616 )
617 if checkout is not None:
618 _rollback(restore_docs=False)
619 emit(checkout)
620 return _failure(2, checkout, checkout)
621
622 # Step D: write doc patch bytes (tree now dirty on the feature branch).
623 try:
624 atomic_write_text(handover_path, plan.handover_text)
625 atomic_write_text(roadmap_path, plan.roadmap_text)
626 except WriteFailure as exc:
627 _rollback(restore_docs=True)
628 emit(f"write failed: {exc}")
629 return _failure(5, str(exc), None)
630
631 # Step E: commit dirty docs already on the feature branch (§GSW.6).
632 rel_handover = _repo_relative(repo_root, handover_path)
633 rel_roadmap = _repo_relative(repo_root, roadmap_path)
634 commit = adapter.commit_feature(
635 branch=plan.feature_branch,
636 message=plan.commit_message,
637 paths=[rel_handover, rel_roadmap],
638 )
639 if isinstance(commit, (ReadError, WriteError)):
640 _rollback(restore_docs=True)
641 cmd = commit.command if hasattr(commit, "command") else "commit_feature"
642 emit(str(commit))
643 return _failure(2, str(commit), cmd)
644
645 # Step F (§GSW.3.4): stamp the sync marker only after commit success.
646 if (
647 drift.d1_handover_vs_git == "aligned"
648 and drift.d2_anchor_vs_canonical == "aligned"
649 ):
650 _write_sync_marker(repo_root, reads)
651
652 # Step G: feature-branch push (Tier 1, regime-appropriate).
653 push_error = _push_feature_branch(runner, repo_root, config, plan.feature_branch)
654 if push_error:
655 emit(push_error)
656
657 if plan.pr_url:
658 emit(f"docs-only PR URL (operator-gated — do not auto-open): {plan.pr_url}")
659
660 _, workspace_relay = _emit_governance_gate_footer(
661 config,
662 repo_root,
663 handover_text=handover_text or plan.handover_text,
664 roadmap_text=roadmap_text or plan.roadmap_text,
665 emit=emit,
666 kit_root=kit_root,
667 )
668 return GovernanceSyncResult(
669 exit_code=0,
670 dry_run=False,
671 reads=reads,
672 drift=drift,
673 plan=plan,
674 committed=commit.committed,
675 commit_sha=commit.sha,
676 messages=("applied",),
677 workspace_relay=workspace_relay,
678 )
679
680
681 def _feature_branch_name(config: OverseerConfig) -> str:
682 slug = f"governance-sync-{date.today().isoformat()}"
683 pattern = config.vcs.git.feature_branch_pattern
684 return pattern.replace("{slug}", slug)
685
686
687 def _commit_message(
688 main_sha: str,
689 drift: DriftReport,
690 sections: tuple[str, ...],
691 realign_summary: str | None,
692 ) -> str:
693 drift_tokens = (
694 f"D1={drift.d1_handover_vs_git},"
695 f"D2={drift.d2_anchor_vs_canonical},"
696 f"D3={drift.d3_queue_vs_merged}"
697 )
698 subject = f"chore(governance): sync handover+roadmap to {main_sha[:7]} (drift: {drift_tokens})"
699 body_lines = ["Patched sections:", *[f"- {name}" for name in sections]]
700 if realign_summary:
701 body_lines.append(f"Realign: {realign_summary}")
702 return subject + "\n\n" + "\n".join(body_lines)
703
704
705 def _build_pr_url(
706 runner: CommandRunner,
707 repo_root: Path,
708 config: OverseerConfig,
709 feature_branch: str,
710 ) -> str | None:
711 if config.vcs.regime == "muse-only":
712 return None
713 remote = config.vcs.git.remote
714 main = config.vcs.git.main_branch
715 cmd = f"git remote get-url {quote_arg(remote)}"
716 result = runner.run(cmd, cwd=str(repo_root))
717 if not result.ok:
718 return f"https://github.com/<owner>/<repo>/compare/{main}...{feature_branch}?expand=1"
719 owner_repo = _parse_github_remote(result.stdout.strip())
720 if not owner_repo:
721 return f"https://github.com/<owner>/<repo>/compare/{main}...{feature_branch}?expand=1"
722 owner, repo = owner_repo
723 return f"https://github.com/{owner}/{repo}/compare/{main}...{feature_branch}?expand=1"
724
725
726 def _parse_github_remote(url: str) -> tuple[str, str] | None:
727 ssh = re.match(r"git@github\.com:(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?$", url)
728 if ssh:
729 return ssh.group("owner"), ssh.group("repo")
730 https = re.match(r"https://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?$", url)
731 if https:
732 return https.group("owner"), https.group("repo")
733 return None
734
735
736 def _write_sync_marker(repo_root: Path, reads: VerifiedReads | None = None) -> None:
737 """Write enriched ``last_governance_sync`` marker (§GFG.5.2)."""
738 marker = repo_root / ".overseer" / GOVERNANCE_SYNC_MARKER
739 stamp = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
740 r1 = ""
741 r3 = ""
742 if reads is not None:
743 r1 = reads.r1_github_main_sha or ""
744 r3 = reads.r3_canonical_main_sha or ""
745 body = f"{stamp}\nr1={r1}\nr3={r3}\n"
746 atomic_write_text(marker, body)
747
748
749 def _repo_relative(repo_root: Path, path: Path) -> str:
750 return path.resolve().relative_to(repo_root.resolve()).as_posix()
751
752
753 @dataclass(frozen=True)
754 class BranchState:
755 """Regime-specific ``original_branch_state`` captured at step A (§GSW.4.1)."""
756
757 git_branch: str | None
758 muse_branch: str | None
759
760
761 def _muse_command_prefix(adapter: VcsAdapter, repo_root: Path) -> str:
762 muse_cwd = str(getattr(adapter, "muse_cwd", repo_root))
763 return f"muse -C {quote_arg(muse_cwd)}"
764
765
766 def _capture_branch_state(
767 config: OverseerConfig,
768 adapter: VcsAdapter,
769 runner: CommandRunner,
770 repo_root: Path,
771 ) -> tuple[BranchState | None, str | None]:
772 """Capture current branch identity per regime; fail closed on unreadable HEAD.
773
774 Returns ``(state, None)`` on success or ``(None, failing_command)``.
775 """
776 root = str(repo_root)
777 regime = config.vcs.regime
778 git_branch: str | None = None
779 muse_branch: str | None = None
780
781 if regime in {"git-only", "muse+git-mirror"}:
782 command = "git rev-parse --abbrev-ref HEAD"
783 result = runner.run(command, cwd=root)
784 if not result.ok or not result.stdout.strip():
785 return None, command
786 git_branch = result.stdout.strip()
787
788 if regime in {"muse-only", "muse+git-mirror"}:
789 command = f"{_muse_command_prefix(adapter, repo_root)} rev-parse --abbrev-ref HEAD"
790 result = runner.run(command, cwd=root)
791 if not result.ok or not result.stdout.strip():
792 return None, command
793 muse_branch = result.stdout.strip()
794
795 return BranchState(git_branch=git_branch, muse_branch=muse_branch), None
796
797
798 def _ensure_feature_branch(
799 adapter: VcsAdapter,
800 runner: CommandRunner,
801 repo_root: Path,
802 config: OverseerConfig,
803 branch: str,
804 state: BranchState,
805 ) -> str | None:
806 """Place current HEAD(s) on ``branch`` before any doc write (§GSW.5).
807
808 Under ``muse+git-mirror`` both the Muse HEAD and the Git HEAD must be on
809 ``branch`` (§GSW.5.1 dual-HEAD rule). Returns the failing command, or
810 ``None`` on success.
811 """
812 regime = config.vcs.regime
813 if regime in {"muse-only", "muse+git-mirror"}:
814 muse_error = _ensure_muse_branch(adapter, runner, repo_root, branch, state.muse_branch)
815 if muse_error is not None:
816 return muse_error
817 if regime in {"git-only", "muse+git-mirror"}:
818 git_error = _ensure_git_branch(runner, repo_root, branch, state.git_branch)
819 if git_error is not None:
820 return git_error
821 return None
822
823
824 def _ensure_muse_branch(
825 adapter: VcsAdapter,
826 runner: CommandRunner,
827 repo_root: Path,
828 branch: str,
829 current: str | None,
830 ) -> str | None:
831 """Create/switch the Muse HEAD to ``branch`` (§GSW.5.2).
832
833 Switch to an existing branch retries with ``--autoshelf`` so uncommitted
834 tracked changes carry across the checkout (§GSW.6.2 allowed secondary
835 guard; bare-checkout-only is the live defect, ``--force`` is forbidden).
836 """
837 if current == branch:
838 return None
839 root = str(repo_root)
840 prefix = _muse_command_prefix(adapter, repo_root)
841 create = runner.run(f"{prefix} checkout -b {quote_arg(branch)}", cwd=root)
842 if create.ok:
843 return None
844 switch_cmd = f"{prefix} checkout {quote_arg(branch)}"
845 if runner.run(switch_cmd, cwd=root).ok:
846 return None
847 carry_cmd = f"{prefix} checkout --autoshelf {quote_arg(branch)}"
848 if runner.run(carry_cmd, cwd=root).ok:
849 return None
850 return carry_cmd
851
852
853 def _ensure_git_branch(
854 runner: CommandRunner,
855 repo_root: Path,
856 branch: str,
857 current: str | None,
858 ) -> str | None:
859 """Create/switch the Git HEAD to ``branch`` (git carries dirty changes)."""
860 if current == branch:
861 return None
862 root = str(repo_root)
863 create = runner.run(f"git checkout -b {quote_arg(branch)}", cwd=root)
864 if create.ok:
865 return None
866 switch_cmd = f"git checkout {quote_arg(branch)}"
867 if runner.run(switch_cmd, cwd=root).ok:
868 return None
869 return switch_cmd
870
871
872 _UNIQUIFY_LIMIT = 100
873
874
875 @dataclass(frozen=True)
876 class _BranchReconcileProbe:
877 """Per-history §GSB.3.2 classification of the candidate dated branch.
878
879 ``tip`` is ``T_exist`` and ``target`` is ``T_target`` in that history's
880 own id space (Git SHAs vs Muse ``sha256:`` ids are never cross-compared).
881 """
882
883 exists: bool
884 tip: str | None = None
885 target: str | None = None
886 ancestor: bool = False
887
888
889 def _reconcile_feature_branch(
890 adapter: VcsAdapter,
891 runner: CommandRunner,
892 repo_root: Path,
893 config: OverseerConfig,
894 branch: str,
895 state: BranchState,
896 ) -> tuple[str, str | None]:
897 """C0 reconcile of the dated sync-branch name before ensure (§GSB.3).
898
899 Classifies the existing branch tip against ``T_target`` on every
900 applicable history. All existing sides ancestor/equal → fast-forward
901 the tips without checking ``branch`` out; any diverged side →
902 deterministic ``-N`` uniquify of the shared name (§GSB.3.2 cross-history
903 rule). Returns ``(reconciled_branch, failing_command_or_None)``.
904 """
905 regime = config.vcs.regime
906 muse_probe: _BranchReconcileProbe | None = None
907 git_probe: _BranchReconcileProbe | None = None
908
909 if regime in {"muse-only", "muse+git-mirror"}:
910 muse_probe, error = _classify_muse_branch(
911 adapter, runner, repo_root, config, branch, state.muse_branch
912 )
913 if error is not None:
914 return branch, error
915 if regime in {"git-only", "muse+git-mirror"}:
916 git_probe, error = _classify_git_branch(
917 runner, repo_root, config, branch, state.git_branch
918 )
919 if error is not None:
920 return branch, error
921
922 existing = [p for p in (muse_probe, git_probe) if p is not None and p.exists]
923 if not existing:
924 return branch, None
925
926 # §GSB.3.2 cross-history rule: if either side diverged, uniquify the
927 # shared name — never FF one history and uniquify the other under the
928 # same name.
929 if any(not probe.ancestor for probe in existing):
930 return _uniquify_branch(adapter, runner, repo_root, config, branch)
931
932 if muse_probe is not None and muse_probe.exists and muse_probe.tip != muse_probe.target:
933 error = _fast_forward_muse(
934 adapter, runner, repo_root, branch, muse_probe.target or "", state.muse_branch
935 )
936 if error is not None:
937 return branch, error
938 if git_probe is not None and git_probe.exists and git_probe.tip != git_probe.target:
939 error = _fast_forward_git(
940 runner, repo_root, branch, git_probe.target or "", state.git_branch
941 )
942 if error is not None:
943 return branch, error
944 return branch, None
945
946
947 def _classify_git_branch(
948 runner: CommandRunner,
949 repo_root: Path,
950 config: OverseerConfig,
951 branch: str,
952 original: str | None,
953 ) -> tuple[_BranchReconcileProbe | None, str | None]:
954 """Probe existence + §GSB.3.2 ancestor classification on the Git history."""
955 root = str(repo_root)
956 probe_cmd = f"git rev-parse --verify {quote_arg('refs/heads/' + branch)}"
957 probe = runner.run(probe_cmd, cwd=root)
958 if not probe.ok:
959 return _BranchReconcileProbe(exists=False), None
960 t_exist = probe.stdout.strip()
961 if not t_exist:
962 return None, probe_cmd
963
964 # §GSB.3.2.1 reconcile base ref: O_H when HEAD is elsewhere; configured
965 # main when HEAD already names the dated branch.
966 base_ref = original if original and original != branch else config.vcs.git.main_branch
967 target_cmd = f"git rev-parse {quote_arg(base_ref)}"
968 target = runner.run(target_cmd, cwd=root)
969 if not target.ok or not target.stdout.strip():
970 return None, target_cmd
971 t_target = target.stdout.strip()
972
973 if t_exist == t_target:
974 return _BranchReconcileProbe(True, t_exist, t_target, True), None
975 ancestor_cmd = (
976 f"git merge-base --is-ancestor {quote_arg(t_exist)} {quote_arg(t_target)}"
977 )
978 result = runner.run(ancestor_cmd, cwd=root)
979 if result.exit_code == 0:
980 return _BranchReconcileProbe(True, t_exist, t_target, True), None
981 if result.exit_code == 1:
982 return _BranchReconcileProbe(True, t_exist, t_target, False), None
983 return None, ancestor_cmd
984
985
986 def _classify_muse_branch(
987 adapter: VcsAdapter,
988 runner: CommandRunner,
989 repo_root: Path,
990 config: OverseerConfig,
991 branch: str,
992 original: str | None,
993 ) -> tuple[_BranchReconcileProbe | None, str | None]:
994 """Probe existence + §GSB.3.2 ancestor classification on the Muse history."""
995 root = str(repo_root)
996 prefix = _muse_command_prefix(adapter, repo_root)
997 probe_cmd = f"{prefix} rev-parse {quote_arg(branch)}"
998 probe = runner.run(probe_cmd, cwd=root)
999 if not probe.ok:
1000 return _BranchReconcileProbe(exists=False), None
1001 t_exist = probe.stdout.strip()
1002 if not t_exist:
1003 return None, probe_cmd
1004
1005 base_ref = (
1006 original
1007 if original and original != branch
1008 else (config.vcs.muse.main_branch or "")
1009 )
1010 target_cmd = f"{prefix} rev-parse {quote_arg(base_ref)}"
1011 if not base_ref:
1012 return None, target_cmd
1013 target = runner.run(target_cmd, cwd=root)
1014 if not target.ok or not target.stdout.strip():
1015 return None, target_cmd
1016 t_target = target.stdout.strip()
1017
1018 if t_exist == t_target:
1019 return _BranchReconcileProbe(True, t_exist, t_target, True), None
1020 # Muse 0.2.x has no --is-ancestor flag; the LCA equals T_exist exactly
1021 # when T_exist is an ancestor of T_target.
1022 ancestor_cmd = (
1023 f"{prefix} merge-base --json {quote_arg(t_exist)} {quote_arg(t_target)}"
1024 )
1025 result = runner.run(ancestor_cmd, cwd=root)
1026 if not result.ok:
1027 return None, ancestor_cmd
1028 try:
1029 payload = json.loads(result.stdout)
1030 except json.JSONDecodeError:
1031 return None, ancestor_cmd
1032 if not isinstance(payload, dict):
1033 return None, ancestor_cmd
1034 ancestor = payload.get("merge_base") == t_exist
1035 return _BranchReconcileProbe(True, t_exist, t_target, ancestor), None
1036
1037
1038 def _fast_forward_git(
1039 runner: CommandRunner,
1040 repo_root: Path,
1041 branch: str,
1042 target: str,
1043 original: str | None,
1044 ) -> str | None:
1045 """§GSB.3.3 Git fast-forward: ancestor-validated tip move, never checkout-as-FF.
1046
1047 Returns the failing command (or a clobber-refusal message) or ``None``.
1048 """
1049 root = str(repo_root)
1050 if original != branch:
1051 ff_cmd = f"git branch -f {quote_arg(branch)} {quote_arg(target)}"
1052 if not runner.run(ff_cmd, cwd=root).ok:
1053 return ff_cmd
1054 return None
1055
1056 # O_H == B (§GSB.3.2.1): git branch -f refuses the checked-out branch,
1057 # so move the ref directly, then refresh the stale worktree without
1058 # checkout --force (§GSB.3.3 / §GSB.3.4).
1059 status_cmd = "git status --porcelain"
1060 pre = runner.run(status_cmd, cwd=root)
1061 if not pre.ok:
1062 return status_cmd
1063 pre_dirty = bool(pre.stdout.strip())
1064 update_cmd = f"git update-ref {quote_arg('refs/heads/' + branch)} {quote_arg(target)}"
1065 if not runner.run(update_cmd, cwd=root).ok:
1066 return update_cmd
1067 post = runner.run(status_cmd, cwd=root)
1068 if not post.ok:
1069 return status_cmd
1070 if not post.stdout.strip():
1071 return None
1072 if pre_dirty:
1073 # Uncommitted operator work is mixed into the stale tree — a refresh
1074 # would clobber it. Fail closed (§GSB.3.3).
1075 return f"worktree refresh after {update_cmd} would clobber uncommitted changes"
1076 # Tree was clean at T_exist, so the only difference is committed stale
1077 # content — a clean-tree-verified reset discards nothing uncommitted.
1078 reset_cmd = f"git reset --hard {quote_arg(target)}"
1079 if not runner.run(reset_cmd, cwd=root).ok:
1080 return reset_cmd
1081 return None
1082
1083
1084 def _fast_forward_muse(
1085 adapter: VcsAdapter,
1086 runner: CommandRunner,
1087 repo_root: Path,
1088 branch: str,
1089 target: str,
1090 original: str | None,
1091 ) -> str | None:
1092 """§GSB.3.3 Muse fast-forward via ``muse update-ref`` (never checkout).
1093
1094 When Muse HEAD already names the dated branch, a tip-only move leaves
1095 the shared worktree stale; refresh uses a clean-tree-verified
1096 ``muse reset --hard`` (live Muse refuses it on tracked changes, so it
1097 can never clobber operator work) — ``--force`` stays forbidden.
1098 """
1099 root = str(repo_root)
1100 prefix = _muse_command_prefix(adapter, repo_root)
1101 update_cmd = f"{prefix} update-ref {quote_arg(branch)} {quote_arg(target)}"
1102 if original != branch:
1103 if not runner.run(update_cmd, cwd=root).ok:
1104 return update_cmd
1105 return None
1106
1107 pre_dirty, error = _muse_tracked_dirty(runner, repo_root, prefix)
1108 if error is not None:
1109 return error
1110 if not pre_dirty:
1111 # Worktree matches T_exist bytes exactly: reset --hard while still
1112 # clean refreshes worktree + tip together (discarding only committed
1113 # T_exist content); update-ref then locks the frozen tip form.
1114 reset_cmd = f"{prefix} reset {quote_arg(target)} --hard"
1115 if not runner.run(reset_cmd, cwd=root).ok:
1116 return reset_cmd
1117 if not runner.run(update_cmd, cwd=root).ok:
1118 return update_cmd
1119 return None
1120
1121 # Tracked changes present relative to the stale tip: this is either the
1122 # live mirror shape (shared worktree already holds T_target bytes) or
1123 # uncommitted operator work. Move the tip, then verify the worktree
1124 # matches the new tip; anything else fails closed (§GSB.3.4).
1125 if not runner.run(update_cmd, cwd=root).ok:
1126 return update_cmd
1127 post_dirty, error = _muse_tracked_dirty(runner, repo_root, prefix)
1128 if error is not None:
1129 return error
1130 if post_dirty:
1131 return f"worktree refresh after {update_cmd} would clobber uncommitted changes"
1132 return None
1133
1134
1135 def _muse_tracked_dirty(
1136 runner: CommandRunner,
1137 repo_root: Path,
1138 prefix: str,
1139 ) -> tuple[bool | None, str | None]:
1140 """Tracked-change dirtiness via ``muse status --json`` (§GSB.3.4).
1141
1142 Muse 0.2.x sets ``dirty`` for untracked files too; staleness/clobber
1143 checks must key on tracked changes (``total_changes``) only.
1144 """
1145 status_cmd = f"{prefix} status --json"
1146 result = runner.run(status_cmd, cwd=str(repo_root))
1147 if not result.ok:
1148 return None, status_cmd
1149 try:
1150 payload = json.loads(result.stdout)
1151 except json.JSONDecodeError:
1152 return None, status_cmd
1153 if not isinstance(payload, dict):
1154 return None, status_cmd
1155 total = payload.get("total_changes")
1156 if isinstance(total, int):
1157 return total > 0, None
1158 if "dirty" in payload:
1159 return bool(payload["dirty"]), None
1160 return None, status_cmd
1161
1162
1163 def _uniquify_branch(
1164 adapter: VcsAdapter,
1165 runner: CommandRunner,
1166 repo_root: Path,
1167 config: OverseerConfig,
1168 branch: str,
1169 ) -> tuple[str, str | None]:
1170 """§GSB.3.3 deterministic uniquify: lowest ``-N`` (N≥2) free everywhere.
1171
1172 The diverged existing branch is never deleted, rewound, or force-checked
1173 out; the suffix contains no secrets, paths, or hostnames.
1174 """
1175 regime = config.vcs.regime
1176 root = str(repo_root)
1177 prefix = (
1178 _muse_command_prefix(adapter, repo_root) if regime != "git-only" else None
1179 )
1180 for suffix in range(2, _UNIQUIFY_LIMIT):
1181 candidate = f"{branch}-{suffix}"
1182 taken = False
1183 if prefix is not None:
1184 taken = runner.run(
1185 f"{prefix} rev-parse {quote_arg(candidate)}", cwd=root
1186 ).ok
1187 if not taken and regime in {"git-only", "muse+git-mirror"}:
1188 taken = runner.run(
1189 f"git rev-parse --verify {quote_arg('refs/heads/' + candidate)}",
1190 cwd=root,
1191 ).ok
1192 if not taken:
1193 return candidate, None
1194 return branch, f"could not uniquify branch {branch!r} within -{_UNIQUIFY_LIMIT - 1}"
1195
1196
1197 def _rollback_apply(
1198 *,
1199 config: OverseerConfig,
1200 repo_root: Path,
1201 adapter: VcsAdapter,
1202 runner: CommandRunner,
1203 handover_path: Path,
1204 roadmap_path: Path,
1205 original_handover: str,
1206 original_roadmap: str,
1207 marker_path: Path,
1208 prior_marker: str | None,
1209 branch_state: BranchState,
1210 restore_docs: bool,
1211 emit,
1212 ) -> None:
1213 """Restore docs → marker → original branch on mid-apply failure (§GSW.4.2).
1214
1215 Doc bytes are restored first so the tree is clean before the restore
1216 checkout (§GSW.4.3); branch restore is best-effort on both histories and
1217 never uses ``--force``.
1218 """
1219 if restore_docs:
1220 atomic_write_text(handover_path, original_handover)
1221 atomic_write_text(roadmap_path, original_roadmap)
1222
1223 _restore_marker(marker_path, prior_marker)
1224
1225 for failed_command in _restore_branch_state(
1226 config, adapter, runner, repo_root, branch_state
1227 ):
1228 emit(f"branch restore failed: {failed_command}")
1229
1230
1231 def _restore_marker(marker_path: Path, prior_marker: str | None) -> None:
1232 """Leave no new stamp behind on mid-apply failure (§GSW.3.4)."""
1233 if prior_marker is None:
1234 if marker_path.is_file():
1235 marker_path.unlink()
1236 return
1237 if not marker_path.is_file() or marker_path.read_text(encoding="utf-8") != prior_marker:
1238 atomic_write_text(marker_path, prior_marker)
1239
1240
1241 def _restore_branch_state(
1242 config: OverseerConfig,
1243 adapter: VcsAdapter,
1244 runner: CommandRunner,
1245 repo_root: Path,
1246 state: BranchState,
1247 ) -> tuple[str, ...]:
1248 """Best-effort restore of captured branch identity on both histories.
1249
1250 Returns the failing commands (empty on success). If one side fails the
1251 other is still attempted (§GSW.4.2 dual restore).
1252 """
1253 root = str(repo_root)
1254 regime = config.vcs.regime
1255 errors: list[str] = []
1256
1257 if regime in {"muse-only", "muse+git-mirror"} and state.muse_branch:
1258 prefix = _muse_command_prefix(adapter, repo_root)
1259 probe = runner.run(f"{prefix} rev-parse --abbrev-ref HEAD", cwd=root)
1260 current = probe.stdout.strip() if probe.ok else None
1261 if current != state.muse_branch:
1262 restore_cmd = f"{prefix} checkout {quote_arg(state.muse_branch)}"
1263 if not runner.run(restore_cmd, cwd=root).ok:
1264 carry_cmd = f"{prefix} checkout --autoshelf {quote_arg(state.muse_branch)}"
1265 if not runner.run(carry_cmd, cwd=root).ok:
1266 errors.append(carry_cmd)
1267
1268 if regime in {"git-only", "muse+git-mirror"} and state.git_branch:
1269 probe = runner.run("git rev-parse --abbrev-ref HEAD", cwd=root)
1270 current = probe.stdout.strip() if probe.ok else None
1271 if current != state.git_branch:
1272 restore_cmd = f"git checkout {quote_arg(state.git_branch)}"
1273 if not runner.run(restore_cmd, cwd=root).ok:
1274 errors.append(restore_cmd)
1275
1276 return tuple(errors)
1277
1278
1279 def _push_feature_branch(
1280 runner: CommandRunner,
1281 repo_root: Path,
1282 config: OverseerConfig,
1283 branch: str,
1284 ) -> str | None:
1285 if config.vcs.regime == "muse-only":
1286 return None
1287 remote = config.vcs.git.remote
1288 cmd = f"git push -u {quote_arg(remote)} {quote_arg(branch)}"
1289 result = runner.run(cmd, cwd=str(repo_root))
1290 if not result.ok:
1291 return cmd
1292 return None