oc_orchestrator
Loading...
Searching...
No Matches
work_queue_manage.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_orchestrator/work_queue_manage.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by tests/test_258_queue_manage_cli.py and tests/test_271_standalone_queue.py"
6"""
7The queue's operator surface: list, add, move, dequeue, complete, next.
8
9Purpose
10-------
11Plan 258 P4. The queue CLI plan 223 retired is revived in the SHAPE
12that 140 dispatches taught it, and only in that shape. What comes back
13is ``list``, ``add``, ``move``, ``complete``, ``health``, ``reconcile``
14and ``history``, plus ``dequeue`` and ``next``. What does NOT come back
15is everything the letter model or the dispatcher now owns:
16
17``launch``
18 ``core/dispatch.py`` renders launch files.
19``prep-next``
20 ``next`` prints what the operator would dispatch; ``dispatch.ps1``
21 and ``prepare_worktree`` create the branch, the worktree and its
22 venv. This command never creates either, and never runs anything.
23``status-scan``
24 ``plan_manage agents active`` and the per-worktree
25 ``.PLAN_STATUS.md`` files cover it.
26``auto-assign`` / ``add-agent`` / ``list-agents``
27 The roster, the letters, the pools and the Plan-Type suggesters are
28 retired by operator ruling and are not revived.
29
30**This command dispatches nothing.** The autonomy boundary in the
31factory-decomposition design note §6 reserves the close and every
32ruling for the human, and automated dispatch is a later plan's work.
33``next`` prints the line an operator would run; that is the whole of
34it, and this module does not import ``subprocess`` so it could not do
35more if it tried.
36
37Responsibilities
38----------------
39- :func:`build_parser` -- the command shape, exposed so a witness can
40 read the defaults off it.
41- :func:`main` -- argument dispatch and the exit-code policy. It carries
42 the two consumer seams (plan 271 P2): ``prog``, the name the CLI gives
43 itself in ``--help``, and ``backend_factory``, which builds the
44 ``--backend registry`` backend. Both default to this package standing
45 alone: it names itself, and it has no registry to offer.
46- :func:`format_health_report` / :func:`format_reconcile_report` --
47 the operator-facing text, kept out of the command bodies.
48
49Diagnostics
50-----------
51Domain: OC_CHAT.MAIN
52Levels:
53 L2 - lifecycle events (which subcommand ran)
54 L3 - semantic details (a refusal and its exit code)
55 L4 - deep tracing (module load)
56
57Contracts
58---------
59- EXIT CODES: ``0`` success, ``1`` a semantic refusal (the operator
60 asked for something that does not make sense -- an unknown item, a
61 duplicate, a bad position, a missing plan file), ``2`` a BACKEND
62 refusal (:class:`~oc_orchestrator.work_queue.QueueBackendError` -- a
63 corrupt log, a storage constraint, a missing registry argument).
64 Argparse's own usage errors keep argparse's ``2`` via ``SystemExit``.
65- ``--backend file`` is the DEFAULT, and the file backend is the mode
66 the package's own tests run in (design note §7).
67- ``--backend registry`` is a PLUGIN POINT and not this package's
68 business: the registry that implements it is a consumer's, so this
69 module names none of it, lazily or otherwise, and refuses the flag
70 with :data:`NO_BACKEND_FACTORY` when no consumer supplied a factory.
71- Every path is injected: ``--queue-dir`` for the queue,
72 ``--plans-dir`` for plan files, ``--agent-registry`` for the instance
73 registry, ``--registry`` for the orchestration registry. The
74 defaults are today's values and nothing is discovered.
75- ``add`` reads a plan's header through
76 ``plan_metadata.py``'s provider -- the dispatch layer's one
77 plan-header reader -- and never parses a plan file itself.
78
79Dependencies
80------------
81- oc_diagnostics (structured debug logging via _dbg)
82- oc_orchestrator.work_queue (the contract)
83- oc_orchestrator.work_queue_file (the default backend)
84- oc_orchestrator.plan_inventory (the plans-dir literal and the terminal states)
85- oc_orchestrator.plan_metadata (the one plan-header reader)
86- oc_orchestrator.agent_instances (the instance registry ``health`` reads)
87"""
88
89from __future__ import annotations
90
91import argparse
92import sys
93from collections.abc import Callable
94from pathlib import Path
95
96from oc_diagnostics import _dbg
97
99 AgentInstanceRegistry,
100 AgentRegistryError,
101 default_agent_registry,
102)
104 DEFAULT_PLANS_DIR,
105 TERMINAL_STATUSES,
106 PlanPaths,
107)
108from oc_orchestrator.plan_metadata import resolve_provider
109from oc_orchestrator.work_queue import (
110 KIND_PLAN,
111 QueueBackend,
112 QueueBackendError,
113 QueueError,
114 QueueState,
115 WorkItem,
116)
118 DEFAULT_QUEUE_DIR,
119 SECTIONS,
120 FileQueueBackend,
121 QueuePaths,
122)
123
124_dbg("OC_CHAT.MAIN", "module", "work_queue_manage loaded", 4)
125
126
127#: Success, a semantic refusal, a backend refusal.
128EXIT_OK = 0
129EXIT_REFUSED = 1
130EXIT_BACKEND = 2
131
132#: What ``--backend registry`` says when no consumer supplied a factory.
133#: It is a BACKEND refusal -- the same arm that already refused a missing
134#: operator slot -- so it keeps that arm's ``EXIT_BACKEND`` and its
135#: one-line ``backend refused: ...`` stderr shape.
136NO_BACKEND_FACTORY = (
137 "--backend registry needs a consumer-supplied backend; "
138 "oc_chat supplies one through its shim"
139)
140
141#: What ``next`` prints. ``<model>`` stays a placeholder on purpose:
142#: the model is the OPERATOR'S attestation, not a queue field, and the
143#: queue has no business guessing it.
144DISPATCH_LINE = "scripts/dispatch.ps1 {number} <model>"
145
146
147# ── Plan files ───────────────────────────────────────────────────────────
148
149
150def _plan_file(paths: PlanPaths, plan_id: str) -> Path | None:
151 """The ACTIVE plan file for *plan_id*, or ``None``."""
152 candidate = paths.plans_dir / f"{plan_id}.md"
153 return candidate if candidate.is_file() else None
154
155
156def _terminal_plan_file(paths: PlanPaths, plan_id: str) -> Path | None:
157 """A state-suffixed plan file in a TERMINAL state, or ``None``.
158
159 ``plan_inventory.reconcile`` renames a plan's file to
160 ``<plan_id>_<status>.md`` when its status changes, so the
161 filesystem alone answers this -- no inventory singleton, no cwd.
162 """
163 for status in TERMINAL_STATUSES:
164 candidate = paths.plans_dir / f"{plan_id}_{status}.md"
165 if candidate.is_file():
166 return candidate
167 return None
168
169
170def _plan_worktree(paths: PlanPaths, plan_id: str) -> Path | None:
171 """The worktree the plan's header declares, through the one reader."""
172 plan_path = _plan_file(paths, plan_id) or _terminal_plan_file(paths, plan_id)
173 if plan_path is None:
174 return None
175 meta = resolve_provider(None).metadata(plan_path, plan_id=plan_id)
176 return Path(meta.worktree) if meta.worktree else None
177
178
179def _plan_title(paths: PlanPaths, plan_id: str) -> str | None:
180 """The plan's ``# `` heading, through the one reader."""
181 plan_path = _plan_file(paths, plan_id)
182 if plan_path is None:
183 return None
184 return resolve_provider(None).metadata(plan_path, plan_id=plan_id).title
185
186
187def _plan_number(plan_id: str) -> str:
188 """The ``NNN`` of a ``NNN-stem`` plan id, or the id when it has none."""
189 head = plan_id.split("-", 1)[0]
190 return head if head.isdigit() else plan_id
191
192
193# ── Backends ─────────────────────────────────────────────────────────────
194
195
197 args: argparse.Namespace,
198 backend_factory: Callable[[argparse.Namespace], QueueBackend] | None = None,
199) -> QueueBackend:
200 """The backend the flags ask for. ``file`` unless told otherwise.
201
202 The ``registry`` arm is the PLUGIN POINT plan 258's receipt predicted
203 and plan 271 P2 built. Before the move this function reached four
204 ``oc_chat.registry`` names on four consecutive lines, and the receipt
205 modelled them as a named cut precisely because a later extraction
206 would replace them with a seam. This is that seam: the package holds
207 no registry, so a consumer that wants one passes *backend_factory*
208 and the package names nothing of it at any depth.
209 """
210 func = "_build_backend"
211 if args.backend == "file":
212 _dbg("OC_CHAT.MAIN", func, f"file backend at {args.queue_dir}", 2)
213 return FileQueueBackend(QueuePaths(queue_dir=Path(args.queue_dir)))
214
215 if backend_factory is None:
216 # Raised bare, exactly as the pre-271 registry arm raised its
217 # missing-operator-slot refusal: `main`'s QueueBackendError handler
218 # owns the stderr line and the L1 trace, so logging here as well
219 # would print the same refusal twice.
220 raise QueueBackendError(NO_BACKEND_FACTORY)
221 _dbg("OC_CHAT.MAIN", func, "registry backend from the consumer's factory", 2)
222 return backend_factory(args)
223
224
225def _paths(args: argparse.Namespace) -> PlanPaths:
226 return PlanPaths(plans_dir=Path(args.plans_dir))
227
228
229# ── Rendering ────────────────────────────────────────────────────────────
230
231
232def _describe(item: WorkItem) -> str:
233 """One work item as one line of operator-facing text."""
234 parts = [item.item_id]
235 extras = [
236 f"{key}={value}"
237 for key, value in (
238 ("lane", item.lane),
239 ("instance", item.instance),
240 )
241 if value
242 ]
243 if extras:
244 parts.append(f"[{', '.join(extras)}]")
245 if item.note:
246 parts.append(f"- {item.note}")
247 return " ".join(parts)
248
249
250def format_state(state: QueueState) -> str:
251 """The three buckets, as ``list`` prints them."""
252 lines: list[str] = []
253 for title, attribute in SECTIONS:
254 lines.append(f"## {title}")
255 items = getattr(state, attribute)
256 if not items:
257 lines.append(" (none)")
258 for position, item in enumerate(items, start=1):
259 lines.append(f" {position}. {_describe(item)}")
260 lines.append("")
261 return "\n".join(lines).rstrip()
262
263
265 state: QueueState,
266 paths: PlanPaths,
267 agent_registry: Path | None,
268) -> str:
269 """The two forgetting cases, plus the counts.
270
271 A pending item is stale when its plan file is gone or when the plan
272 reached a terminal state and nobody dequeued it. An in-progress item
273 is stale when its instance is closed in ``agents.sqlite`` -- the
274 worker went away without the queue being told -- or when the
275 worktree its plan declares holds no ``.PLAN_STATUS.md``.
276 """
277 func = "format_health_report"
278 lines = ["## Queue health", ""]
279 lines.append(
280 f"pending {len(state.pending)} in progress {len(state.in_progress)} "
281 f"completed {len(state.completed)}"
282 )
283
284 stale_pending: list[str] = []
285 for item in state.pending:
286 if _terminal_plan_file(paths, item.item_id) is not None:
287 stale_pending.append(
288 f"- {item.item_id}: the plan file is in a terminal state; "
289 "it was never dequeued or completed"
290 )
291 elif _plan_file(paths, item.item_id) is None:
292 stale_pending.append(
293 f"- {item.item_id}: missing plan file under {paths.plans_dir}"
294 )
295 if stale_pending:
296 lines.extend(["", "## Pending items that need attention", *stale_pending])
297
298 registry = _open_instance_registry(agent_registry)
299 stale_running: list[str] = []
300 for item in state.in_progress:
301 if registry is not None and item.instance:
302 state_text = _instance_state(registry, item.instance)
303 if state_text == "closed":
304 stale_running.append(
305 f"- {item.item_id}: instance {item.instance} is closed; "
306 "nobody completed the item"
307 )
308 continue
309 worktree = _plan_worktree(paths, item.item_id)
310 if worktree is not None and not (worktree / ".PLAN_STATUS.md").is_file():
311 stale_running.append(
312 f"- {item.item_id}: no .PLAN_STATUS.md under {worktree}"
313 )
314 if stale_running:
315 lines.extend(["", "## In-progress items that need attention", *stale_running])
316
317 if not stale_pending and not stale_running:
318 lines.extend(["", "Nothing needs attention."])
319 _dbg(
320 "OC_CHAT.MAIN",
321 func,
322 f"{len(stale_pending)} pending, {len(stale_running)} running flagged",
323 3,
324 )
325 return "\n".join(lines)
326
327
328def _open_instance_registry(path: Path | None) -> AgentInstanceRegistry | None:
329 """The instance registry, or ``None`` when there is no file to read."""
330 if path is None or not Path(path).is_file():
331 return None
332 return AgentInstanceRegistry(Path(path))
333
334
335def _instance_state(registry: AgentInstanceRegistry, agent_id: str) -> str | None:
336 """*agent_id*'s state, or ``None`` when the registry does not know it."""
337 func = "_instance_state"
338 try:
339 return registry.get(agent_id).state
340 except (AgentRegistryError, ValueError) as exc:
341 # EXTERNAL_ERROR (L2): health is a report, not a gate. An
342 # unknown or malformed id is worth a trace and nothing more.
343 _dbg("OC_CHAT.MAIN", func, f"EXTERNAL_ERROR: {agent_id}: {exc}", 3)
344 return None
345
346
347def format_reconcile_report(drifts: tuple[object, ...], applied: bool) -> str:
348 """The drift list, as ``reconcile`` prints it."""
349 lines = ["## Queue reconcile", ""]
350 if not drifts:
351 lines.append("No drift detected.")
352 return "\n".join(lines)
353 for drift in drifts:
354 mark = "+" if getattr(drift, "fixable", False) else "!"
355 lines.append(
356 f"{mark} {getattr(drift, 'item_id', '?')}: "
357 f"{getattr(drift, 'kind', '?')} - {getattr(drift, 'detail', '')}"
358 )
359 unfixable = [d for d in drifts if not getattr(d, "fixable", False)]
360 lines.append("")
361 if applied:
362 lines.append(
363 f"Applied {len(drifts) - len(unfixable)} edit(s) to the log as "
364 "operations attributed to reconcile."
365 )
366 if unfixable:
367 lines.append(
368 f"{len(unfixable)} deletion(s) could not be applied: the "
369 "operation set has no remove. The projection was re-rendered "
370 "from the log, so those bullets are back."
371 )
372 else:
373 lines.append("(dry run; pass --apply to record these as operations)")
374 return "\n".join(lines)
375
376
377# ── Commands ─────────────────────────────────────────────────────────────
378
379
380def _cmd_list(args: argparse.Namespace, backend: QueueBackend) -> int:
381 print(format_state(backend.status()))
382 return EXIT_OK
383
384
385def _cmd_add(args: argparse.Namespace, backend: QueueBackend) -> int:
386 """Queue a plan, refusing an id with no active plan file."""
387 func = "_cmd_add"
388 paths = _paths(args)
389 if _plan_file(paths, args.plan_id) is None:
390 terminal = _terminal_plan_file(paths, args.plan_id)
391 detail = (
392 f"its only file is {terminal.name}, a terminal state"
393 if terminal is not None
394 else f"no {args.plan_id}.md"
395 )
396 print(
397 f"refused: {args.plan_id} has no active plan file under "
398 f"{paths.plans_dir} ({detail})",
399 file=sys.stderr,
400 )
401 _dbg("OC_CHAT.MAIN", func, f"VALIDATION_ERROR: {args.plan_id}", 3)
402 return EXIT_REFUSED
403 item = WorkItem(
404 kind=KIND_PLAN,
405 item_id=args.plan_id,
406 lane=args.lane,
407 note=args.note or _plan_title(paths, args.plan_id),
408 )
409 backend.enqueue(item, position=args.position)
410 print(f"Queued {_describe(item)}")
411 return EXIT_OK
412
413
414def _cmd_move(args: argparse.Namespace, backend: QueueBackend) -> int:
415 backend.move(args.plan_id, args.position, reason=args.reason)
416 print(f"Moved {args.plan_id} to position {args.position}.")
417 return EXIT_OK
418
419
420def _cmd_dequeue(args: argparse.Namespace, backend: QueueBackend) -> int:
421 backend.dequeue(args.plan_id, instance=args.instance, reason=args.reason)
422 print(f"Dequeued {args.plan_id} to {args.instance}.")
423 return EXIT_OK
424
425
426def _cmd_complete(args: argparse.Namespace, backend: QueueBackend) -> int:
427 """Finish an item. Already-complete is success, as the retired CLI had it."""
428 already = [
429 item.item_id for item in backend.status().completed
430 ]
431 if args.plan_id in already:
432 print(f"{args.plan_id} is already complete.")
433 return EXIT_OK
434 extra: dict[str, object] = {}
435 if args.round_no is not None:
436 extra["round_no"] = args.round_no
437 if args.merge_commit_sha is not None:
438 extra["merge_commit_sha"] = args.merge_commit_sha
439 backend.complete(args.plan_id, reason=args.reason, **extra)
440 print(f"Completed {args.plan_id}.")
441 return EXIT_OK
442
443
444def _cmd_next(args: argparse.Namespace, backend: QueueBackend) -> int:
445 """Print what the operator would dispatch. Never dispatch it.
446
447 The design note's autonomy boundary is the reason this prints
448 rather than runs, and automated dispatch is the hub's plan. The
449 ``<model>`` placeholder stays a placeholder: the model is the
450 operator's attestation.
451 """
452 func = "_cmd_next"
453 state = backend.status()
454 if not state.pending:
455 print("Nothing pending.")
456 return EXIT_OK
457 item = state.pending[0]
458 print(f"next: {_describe(item)}")
459 print(f"dispatch: {DISPATCH_LINE.format(number=_plan_number(item.item_id))}")
460 print("(this command prints the line; it does not run it)")
461 _dbg("OC_CHAT.MAIN", func, f"next is {item.item_id}", 2)
462 return EXIT_OK
463
464
465def _cmd_health(args: argparse.Namespace, backend: QueueBackend) -> int:
466 registry_path = (
467 Path(args.agent_registry) if args.agent_registry else default_agent_registry()
468 )
469 print(format_health_report(backend.status(), _paths(args), registry_path))
470 return EXIT_OK
471
472
473def _cmd_reconcile(args: argparse.Namespace, backend: QueueBackend) -> int:
474 """Compare the projection with the log. Dry-run unless ``--apply``."""
475 func = "_cmd_reconcile"
476 reconcile = getattr(backend, "reconcile", None)
477 if reconcile is None:
478 print(
479 f"refused: the {args.backend} backend has no projection to reconcile",
480 file=sys.stderr,
481 )
482 return EXIT_REFUSED
483 drifts = reconcile(apply=args.apply)
484 print(format_reconcile_report(drifts, applied=args.apply))
485 _dbg("OC_CHAT.MAIN", func, f"{len(drifts)} drift(s), apply={args.apply}", 2)
486 return EXIT_OK
487
488
489def _cmd_history(args: argparse.Namespace, backend: QueueBackend) -> int:
490 ops = backend.history(args.plan_id)
491 if not ops:
492 print(f"No recorded operations for {args.plan_id}.")
493 return EXIT_OK
494 for op in ops:
495 print(f"{op.seq:>4} {op.at} {op.op:<9} {op.actor} {dict(op.payload)}")
496 return EXIT_OK
497
498
499_COMMANDS = {
500 "list": _cmd_list,
501 "add": _cmd_add,
502 "move": _cmd_move,
503 "dequeue": _cmd_dequeue,
504 "complete": _cmd_complete,
505 "next": _cmd_next,
506 "health": _cmd_health,
507 "reconcile": _cmd_reconcile,
508 "history": _cmd_history,
509}
510
511
512# ── Parser ───────────────────────────────────────────────────────────────
513
514
515#: How this CLI names itself in ``--help``. A consumer that exposes the
516#: tool under its own module path passes its own (plan 271 P2): the
517#: shim's help output must stay byte-identical to what it printed before
518#: the split, and ``prog`` is the only line in it that could differ.
519DEFAULT_PROG = "python -m oc_orchestrator.work_queue_manage"
520
521
522def build_parser(prog: str = DEFAULT_PROG) -> argparse.ArgumentParser:
523 """The command shape, exposed so a witness can read the defaults."""
524 parser = argparse.ArgumentParser(
525 prog=prog,
526 description="Manage the OC Chat work queue. This command never dispatches.",
527 )
528 parser.add_argument(
529 "--backend",
530 choices=["file", "registry"],
531 default="file",
532 help="Queue backend (default: file, the tested mode)",
533 )
534 parser.add_argument(
535 "--queue-dir",
536 default=str(DEFAULT_QUEUE_DIR),
537 help=f"File-backend queue directory (default: {DEFAULT_QUEUE_DIR})",
538 )
539 parser.add_argument(
540 "--plans-dir",
541 default=str(DEFAULT_PLANS_DIR),
542 help=f"Where plan files live (default: {DEFAULT_PLANS_DIR})",
543 )
544 parser.add_argument("--registry", help="Registry-backend SQLite path")
545 parser.add_argument("--slot", help="Registry queue slot (default: the actor slot)")
546 parser.add_argument(
547 "--actor-slot", help="Registry actor slot (default: the active operator)"
548 )
549 parser.add_argument(
550 "--agent-registry", help="agents.sqlite path, read by health"
551 )
552 sub = parser.add_subparsers(dest="command", required=True)
553
554 sub.add_parser("list", help="Print pending, in-progress and completed items")
555
556 add = sub.add_parser("add", help="Queue a plan")
557 add.add_argument("plan_id")
558 add.add_argument("--lane", help="Free-text preference, e.g. a model family")
559 add.add_argument("--position", type=int, help="1-based position (default: last)")
560 add.add_argument("--note", help="Free text (default: the plan's title)")
561
562 move = sub.add_parser("move", help="Move a pending item to another position")
563 move.add_argument("plan_id")
564 move.add_argument("--position", type=int, required=True)
565 move.add_argument("--reason", required=True)
566
567 dequeue = sub.add_parser("dequeue", help="Take an item into progress")
568 dequeue.add_argument("plan_id")
569 dequeue.add_argument("--instance", required=True, help="agent-NNN")
570 dequeue.add_argument("--reason", required=True)
571
572 complete = sub.add_parser("complete", help="Finish an item")
573 complete.add_argument("plan_id")
574 complete.add_argument("--reason", required=True)
575 complete.add_argument(
576 "--round-no", type=int, help="Required by the registry backend"
577 )
578 complete.add_argument(
579 "--merge-commit-sha", help="Required by the registry backend"
580 )
581
582 sub.add_parser(
583 "next",
584 help="Print the first pending item and the dispatch line. Never runs it.",
585 )
586 sub.add_parser("health", help="Report items that look forgotten")
587
588 reconcile = sub.add_parser(
589 "reconcile", help="Compare the projection with the log (dry run by default)"
590 )
591 reconcile.add_argument(
592 "--apply",
593 action="store_true",
594 help="Record the hand edits as operations attributed to reconcile",
595 )
596
597 history = sub.add_parser("history", help="Print every operation for one item")
598 history.add_argument("plan_id")
599 return parser
600
601
603 argv: list[str] | None = None,
604 *,
605 prog: str = DEFAULT_PROG,
606 backend_factory: Callable[[argparse.Namespace], QueueBackend] | None = None,
607) -> int:
608 """Run one subcommand and return its exit code.
609
610 *prog* and *backend_factory* are the consumer seams (plan 271 P2).
611 Both default to this package standing alone: it names itself, and
612 ``--backend registry`` refuses because this package has no registry
613 to build one from. ``oc_chat``'s shim passes its own module path and
614 a factory that builds ``RegistryQueueBackend``, which is what keeps
615 every output and every queue transition identical across the split.
616 """
617 func = "main"
618 for stream in (sys.stdout, sys.stderr):
619 try:
620 stream.reconfigure(errors="replace")
621 except (AttributeError, ValueError):
622 pass # non-reconfigurable stream (tests, pipes wrapped by harnesses)
623
624 args = build_parser(prog).parse_args(argv)
625 _dbg("OC_CHAT.MAIN", func, f"subcommand {args.command}", 2)
626 try:
627 backend = _build_backend(args, backend_factory)
628 return _COMMANDS[args.command](args, backend)
629 except QueueBackendError as exc:
630 # The BACKEND refused: a corrupt log, a storage constraint, an
631 # argument only that backend demands. A different backend would
632 # not have refused, so it gets its own exit code.
633 print(f"backend refused: {exc}", file=sys.stderr)
634 _dbg("OC_CHAT.MAIN", func, f"SYSTEM_ERROR: {exc}", 1, override=True)
635 return EXIT_BACKEND
636 except QueueError as exc:
637 print(f"refused: {exc}", file=sys.stderr)
638 _dbg("OC_CHAT.MAIN", func, f"VALIDATION_ERROR: {exc}", 3)
639 return EXIT_REFUSED
640
641
642if __name__ == "__main__":
643 sys.exit(main())
int _cmd_dequeue(argparse.Namespace args, QueueBackend backend)
str|None _instance_state(AgentInstanceRegistry registry, str agent_id)
str|None _plan_title(PlanPaths paths, str plan_id)
int _cmd_reconcile(argparse.Namespace args, QueueBackend backend)
int _cmd_complete(argparse.Namespace args, QueueBackend backend)
Path|None _plan_worktree(PlanPaths paths, str plan_id)
int _cmd_add(argparse.Namespace args, QueueBackend backend)
argparse.ArgumentParser build_parser(str prog=DEFAULT_PROG)
int _cmd_list(argparse.Namespace args, QueueBackend backend)
str format_reconcile_report(tuple[object,...] drifts, bool applied)
Path|None _terminal_plan_file(PlanPaths paths, str plan_id)
str format_health_report(QueueState state, PlanPaths paths, Path|None agent_registry)
int main(list[str]|None argv=None, *, str prog=DEFAULT_PROG, Callable[[argparse.Namespace], QueueBackend]|None backend_factory=None)
QueueBackend _build_backend(argparse.Namespace args, Callable[[argparse.Namespace], QueueBackend]|None backend_factory=None)
int _cmd_move(argparse.Namespace args, QueueBackend backend)
Path|None _plan_file(PlanPaths paths, str plan_id)
int _cmd_health(argparse.Namespace args, QueueBackend backend)
int _cmd_history(argparse.Namespace args, QueueBackend backend)
PlanPaths _paths(argparse.Namespace args)
AgentInstanceRegistry|None _open_instance_registry(Path|None path)
int _cmd_next(argparse.Namespace args, QueueBackend backend)