7Registry-native round-close commands.
11Expose the seven-phase round-close orchestration beneath
12``python -m oc_chat.registry round``: close, resume, abort, and status.
16- Parse round-close command arguments and plan specs.
17- Resolve the registry, orchestrator actor, and the git seam.
18- Drive run_round_close / resume_round / run_abort and render results.
19- Render the versioned escalation document when a job stops.
31- ``status`` is read-only; it never mutates the registry or git.
32- Interactive prompting is the default; ``--non-interactive`` disables it.
36- oc_diagnostics (structured debug logging via _dbg)
37- oc_chat.registry.backends.sqlite (SQLite backend)
38- oc_chat.registry.discovery (registry path discovery)
39- oc_chat.registry (Registry facade carried in the round context)
40- oc_chat.registry.errors (PreflightFailedError reported by the resume command)
41- oc_chat.registry.round.executors (round context, plan specs, test and lint results and run_abort)
42- oc_chat.registry.round.git_shim (GitShim git seam over the workspace repository)
43- oc_chat.registry.round.resume (resume_round for the resume command)
44- oc_chat.registry.round.state_machine (run_round_close, step definitions and job ids)
45- pytest (not imported; python -m pytest subprocess run by the default round-close test runner)
46- oct_lint (not imported; python -m oct_lint subprocess run by the default round-close lint runner)
48from __future__
import annotations
52from pathlib
import Path
56from oc_diagnostics
import _dbg
79_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
83 """Attach registry-native round-close subcommands."""
85 round_parser.set_defaults(handler=_cmd_round_group, round_parser=round_parser)
86 subcommands = round_parser.add_subparsers(dest=
"round_command")
88 close = subcommands.add_parser(
"close", help=
"Drive a fresh round-close")
90 close.add_argument(
"round_no", type=int)
95 metavar=
"PLAN_ID:BRANCH:WORKTREE[:ORDER]",
96 help=
"A plan to merge; repeatable. Order defaults to argument order.",
98 close.add_argument(
"--staging-branch", help=
"Override the staging branch name.")
99 close.add_argument(
"--base-ref", default=
"main")
100 close.add_argument(
"--main-branch", default=
"main")
101 close.add_argument(
"--app-version")
102 close.add_argument(
"--roadmap-dir", help=
"ROADMAP artifact directory.")
104 "--allow-untracked-roadmap-edits",
106 help=
"Proceed through cleanup after recording a ROADMAP audit override.",
111 help=
"Do not prompt at phase boundaries (default is interactive).",
113 close.set_defaults(handler=_cmd_close)
115 resume = subcommands.add_parser(
"resume", help=
"Resume a crashed round-close")
117 resume.add_argument(
"round_no", type=int)
118 resume.add_argument(
"--plan", action=
"append", default=[])
119 resume.add_argument(
"--staging-branch")
120 resume.add_argument(
"--base-ref", default=
"main")
121 resume.add_argument(
"--main-branch", default=
"main")
122 resume.add_argument(
"--roadmap-dir", help=
"ROADMAP artifact directory.")
124 "--allow-untracked-roadmap-edits",
126 help=
"Proceed through cleanup after recording a ROADMAP audit override.",
128 resume.add_argument(
"--non-interactive", action=
"store_true")
129 resume.set_defaults(handler=_cmd_resume)
131 abort = subcommands.add_parser(
"abort", help=
"Abort a round and reset its plans")
133 abort.add_argument(
"round_no", type=int)
134 abort.add_argument(
"--plan", action=
"append", default=[])
135 abort.add_argument(
"--staging-branch")
136 abort.add_argument(
"--base-ref", default=
"main")
137 abort.add_argument(
"--reason", required=
True)
138 abort.set_defaults(handler=_cmd_abort)
140 status = subcommands.add_parser(
"status", help=
"Show round and job status")
141 status.add_argument(
"round_no", type=int)
142 status.add_argument(
"--registry", help=
"Registry SQLite path.")
143 status.add_argument(
"--json", action=
"store_true")
144 status.set_defaults(handler=_cmd_status)
148 args.round_parser.print_help()
153 """Prompt the operator at a phase boundary; default is to continue."""
155 reply = input(f
"[round] proceed with phase {step.step_name}? [Y/abort] ").strip()
156 return "abort" if reply.lower()
in {
"abort",
"a",
"n",
"no"}
else "continue"
161 prompter =
None if args.non_interactive
else _interactive_prompter
162 result = run_round_close(
164 interactive=
not args.non_interactive,
172 prompter =
None if args.non_interactive
else _interactive_prompter
174 result = resume_round(
176 interactive=
not args.non_interactive,
179 except PreflightFailedError
as exc:
180 print(f
"resume preflight failed: {'; '.join(exc.reasons)}", file=sys.stderr)
187 outcome = run_abort(ctx, reason=args.reason)
188 print(f
"aborted round {ctx.round_no}: {outcome.result_summary}")
193 registry, backend =
_registry(args.registry)
194 round_row = backend.get_round(args.round_no)
195 if round_row
is None:
196 print(f
"round not found: {args.round_no}")
198 job = backend.latest_round_job(args.round_no)
199 steps = backend.list_round_job_steps(str(job[
"job_id"]))
if job
else []
201 payload = {
"round": round_row,
"job": job,
"steps": steps}
202 print(json.dumps(payload, indent=2, sort_keys=
True, default=str))
204 print(f
"round {args.round_no}: {round_row['status']} / {round_row['cleanup_status']}")
206 print(f
"job {job['job_id']}: {job['status']} @ {job['current_step'] or '-'}")
208 print(f
" {step['step_no']:>2} {step['step_name']:<24} {step['status']}")
213 registry, backend =
_registry(args.registry)
216 location = discover_registry_location(registry_path=args.registry)
217 repo_dir = Path(location.workspace_root)
219 staging = args.staging_branch
or f
"round/{args.round_no}/staging"
221 roadmap_dir = getattr(args,
"roadmap_dir",
None)
222 if roadmap_dir
is None and hasattr(args,
"allow_untracked_roadmap_edits"):
223 roadmap_dir = str(repo_dir /
"oc_chat" /
"roadmap")
227 round_no=args.round_no,
229 actor_slot=actor_slot,
230 staging_branch=staging,
232 actor_type=actor_type,
233 main_branch=getattr(args,
"main_branch",
"main"),
234 base_ref=getattr(args,
"base_ref",
"main"),
238 roadmap_dir=roadmap_dir,
239 allow_untracked_roadmap_edits=bool(
240 getattr(args,
"allow_untracked_roadmap_edits",
False)
242 app_version=getattr(args,
"app_version",
None),
247 specs: list[PlanSpec] = []
248 for index, raw
in enumerate(raw_specs, start=1):
249 parts = raw.split(
":")
253 f
"--plan must be PLAN_ID:BRANCH:WORKTREE[:ORDER], got {raw!r}"
255 order = int(parts[3])
if len(parts) >= 4
and parts[3]
else index
268 def _run(ctx: RoundContext) -> TestRunResult:
269 clock = ctx.registry.clock
270 started_ns = clock.now_monotonic_ns()
271 completed = subprocess.run(
272 [sys.executable,
"-m",
"pytest",
"-q"],
278 duration = (clock.now_monotonic_ns() - started_ns) / 1_000_000_000
280 passed=completed.returncode == 0,
281 duration_seconds=duration,
288 def _run(_ctx: RoundContext) -> LintRunResult:
289 completed = subprocess.run(
290 [sys.executable,
"-m",
"oct_lint",
"--profile",
"strict"],
302 func =
"_default_roadmap_bumper"
303 _dbg(
"OC_CHAT.MAIN", func,
"roadmap bump deferred to operator", 2)
305 def _bump(_ctx: RoundContext) -> str |
None:
314 if result.escalation
is not None:
315 print(json.dumps(result.escalation.to_dict(), indent=2, sort_keys=
True))
317 print(f
"round-close {result.status}: {len(result.completed_steps)} steps")
318 return 0
if result.status ==
"done" else 1
324 with backend.connect()
as conn:
326 "SELECT orchestrator_slot FROM orchestration_state WHERE id = 1"
328 if row
is not None and row[
"orchestrator_slot"]:
329 return str(row[
"orchestrator_slot"])
330 operator_slot = backend.active_operator_slot()
331 if operator_slot
is None:
332 raise ValueError(
"no active orchestrator or operator slot")
337 slot_kind = backend.actor_slot_kind(slot)
339 "operator":
"human_operator",
340 "orchestrator":
"orchestrator_agent",
341 "worker":
"worker_agent",
345def _registry(registry_path: str |
None) -> tuple[Registry, SQLiteBackend]:
346 location = discover_registry_location(registry_path=registry_path)
352 parser.add_argument(
"--registry", help=
"Registry SQLite path.")
355 help=
"Orchestrator slot. Defaults to the current orchestrator.",
int _cmd_resume(argparse.Namespace args)
_default_roadmap_bumper(str actor_slot, str actor_type)
str _actor_type_for_slot(SQLiteBackend backend, str slot)
list[PlanSpec] _parse_plan_specs(list[str] raw_specs)
int _render_result(RoundCloseResult result)
RoundContext _build_context(argparse.Namespace args)
str _interactive_prompter(StepDef step, RoundContext _ctx)
int _cmd_abort(argparse.Namespace args)
None add_round_subcommands(argparse.ArgumentParser round_parser)
str _resolve_orchestrator(SQLiteBackend backend, str|None actor_slot)
int _cmd_close(argparse.Namespace args)
_default_test_runner(Path repo_dir)
int _cmd_status(argparse.Namespace args)
_default_lint_runner(Path repo_dir)
tuple[Registry, SQLiteBackend] _registry(str|None registry_path)
int _cmd_round_group(argparse.Namespace args)
None _add_common(argparse.ArgumentParser parser)