7Legacy queue/status migration into the M11 registry.
11Project the Markdown-era orchestration state into the SQLite registry
12without making the registry depend on those legacy files afterward.
16- Parse the legacy agent queue, plan headers, status files, and git evidence.
17- Map legacy lifecycle names onto the ADR-00Z registry statuses.
18- Build a deterministic projection for dry-run, import, verify, and rollback.
19- Delegate all current-state SQL and operation-log writes to SQLiteBackend.
31- No raw wall-clock imports are used here; timestamps come from the backend Clock.
32- Import uses one SQLite transaction for projected rows and audit rows.
33- Unknown legacy statuses fail the import before writing audit rows.
37- oc_diagnostics (structured debug logging via _dbg)
38- oc_chat.registry._legacy_queue (read-only legacy queue parser; plan 223
39 moved it here when the queue machinery retired)
40- oc_chat.scripts.plan_status (legacy status file parser)
41- oc_chat.registry.backends.sqlite (SQL owner for migration writes)
42- oc_chat.registry (Registry facade passed to the import, verify and rollback entry points)
43- yaml (fallback parse of legacy status-file frontmatter)
44- git (not imported; run as a subprocess for worktree list, the first-parent log of main and rev-parse main, the legacy status-file and round evidence)
46from __future__
import annotations
48from dataclasses
import dataclass
51from pathlib
import Path
54from typing
import Mapping, Sequence
58from oc_diagnostics
import _dbg
66_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
69PLAN_ID_RE = re.compile(
r"^(?P<number>\d{3})-(?P<slug>.+)$")
70LEGACY_PLAN_ID_RE = re.compile(
r"\b\d{3}-[A-Za-z0-9][A-Za-z0-9_.-]*\b")
71ROUND_RE = re.compile(
r"\bround[-\s]*(?P<round>\d+)\b", re.IGNORECASE)
75 r"^\*\*Branch:\*\*\s*(?:`(?P<ticks>[^`\n]+)`|(?P<plain>\S+))",
76 re.IGNORECASE | re.MULTILINE,
78 "worktree": re.compile(
80 r"^\*\*Worktree:\*\*\s*(?:`(?P<ticks>[^`\n]+)`|(?P<plain>\S+))",
81 re.IGNORECASE | re.MULTILINE,
83 "plan_type": re.compile(
84 r"^\*\*Plan Type:\*\*\s*(?P<value>.+?)\s*$",
85 re.IGNORECASE | re.MULTILINE,
88 r"^\*\*Status:\*\*\s*(?P<value>.+?)\s*$",
89 re.IGNORECASE | re.MULTILINE,
91 "suggested_model": re.compile(
92 r"^\*\*Suggested model:\*\*\s*(?P<value>.+?)\s*$",
93 re.IGNORECASE | re.MULTILINE,
96H1_RE = re.compile(
r"^#\s+(?P<value>.+?)\s*$", re.MULTILINE)
97VALID_LEGACY_STATUSES = {
105STATUS_NORMALISATIONS = {
108 "pending":
"Pending",
109 "implementing":
"In Progress",
110 "in progress":
"In Progress",
111 "in-progress":
"In Progress",
112 "completed":
"Completed",
113 "partially completed":
"Partial",
114 "partial":
"Partial",
116 "blocked":
"Blocked",
118PRE_ROUND_SYSTEM_PLAN_CEILING = 50
121@dataclass(frozen=True)
123 """Evidence that a plan was integrated by a closed mainline round."""
127 merge_commit_sha: str
131@dataclass(frozen=True)
133 """Filesystem inputs used by migration commands."""
136 queue_path: Path |
None =
None
137 plan_dir: Path |
None =
None
138 status_paths: tuple[Path, ...] |
None =
None
139 round_evidence: tuple[RoundEvidence, ...] |
None =
None
140 discovered_in_main_at_sha: str |
None =
None
143@dataclass(frozen=True)
147 summary: dict[str, object]
148 errors: tuple[str, ...] = ()
149 warnings: tuple[str, ...] = ()
150 backup_path: str |
None =
None
156@dataclass(frozen=True)
159 summary: dict[str, object]
160 errors: tuple[str, ...] = ()
161 warnings: tuple[str, ...] = ()
162 diff: dict[str, object] |
None =
None
168@dataclass(frozen=True)
171 summary: dict[str, object]
172 errors: tuple[str, ...] = ()
173 restored_from: str |
None =
None
179@dataclass(frozen=True)
189@dataclass(frozen=True)
197 agent_label: str |
None
201 body_observed_at: str
203 blocked_reason: str |
None
206@dataclass(frozen=True)
213 plan_type: str |
None
219 legacy_status: str |
None
223@dataclass(frozen=True)
234@dataclass(frozen=True)
236 agents: tuple[dict[str, object], ...]
237 plans: tuple[dict[str, object], ...]
238 queue_entries: tuple[dict[str, object], ...]
239 rounds: tuple[dict[str, object], ...]
240 round_plans: tuple[dict[str, object], ...]
241 errors: tuple[str, ...]
242 warnings: tuple[str, ...]
246 "agents": list(self.
agents),
247 "plans": list(self.
plans),
249 "rounds": list(self.
rounds),
263 status_counts: dict[str, int] = {}
264 for plan
in self.
plans:
265 status = str(plan[
"status"])
266 status_counts[status] = status_counts.get(status, 0) + 1
268 "agents": len(self.
agents),
269 "plans": len(self.
plans),
271 "rounds": len(self.
rounds),
273 "status_counts": dict(sorted(status_counts.items())),
279 options: MigrationOptions,
280) -> MigrationProjection:
281 """Build the deterministic legacy-to-registry projection."""
284 root = options.workspace_root.resolve()
285 queue_path = (options.queue_path
or root / _legacy_queue.DEFAULT_AGENT_QUEUE).resolve()
286 plan_dir = (options.plan_dir
or root /
"oc_chat" /
"plans").resolve()
287 warnings: list[str] = []
288 errors: list[str] = []
294 options.status_paths,
296 backend.clock.now_wall(),
300 discovered_sha = options.discovered_in_main_at_sha
or _main_sha(root)
305 | set(status_sources)
306 | (set(completed) & set(headers))
316 plan_rows: list[dict[str, object]] = []
317 queue_rows: list[dict[str, object]] = []
318 rounds_by_no: dict[int, dict[str, object]] = {}
319 round_plans: list[dict[str, object]] = []
320 round_orders: dict[int, int] = {}
322 for plan_id
in sorted(plan_ids, key=_plan_sort_key):
324 source = status_sources.get(plan_id)
325 queue_entry = pending.get(plan_id)
332 evidence.get(plan_id),
339 assigned_slot =
_assigned_slot(queue_entry, source, completed.get(plan_id))
342 if queue_entry
and mapped[
"status"]
in {
"pending",
"implementing",
"ready_for_merge"}
345 body_observed_at = source.body_observed_at
if source
is not None else None
348 _first_text(source.branch
if source
else None, header.branch)
350 worktree =
_first_text(source.worktree
if source
else None, header.worktree)
353 "plan_number": header.plan_number,
354 "title": header.title,
355 "plan_type": header.plan_type,
356 "status": mapped[
"status"],
357 "assigned_slot": assigned_slot,
358 "queue_position": queue_position,
360 "worktree": worktree,
361 "body_path": header.source_file,
362 "body_sha256": header.body_sha256,
363 "body_observed_at": body_observed_at,
364 "model_override": model,
365 "thinking_override": thinking,
366 "session_override": session,
367 "ready_for_merge_at": mapped[
"ready_for_merge_at"],
368 "completed_at": mapped[
"completed_at"],
369 "closed_round": mapped[
"closed_round"],
370 "legacy_status": legacy_status,
376 source.source_file
if source
else header.source_file,
379 if mapped[
"backfill_payload"]
is not None:
380 row[
"backfill_payload"] = mapped[
"backfill_payload"]
381 if mapped[
"blocked_payload"]
is not None:
382 row[
"blocked_payload"] = mapped[
"blocked_payload"]
383 plan_rows.append(row)
385 plan_evidence = evidence.get(plan_id)
386 if plan_evidence
is not None and mapped[
"closed_round"]
is not None:
387 rounds_by_no[plan_evidence.round_no] =
_round_row(plan_evidence)
388 round_orders[plan_evidence.round_no] = round_orders.get(
389 plan_evidence.round_no,
394 "round_no": plan_evidence.round_no,
396 "merge_order": round_orders[plan_evidence.round_no],
397 "merge_commit_sha": plan_evidence.merge_commit_sha,
398 "test_result":
"pass",
402 if queue_entry
is not None:
406 "slot": queue_entry.slot,
407 "position": queue_entry.position,
408 "source_file": queue_entry.source_file,
412 "slot": queue_entry.slot,
413 "position": queue_entry.position,
414 "source_file": queue_entry.source_file,
419 if queue_path.is_file()
is False:
420 warnings.append(f
"legacy queue not found: {queue_path}")
422 warnings.append(f
"no plan headers found in {plan_dir}")
426 agents=tuple(agents),
427 plans=tuple(plan_rows),
428 queue_entries=tuple(sorted(queue_rows, key=
lambda row: (row[
"slot"], row[
"position"]))),
429 rounds=tuple(rounds_by_no[key]
for key
in sorted(rounds_by_no)),
430 round_plans=tuple(sorted(round_plans, key=
lambda row: (row[
"round_no"], row[
"merge_order"]))),
431 errors=tuple(errors),
432 warnings=tuple(warnings),
438 options: MigrationOptions,
440 dry_run: bool =
False,
442 """Dry-run or apply the legacy migration projection."""
445 if projection.errors:
449 summary=projection.summary(),
450 errors=projection.errors,
451 warnings=projection.warnings,
457 summary=projection.summary(),
458 warnings=projection.warnings,
462 actor_slot = backend.active_operator_slot()
463 if actor_slot
is None:
467 summary=projection.summary(),
468 errors=(
"no active human operator slot exists",),
469 warnings=projection.warnings,
472 backup_path =
_backup_path(options.workspace_root.resolve(), backend.clock.now_wall())
473 backend.backup(backup_path)
474 registry.acquire_lock(
478 {
"command":
"migrate.import",
"version": 1},
481 backend.apply_migration_projection(
482 projection.to_backend_payload(),
483 actor_slot=actor_slot,
484 actor_type=
"human_operator",
487 registry.release_lock(
"migrate-import", actor_slot)
492 summary=projection.summary(),
493 warnings=projection.warnings,
494 backup_path=str(backup_path),
498def run_verify(registry: Registry, options: MigrationOptions) -> VerifyResult:
499 """Compare the projected legacy state with the registry tables."""
502 if projection.errors:
505 summary=projection.summary(),
506 errors=projection.errors,
507 warnings=projection.warnings,
510 expected = projection.expected_snapshot()
511 actual = backend.migration_snapshot()
513 backfill_violations = backend.migration_null_round_backfill_violations()
514 if backfill_violations:
515 diff[
"null_round_backfill_violations"] = backfill_violations
519 summary=projection.summary(),
520 warnings=projection.warnings,
521 diff=
None if ok
else diff,
525def run_rollback(registry: Registry, options: MigrationOptions) -> RollbackResult:
526 """Restore the most recent pre-migration backup."""
529 backups = sorted(
_backup_dir(options.workspace_root.resolve()).glob(
"pre-migrate-*.sqlite"))
533 summary={
"backups": 0},
534 errors=(
"no pre-migration backup found",),
537 backend.restore(latest)
540 summary={
"restored": str(latest)},
541 restored_from=str(latest),
546 """Return the v0.2.x cutover placeholder."""
551 message=
"migrate cutover not implemented in v0.2.x; see M11.8",
556 """Check the migration backfill marker post-condition."""
558 violations =
_sqlite_backend(registry).migration_null_round_backfill_violations()
559 return {
"ok":
not violations,
"violations": violations}
563 rows: list[dict[str, object]] = []
564 for label, info
in _legacy_queue.LEGACY_AGENT_POOLS.items():
566 role = str(info.get(
"role",
"Worker"))
567 slot_kind =
"orchestrator" if "orchestrator" in role.lower()
else "worker"
569 model_class =
"frontier"
571 model_class =
"small"
572 orchestrator_eligible = slot_kind ==
"orchestrator"
575 "slot_kind": slot_kind,
576 "identity_kind":
"agent",
577 "model_class": model_class,
578 "orchestrator_eligible": 1
if orchestrator_eligible
else 0,
580 "profile": str(info.get(
"profile",
""))
or None,
585 "slot_kind": slot_kind,
586 "identity_kind":
"agent",
587 "model_class": model_class,
588 "orchestrator_eligible": orchestrator_eligible,
589 "provider": row[
"provider"],
590 "profile": row[
"profile"],
596 "source_file":
"oc_chat.registry._legacy_queue.LEGACY_AGENT_POOLS",
599 return sorted(rows, key=
lambda row: str(row[
"slot"]))
605) -> tuple[dict[str, QueueEntry], dict[str, str]]:
606 pending: dict[str, QueueEntry] = {}
607 completed: dict[str, str] = {}
608 queue = _legacy_queue.load_legacy_queue(queue_path)
610 for label, section
in queue.items():
612 for index, entry
in enumerate(section[
"pending"], start=1):
613 plan_id = _legacy_queue.entry_plan_id(entry)
614 model, thinking, session = _legacy_queue.parse_entry_overrides(entry)
619 source_file=source_file,
624 for plan_id
in section[
"completed"]:
625 completed.setdefault(plan_id, slot_id)
626 return pending, completed
630 headers: dict[str, PlanHeader] = {}
631 if not plan_dir.is_dir():
633 for path
in sorted(plan_dir.glob(
"[0-9][0-9][0-9]-*.md")):
635 match = PLAN_ID_RE.match(plan_id)
638 text = path.read_text(encoding=
"utf-8")
643 plan_number=int(match.group(
"number")),
653 legacy_status=legacy_status,
654 body_sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
660 explicit_paths: tuple[Path, ...] |
None,
664) -> dict[str, StatusSource]:
666 sources: dict[str, StatusSource] = {}
668 if not path.is_file():
672 except Exception
as exc:
673 warnings.append(f
"could not parse {path}: {type(exc).__name__}: {exc}")
675 sources[source.plan_id] = source
681 status, body = plan_status.read(path)
682 data = status.model_dump(mode=
"json")
684 text = path.read_text(encoding=
"utf-8")
685 frontmatter, body = plan_status._split_frontmatter(text)
686 loaded = yaml.safe_load(frontmatter)
687 if not isinstance(loaded, dict):
688 raise ValueError(
"frontmatter must be a YAML mapping")
691 raw_agent = data.get(
"agent")
692 agent_label =
_agent_label(str(raw_agent))
if raw_agent
else None
695 data.get(
"blocked_reason"),
706 plan_id=str(data[
"plan_id"]),
707 legacy_status=legacy_status,
712 agent_label=agent_label,
716 body_observed_at=body_observed_at,
718 blocked_reason=blocked_reason,
723 paths = {root / plan_status.DEFAULT_FILENAME}
725 result = subprocess.run(
726 [
"git",
"worktree",
"list",
"--porcelain"],
734 except (OSError, ValueError):
736 if result
is not None and result.returncode == 0:
737 for line
in result.stdout.splitlines():
738 if line.startswith(
"worktree "):
739 paths.add(Path(line.removeprefix(
"worktree ")).resolve() / plan_status.DEFAULT_FILENAME)
740 return sorted(path
for path
in paths
if path.is_file())
744 options: MigrationOptions,
746) -> dict[str, RoundEvidence]:
747 evidence = list(options.round_evidence
or ())
748 if options.round_evidence
is None:
750 by_plan: dict[str, RoundEvidence] = {}
751 for item
in evidence:
752 by_plan[item.plan_id] = item
758 result = subprocess.run(
764 "--pretty=format:%H%x1f%aI%x1f%s%x1f%b%x1e",
773 except (OSError, ValueError):
775 if result.returncode != 0:
778 evidence: list[RoundEvidence] = []
779 for record
in result.stdout.split(
"\x1e"):
780 fields = record.strip().split(
"\x1f", 3)
783 commit_sha, closed_at, subject, body = fields
784 text = f
"{subject}\n{body}"
785 round_match = ROUND_RE.search(text)
786 if round_match
is None:
788 round_no = int(round_match.group(
"round"))
789 for plan_id
in sorted(set(LEGACY_PLAN_ID_RE.findall(text))):
794 merge_commit_sha=commit_sha,
803 result = subprocess.run(
804 [
"git",
"rev-parse",
"main"],
812 except (OSError, ValueError):
814 if result.returncode != 0:
816 return result.stdout.strip()
or None
822 source: StatusSource |
None,
823 pending: Mapping[str, QueueEntry],
824 completed: Mapping[str, str],
826 if source
is not None:
827 return source.legacy_status
828 if header.legacy_status
is not None:
829 return header.legacy_status
830 if plan_id
in pending:
832 if plan_id
in completed:
839 headers: Mapping[str, PlanHeader],
840 pending: Mapping[str, QueueEntry],
841 completed: Mapping[str, str],
842 status_sources: Mapping[str, StatusSource],
845 chosen: dict[int, str] = {}
846 for plan_id
in sorted(plan_ids, key=_plan_sort_key):
848 previous = chosen.get(number)
850 chosen[number] = plan_id
854 key=
lambda item:
_plan_priority(item, pending, completed, status_sources),
856 loser = plan_id
if winner == previous
else previous
857 chosen[number] = winner
859 f
"duplicate plan_number {number}: kept {winner}, skipped {loser}"
861 return set(chosen.values())
864def _plan_number(plan_id: str, headers: Mapping[str, PlanHeader]) -> int:
865 header = headers.get(plan_id)
866 if header
is not None:
867 return header.plan_number
868 match = PLAN_ID_RE.match(plan_id)
869 return int(match.group(
"number"))
if match
else 0
874 pending: Mapping[str, QueueEntry],
875 completed: Mapping[str, str],
876 status_sources: Mapping[str, StatusSource],
878 if plan_id
in status_sources
or plan_id
in pending:
880 if plan_id
in completed:
889 source: StatusSource |
None,
890 evidence: RoundEvidence |
None,
891 discovered_sha: str |
None,
893) -> dict[str, object] |
None:
894 if legacy_status
not in VALID_LEGACY_STATUSES:
895 errors.append(f
"{plan_id}: unknown legacy status {legacy_status!r}")
898 observed_at = source.body_observed_at
if source
is not None else None
901 "ready_for_merge_at":
None,
902 "completed_at":
None,
903 "closed_round":
None,
904 "backfill_payload":
None,
905 "blocked_payload":
None,
907 if legacy_status ==
"Completed":
908 if evidence
is not None:
911 "status":
"completed",
912 "completed_at": evidence.closed_at,
913 "closed_round": evidence.round_no,
916 elif plan_number < PRE_ROUND_SYSTEM_PLAN_CEILING:
917 completed_at = observed_at
918 base.update({
"status":
"completed",
"completed_at": completed_at})
919 base[
"backfill_payload"] = {
922 "reason":
"pre-round-system completed plan",
923 "body_observed_at": completed_at,
924 "discovered_in_main_at_sha": discovered_sha,
929 "status":
"ready_for_merge",
930 "ready_for_merge_at": observed_at,
933 elif legacy_status ==
"In Progress":
934 base[
"status"] =
"implementing"
935 elif legacy_status ==
"Blocked":
936 reason = source.blocked_reason
if source
is not None else None
937 base[
"blocked_payload"] = {
940 "original_blocked_reason": reason
or "legacy status was Blocked",
942 elif legacy_status ==
"Failed":
943 base[
"status"] =
"failed"
944 elif legacy_status ==
"Partial":
945 base[
"status"] =
"partial"
953 mapped: Mapping[str, object],
955) -> dict[str, object]:
959 "plan_number": header.plan_number,
960 "legacy_status": legacy_status,
961 "mapped_status": mapped[
"status"],
962 "source_file": source_file,
963 "body_path": header.source_file,
964 "body_sha256": header.body_sha256,
965 "ready_for_merge_at": mapped[
"ready_for_merge_at"],
966 "closed_round": mapped[
"closed_round"],
967 "completed_at": mapped[
"completed_at"],
973 "round_no": evidence.round_no,
975 "cleanup_status":
"clean",
976 "orchestrator_slot":
"agent-a",
977 "opened_at": evidence.closed_at,
978 "closed_at": evidence.closed_at,
979 "final_commit_sha": evidence.merge_commit_sha,
984 match = PLAN_ID_RE.match(plan_id)
985 number = int(match.group(
"number"))
if match
else 0
986 path = plan_dir / f
"{plan_id}.md"
1005 queue_entry: QueueEntry |
None,
1006 source: StatusSource |
None,
1007 completed_slot: str |
None,
1009 if queue_entry
is not None:
1010 return queue_entry.slot
1011 if source
is not None and source.agent_label
is not None:
1013 return completed_slot
1018 source: StatusSource |
None,
1019 queue_entry: QueueEntry |
None,
1020) -> tuple[str |
None, str |
None, str |
None]:
1022 _first_text(source.model
if source
else None, queue_entry.model
if queue_entry
else None, header.model),
1023 _first_text(source.thinking
if source
else None, queue_entry.thinking
if queue_entry
else None, header.thinking),
1024 _first_text(source.session
if source
else None, queue_entry.session
if queue_entry
else None, header.session),
1029 match = H1_RE.search(text)
1030 return plan_id
if match
is None else match.group(
"value").strip()
1034 match = HEADER_PATTERNS[name].search(text)
1037 if "value" in match.groupdict():
1045 return None,
None,
None
1047 model_match = re.search(
r"\b(?:gpt|claude|deepseek)[A-Za-z0-9_.-]*\b", value)
1049 thinking_match = re.search(
r"\bthinking\s*[=:]\s*(off|low|medium|high|max)\b", value)
1050 session_match = re.search(
r"\bsession\s*[=:]\s*(fresh|continue|either)\b", value)
1052 None if model_match
is None else model_match.group(0),
1053 None if thinking_match
is None else thinking_match.group(1),
1054 None if session_match
is None else session_match.group(1),
1061 text = value.strip().strip(
"`").strip()
1069 if text.lower()
in {
"main",
"none",
"n/a",
"na",
"tbd"}:
1075 plan_rows: Sequence[dict[str, object]],
1076 warnings: list[str],
1078 by_branch: dict[str, list[dict[str, object]]] = {}
1079 for row
in plan_rows:
1081 if branch
is not None:
1082 by_branch.setdefault(branch, []).append(row)
1083 for branch, rows
in by_branch.items():
1086 plan_ids =
", ".join(str(row[
"plan_id"])
for row
in rows)
1087 warnings.append(f
"duplicate branch {branch}: cleared for {plan_ids}")
1089 row[
"branch"] =
None
1090 payload = row.get(
"payload")
1091 if isinstance(payload, dict):
1092 payload[
"branch"] =
None
1099 key = text.strip().lower()
1100 return STATUS_NORMALISATIONS.get(key, text.strip())
1107 return status
if status
in VALID_LEGACY_STATUSES
else None
1111 text = value.strip()
1112 if text.lower().startswith(
"agent "):
1113 return f
"Agent {text.split()[-1].upper()}"
1114 if text.lower().startswith(
"agent-"):
1115 return f
"Agent {text.rsplit('-', 1)[-1].upper()}"
1117 return f
"Agent {text.upper()}"
1122 text = label.strip().lower()
1123 if text.startswith(
"agent-"):
1125 if text.startswith(
"agent "):
1126 return f
"agent-{text.split()[-1]}"
1128 return f
"agent-{text}"
1129 return text.replace(
" ",
"-")
1133 if isinstance(value, str):
1135 if isinstance(value, Sequence):
1138 if text
is not None:
1146 lowered = model.lower()
1147 return any(marker
in lowered
for marker
in (
"mini",
"flash",
"haiku",
"small"))
1153 lowered = model.lower()
1154 if lowered.startswith(
"gpt"):
1156 if lowered.startswith(
"claude"):
1158 if lowered.startswith(
"deepseek"):
1165 return path.resolve().relative_to(root.resolve()).as_posix()
1173 if hasattr(value,
"isoformat"):
1174 return str(value.isoformat())
1176 return text
if text
else None
1182 if hasattr(value,
"value"):
1184 text = str(value).strip()
1189 for value
in values:
1191 if text
is not None:
1197 for line
in text.splitlines():
1198 stripped = line.strip()
1205 match = PLAN_ID_RE.match(plan_id)
1206 return (int(match.group(
"number"))
if match
else 999999, plan_id)
1210 return root /
".option_c" /
"orchestration" /
"backups"
1214 safe = now.replace(
":",
"-").replace(
"+",
"_").replace(
"/",
"-")
1215 return _backup_dir(root) / f
"pre-migrate-{safe}.sqlite"
1219 backend = registry.backend
1220 if not isinstance(backend, SQLiteBackend):
1221 raise TypeError(
"migrate commands require SQLiteBackend")
1228 "slot": row[
"slot"],
1229 "slot_kind": row[
"slot_kind"],
1230 "identity_kind": row[
"identity_kind"],
1231 "model_class": row[
"model_class"],
1232 "orchestrator_eligible": int(row[
"orchestrator_eligible"]),
1233 "provider": row.get(
"provider"),
1234 "profile": row.get(
"profile"),
1237 for row
in sorted(rows, key=
lambda item: str(item[
"slot"]))
1256 "thinking_override",
1258 "ready_for_merge_at",
1263 {key: row.get(key)
for key
in keys}
1264 for row
in sorted(rows, key=
lambda item: str(item[
"plan_id"]))
1269 rows: Sequence[Mapping[str, object]],
1270) -> list[dict[str, object]]:
1273 "plan_id": row[
"plan_id"],
1274 "slot": row[
"slot"],
1275 "position": row[
"position"],
1277 for row
in sorted(rows, key=
lambda item: (str(item[
"slot"]), int(item[
"position"])))
1286 "orchestrator_slot",
1292 {key: row.get(key)
for key
in keys}
1293 for row
in sorted(rows, key=
lambda item: int(item[
"round_no"]))
1298 rows: Sequence[Mapping[str, object]],
1299) -> list[dict[str, object]]:
1300 keys = (
"round_no",
"plan_id",
"merge_order",
"merge_commit_sha",
"test_result")
1302 {key: row.get(key)
for key
in keys}
1305 key=
lambda item: (int(item[
"round_no"]), int(item[
"merge_order"])),
1311 expected: Mapping[str, object],
1312 actual: Mapping[str, object],
1313) -> dict[str, object]:
1314 diff: dict[str, object] = {}
1315 for section
in (
"agents",
"plans",
"queue_entries",
"rounds",
"round_plans"):
1316 exp = expected.get(section)
1317 act = actual.get(section)
1320 "expected_count": len(exp)
if isinstance(exp, list)
else None,
1321 "actual_count": len(act)
if isinstance(act, list)
else None,
1329 result: dict[str, object] = {}
1330 for name
in getattr(value,
"__dataclass_fields__", {}):
1331 item = getattr(value, name)
1332 if isinstance(item, tuple):
1333 result[name] = list(item)
dict[str, object] to_dict(self)
dict[str, object] to_dict(self)
dict[str, object] to_backend_payload(self)
dict[str, object] expected_snapshot(self)
dict[str, object] summary(self)
dict[str, object] to_dict(self)
dict[str, object] to_dict(self)
ImportResult run_import(Registry registry, MigrationOptions options, *, bool dry_run=False)
tuple[int, str] _plan_priority(str plan_id, Mapping[str, QueueEntry] pending, Mapping[str, str] completed, Mapping[str, StatusSource] status_sources)
tuple[str|None, str|None, str|None] _select_overrides(PlanHeader header, StatusSource|None source, QueueEntry|None queue_entry)
list[RoundEvidence] _discover_round_evidence(Path root)
Path _backup_path(Path root, str now)
dict[str, PlanHeader] _read_plan_headers(Path plan_dir, Path root)
dict[str, StatusSource] _read_status_sources(tuple[Path,...]|None explicit_paths, Path root, str observed_at, list[str] warnings)
SQLiteBackend _sqlite_backend(Registry registry)
dict[str, object] _snapshot_diff(Mapping[str, object] expected, Mapping[str, object] actual)
list[dict[str, object]] _comparable_round_plans(Sequence[Mapping[str, object]] rows)
RollbackResult run_rollback(Registry registry, MigrationOptions options)
int _plan_number(str plan_id, Mapping[str, PlanHeader] headers)
PlanHeader _placeholder_header(str plan_id, Path plan_dir, Path root)
StatusSource _read_status_source(Path path, Path root, str observed_at)
tuple[int, str] _plan_sort_key(str plan_id)
dict[str, object] _round_row(RoundEvidence evidence)
str|None _normalise_branch(str|None value)
bool _looks_small_model(str|None model)
dict[str, RoundEvidence] _round_evidence_by_plan(MigrationOptions options, Path root)
str|None _assigned_slot(QueueEntry|None queue_entry, StatusSource|None source, str|None completed_slot)
str|None _normalise_header_status(object value)
str|None _header_value(str text, str name)
tuple[str|None, str|None, str|None] _parse_suggested_model(str text)
str|None _first_model(object value)
str|None _string_timestamp(object value)
str|None _first_text(*object values)
list[dict[str, object]] _comparable_agents(Sequence[Mapping[str, object]] rows)
dict[str, object] _plan_payload(str plan_id, PlanHeader header, str legacy_status, Mapping[str, object] mapped, str source_file)
list[dict[str, object]] _build_agent_rows()
VerifyResult run_verify(Registry registry, MigrationOptions options)
Path _backup_dir(Path root)
set[str] _dedupe_plan_ids_by_number(set[str] plan_ids, Mapping[str, PlanHeader] headers, Mapping[str, QueueEntry] pending, Mapping[str, str] completed, Mapping[str, StatusSource] status_sources, list[str] warnings)
None _clear_duplicate_branches(Sequence[dict[str, object]] plan_rows, list[str] warnings)
list[dict[str, object]] _comparable_queue_entries(Sequence[Mapping[str, object]] rows)
str _agent_label(str value)
str _parse_title(str text, str plan_id)
dict[str, object] _result_dict(object value)
str|None _first_nonempty_line(str text)
list[dict[str, object]] _comparable_rounds(Sequence[Mapping[str, object]] rows)
MigrationProjection build_projection(Registry registry, MigrationOptions options)
str|None _optional_text(object value)
str|None _main_sha(Path root)
str _agent_label_to_slot(str label)
str _display_path(Path path, Path root)
str _select_legacy_status(str plan_id, PlanHeader header, StatusSource|None source, Mapping[str, QueueEntry] pending, Mapping[str, str] completed)
dict[str, object]|None _map_legacy_status(str plan_id, int plan_number, str legacy_status, StatusSource|None source, RoundEvidence|None evidence, str|None discovered_sha, list[str] errors)
str|None _clean_header_value(str|None value)
list[Path] _discover_status_paths(Path root)
list[dict[str, object]] _comparable_plans(Sequence[Mapping[str, object]] rows)
tuple[dict[str, QueueEntry], dict[str, str]] _read_queue_entries(Path queue_path, Path root)
dict[str, object] audit_null_closed_round_backfills(Registry registry)
str _normalise_legacy_status(object value)
str|None _provider_from_model(str|None model)
CutoverResult run_cutover_stub()