7The file backend: an append-only operation log plus a Markdown projection.
11Plan 258 P2, from the factory-decomposition design note §5 and §7. This
12is the DEFAULT backend and the one the package's own tests run against,
13which is the condition that stops it rotting into dead code kept warm
14for strangers. It is two files under one injected directory:
16``work_queue.ops.jsonl``
17 The append-only log of typed operations, one JSON object per line
18 in the :class:`~oc_orchestrator.work_queue.QueueOp` shape. Opened for
19 APPEND only, flushed and fsynced per write, never rewritten. This
23 The projection, rendered by folding the log. A view a human may
24 also edit; :func:`FileQueueBackend.reconcile` compares the fold
25 with the parsed view and, with ``apply``, records the hand edit as
26 operations so the log stays complete.
28The projection's grammar is lifted from the queue plan 223 retired at
29commit ``9ac4b6a1``, with the letters removed: a first-line marker,
30three tracked H2 sections, ``- <item_id>`` bullets with an optional
31bracketed annotation, and every line outside a tracked bullet list
32preserved verbatim on round trip. :func:`_windows_safe_replace` is
33lifted VERBATIM from ``e1b5c716:oc_chat/oc_chat/core/agent_queue.py``
34because no dispatch-layer library module carries a temp-then-replace
35that recovers from a read-only target: ``scripts/plan_manage.py``'s
36``_atomic_write`` is script-private and has no recovery, and
37``core/provider_secrets.py`` is security code that is not imported for
42- :class:`QueuePaths` -- the injected filesystem inputs, in the
44- :func:`read_ops` / :func:`fold` -- the log, and the state it folds to.
45- :func:`render_projection` / :func:`parse_projection` -- the view.
46- :class:`FileQueueBackend` -- the six contract operations, plus
47 :meth:`~FileQueueBackend.write_projection` and
48 :meth:`~FileQueueBackend.reconcile`.
49- :class:`QueueDrift` / :class:`CorruptLogError` -- what reconcile
50 reports, and what a bad log line raises.
56 L2 - lifecycle events (a write, a fold, a reconcile)
57 L3 - semantic details (a refusal, a drift class)
58 L4 - deep tracing (module load, per-line parse)
62- NO LOCKING. Two writers can lose an update: each reads the log,
63 decides, and appends, so a concurrent pair can both append an op that
64 was computed against the same prefix. That is accepted while the
65 operator session is the sole writer (design note §5, and the plan-255
66 sidecar carries the same statement). A lock file or a daemon is
67 forbidden by operator ruling, not merely out of scope.
68- The log is APPEND-ONLY. No code path in this module opens the ops
69 file for writing, truncation or replacement. The projection is the
70 only file that is rewritten.
71- A corrupt log line is :class:`CorruptLogError` naming the file and
72 the 1-BASED line number. The backend never silently skips a line.
73- A re-render of unchanged state writes NOTHING:
74 :meth:`FileQueueBackend.write_projection` returns ``False`` and the
75 file's bytes and mtime are untouched.
76- ``note`` lives in the log, not in the projection: the annotation
77 grammar is ``key=value`` pairs separated by commas, and free prose
78 would not survive it. ``lane`` and ``instance`` are annotated.
79- Nothing here reads the process working directory. :class:`QueuePaths`
80 defaults to the relative literal that is today's location, exactly as
81 :class:`~oc_orchestrator.plan_inventory.PlanPaths` does, and the caller
86- oc_diagnostics (structured debug logging via _dbg)
87- oc_orchestrator.work_queue (the contract)
90from __future__
import annotations
98from dataclasses
import dataclass, field, replace
99from datetime
import datetime
100from pathlib
import Path
101from typing
import Any
103from oc_diagnostics
import _dbg
121_dbg(
"OC_CHAT.MAIN",
"module",
"work_queue_file loaded", 4)
132DEFAULT_QUEUE_DIR = Path(
".option_c/orchestration")
135OPS_FILENAME =
"work_queue.ops.jsonl"
138PROJECTION_FILENAME =
"work_queue.md"
141MARKER =
"<!-- work-queue: v1 -->"
145SECTIONS: tuple[tuple[str, str], ...] = (
146 (
"Pending",
"pending"),
147 (
"In progress",
"in_progress"),
148 (
"Completed",
"completed"),
153EMPTY_MARK =
"_(none)_"
156_SECTION_RE = re.compile(
r"^##\s+(?P<name>Pending|In progress|Completed)\s*$", re.I)
157_BULLET_RE = re.compile(
159 r"^-\s+(?P<item_id>[A-Za-z0-9][A-Za-z0-9_\-./]*)"
160 r"(?:\s+\[(?P<annotations>[^\]]*)\])?\s*$"
163_ACTOR_RECONCILE =
"reconcile"
167 """A line in the operation log does not parse; the message names it."""
173@dataclass(frozen=
True)
175 """All caller-controlled filesystem inputs to one queue.
177 The :class:`~oc_orchestrator.plan_inventory.PlanPaths` shape: a frozen
178 dataclass whose defaults are today's values, constructed at the
179 boundary and passed whole. ``queue_dir`` is late-bound through a
180 default factory so a caller that monkeypatches
181 :data:`DEFAULT_QUEUE_DIR` is still honoured.
184 queue_dir: Path = field(default_factory=
lambda: DEFAULT_QUEUE_DIR)
186 ops_path_override: Path |
None =
None
188 projection_path_override: Path |
None =
None
192 """The append-only log: the override, else derived."""
199 """The Markdown view: the override, else derived."""
202 return self.
queue_dir / PROJECTION_FILENAME
209 """A local-with-offset ISO 8601 timestamp, seconds resolution.
211 The project-wide convention, matching
212 ``core/plan_inventory.py::_now_iso``.
214 return datetime.now().astimezone().replace(microsecond=0).isoformat()
217def read_ops(ops_path: Path) -> tuple[QueueOp, ...]:
218 """Read every operation in *ops_path*, oldest first.
220 A missing file is an empty log, never a ``FileNotFoundError`` --
221 the same tolerance the retired ``load_queue`` had.
224 CorruptLogError: SYSTEM_ERROR (L1 for the caller) -- a
225 non-blank line is not a JSON object with the operation
226 fields, or names an operation that is not in
227 :data:`~oc_orchestrator.work_queue.OP_NAMES`. The message
228 names the file and the 1-based line number; the reader
229 never silently skips it.
232 path = Path(ops_path)
234 text = path.read_text(encoding=
"utf-8")
235 except FileNotFoundError:
236 _dbg(
"OC_CHAT.MAIN", func, f
"{path}: absent; empty log", 2)
238 ops: list[QueueOp] = []
239 for number, line
in enumerate(text.splitlines(), start=1):
243 _dbg(
"OC_CHAT.MAIN", func, f
"{path}: {len(ops)} operation(s)", 2)
247def _decode_op(line: str, path: Path, number: int) -> QueueOp:
248 """One log line to a :class:`QueueOp`, or a refusal that names it."""
251 raw = json.loads(line)
252 except json.JSONDecodeError
as exc:
253 _dbg(
"OC_CHAT.MAIN", func, f
"SYSTEM_ERROR: line {number}: {exc}", 1, override=
True)
255 f
"{path.name}: line {number} is not JSON: {exc}"
257 if not isinstance(raw, dict):
259 f
"{path.name}: line {number} is not a JSON object: {type(raw).__name__}"
261 missing = {
"seq",
"at",
"op",
"item_id"} - set(raw)
264 f
"{path.name}: line {number} is missing {', '.join(sorted(missing))}"
266 op_name = str(raw[
"op"])
267 if op_name
not in OP_NAMES:
269 f
"{path.name}: line {number} names an unknown operation {op_name!r}; "
270 f
"expected one of {', '.join(OP_NAMES)}"
272 payload = raw.get(
"payload")
or {}
273 if not isinstance(payload, dict):
275 f
"{path.name}: line {number} has a non-object payload"
281 item_id=str(raw[
"item_id"]),
283 actor=str(raw.get(
"actor",
"")),
288 """One :class:`QueueOp` to its log line, without the newline."""
294 "item_id": op.item_id,
295 "payload": dict(op.payload),
304 """Append *op* to the log, flushed and fsynced.
306 The ONLY write path for the log, and it opens the file in append
307 mode. Nothing in this module truncates or replaces it.
310 path = Path(ops_path)
311 path.parent.mkdir(parents=
True, exist_ok=
True)
312 with path.open(
"a", encoding=
"utf-8", newline=
"\n")
as handle:
315 os.fsync(handle.fileno())
316 _dbg(
"OC_CHAT.MAIN", func, f
"{path.name} += {op.op} {op.item_id} (seq {op.seq})", 2)
320 """Rebuild the work item an ``enqueue`` payload carried."""
322 kind=str(payload.get(
"kind", KIND_PLAN)),
331 return None if value
is None else str(value)
334def fold(ops: tuple[QueueOp, ...]) -> QueueState:
335 """Replay *ops* into the three buckets. The fold IS the state.
338 CorruptLogError: SYSTEM_ERROR -- an operation refers to an item
339 the earlier operations never put anywhere. A log that
340 cannot be folded is corrupt, and saying so beats guessing.
343 pending: list[WorkItem] = []
344 in_progress: list[WorkItem] = []
345 completed: list[WorkItem] = []
347 def _take(bucket: list[WorkItem], item_id: str) -> WorkItem |
None:
348 for index, item
in enumerate(bucket):
349 if item.item_id == item_id:
350 return bucket.pop(index)
354 if op.op == OP_ENQUEUE:
360 _take(completed, op.item_id)
361 position = op.payload.get(
"position")
362 index = len(pending)
if position
is None else int(position) - 1
363 pending.insert(index, item)
364 elif op.op == OP_MOVE:
365 item = _take(pending, op.item_id)
368 f
"operation {op.seq} moves {op.item_id!r}, which is not pending"
370 pending.insert(int(op.payload[
"position"]) - 1, item)
371 elif op.op == OP_DEQUEUE:
372 item = _take(pending, op.item_id)
375 f
"operation {op.seq} dequeues {op.item_id!r}, which is not pending"
378 replace(item, instance=
_optional_str(op.payload.get(
"instance")))
380 elif op.op == OP_COMPLETE:
381 item = _take(in_progress, op.item_id)
or _take(pending, op.item_id)
384 f
"operation {op.seq} completes {op.item_id!r}, which the queue "
387 completed.append(item)
392 f
"{len(ops)} op(s) -> {len(pending)}p/{len(in_progress)}i/{len(completed)}c",
396 pending=tuple(pending),
397 in_progress=tuple(in_progress),
398 completed=tuple(completed),
406 """The bracketed ``[k=v, k=v]`` suffix for *item*, or ``""``."""
409 for key, value
in ((
"lane", item.lane), (
"instance", item.instance))
412 return f
" [{', '.join(parts)}]" if parts
else ""
415def _bullets(items: tuple[WorkItem, ...]) -> list[str]:
418 return [f
"- {item.item_id}{_annotation(item)}" for item
in items]
422 """A fresh projection, written when there is no file to splice into."""
429 "The view. The truth is the append-only operation log beside this "
430 f
"file, `{OPS_FILENAME}`. Edit this file by hand if you like: "
431 "`queue_manage reconcile` reports the difference and "
432 "`reconcile --apply` records it as operations. Every line outside "
433 "the three tracked bullet lists is preserved verbatim."
437 for title, attribute
in SECTIONS:
438 lines.append(f
"## {title}")
440 lines.extend(
_bullets(getattr(state, attribute)))
442 return "\n".join(lines).rstrip() +
"\n"
446 """Replace every tracked bullet list inside *text* with fresh content.
448 Lifted in shape from the retired ``agent_queue._splice_into_existing``
449 with the letters removed. Lines outside any tracked section --
450 preamble, freeform paragraphs, unrelated H2s -- are preserved
451 verbatim, and the marker is inserted at the top when absent.
453 func =
"_splice_into_existing"
454 out_lines: list[str] = []
455 skip_existing =
False
457 for line
in text.splitlines():
458 match = _SECTION_RE.match(line)
461 out_lines.append(line)
463 out_lines.extend(
_bullets(getattr(state, attribute)))
471 if line.startswith(
"## ")
or line.startswith(
"# "):
472 skip_existing =
False
475 out_lines.append(line)
477 if MARKER
not in "\n".join(out_lines):
478 out_lines.insert(0,
"")
479 out_lines.insert(0, MARKER)
480 _dbg(
"OC_CHAT.MAIN", func,
"marker injected on save", 3)
482 rendered =
"\n".join(out_lines).rstrip()
484 for title, attribute
in SECTIONS:
485 if not re.search(rf
"^##\s+{re.escape(title)}\s*$", rendered, re.I | re.M):
486 tail.append(f
"## {title}")
488 tail.extend(
_bullets(getattr(state, attribute)))
491 rendered = rendered +
"\n\n" +
"\n".join(tail).rstrip()
492 _dbg(
"OC_CHAT.MAIN", func, f
"appended {len(tail)} line(s) for missing sections", 3)
493 return rendered.rstrip() +
"\n"
497 """The :class:`QueueState` field a tracked H2 title holds."""
498 lowered = title.strip().lower()
499 for section_title, attribute
in SECTIONS:
500 if section_title.lower() == lowered:
506 """The projection text for *state*, spliced into *existing* when given."""
513 """Parse a projection back into a state.
515 Regex-anchored on the three tracked H2 sections, exactly as the
516 retired parser was anchored on its eight. A bullet under any other
517 heading is not a work item and is ignored.
519 func =
"parse_projection"
520 buckets: dict[str, list[WorkItem]] = {
521 attribute: []
for _title, attribute
in SECTIONS
523 current: str |
None =
None
524 for line
in text.splitlines():
525 match = _SECTION_RE.match(line)
529 if line.startswith(
"## ")
or line.startswith(
"# "):
532 bullet = _BULLET_RE.match(line)
533 if bullet
is None or current
is None:
535 buckets[current].append(
541 ", ".join(f
"{name}: {len(items)}" for name, items
in buckets.items()),
545 pending=tuple(buckets[
"pending"]),
546 in_progress=tuple(buckets[
"in_progress"]),
547 completed=tuple(buckets[
"completed"]),
552 """A work item from a bullet's id and its ``[k=v, k=v]`` suffix."""
553 values: dict[str, str] = {}
554 for part
in (annotations
or "").split(
","):
555 key, separator, value = part.partition(
"=")
557 values[key.strip()] = value.strip()
561 lane=values.get(
"lane")
or None,
562 instance=values.get(
"instance")
or None,
567 """Atomically replace *dst* with *src*; tolerate Windows handle/read-only quirks.
569 Windows `os.replace` fails with `PermissionError` when the target is
570 read-only or briefly held by another reader (antivirus, indexer,
571 pytest cleanup). Clear the read-only attribute and retry up to twice
572 with short backoff. Atomicity is preserved on every attempt.
578 for attempt
in range(3):
582 except PermissionError:
585 dst.chmod(stat.S_IWRITE | stat.S_IREAD)
590 time.sleep(0.05 * (attempt + 1))
596@dataclass(frozen=True)
598 """One difference between the fold of the log and the parsed view."""
614 """The default backend: an append-only log and a Markdown view."""
616 def __init__(self, paths: QueuePaths |
None =
None, *, actor: str =
"operator") ->
None:
617 func =
"FileQueueBackend.__init__"
620 _dbg(
"OC_CHAT.MAIN", func, f
"over {self.paths.queue_dir}", 2)
625 """The three buckets, folded out of the log."""
628 def history(self, item_id: str) -> tuple[QueueOp, ...]:
629 """Every recorded operation about *item_id*, oldest first."""
631 op
for op
in read_ops(self.
paths.ops_path)
if op.item_id == item_id
636 def enqueue(self, item: WorkItem, position: int |
None =
None) -> QueueState:
637 """Put *item* into pending at *position*, or at the end.
639 A COMPLETED item may be enqueued again: the item comes back out
640 of completed and the log shows both runs. That is the motion
641 the registry's own enqueue vocabulary calls
642 ``promoted_from_partial`` / ``restored_from_aborted_round``, and
643 whose projection puts a completed plan back into ``pending``.
644 The retired ``add_task`` refused it because a slot's completed
645 LIST was that model's only record; here the log is the record,
646 so nothing is lost by allowing it.
649 DuplicateItemError: VALIDATION_ERROR (L3) -- the item is
650 already pending or already in progress.
651 BadPositionError: VALIDATION_ERROR (L3) -- *position* is
652 not a 1-based index into the pending list.
654 func =
"FileQueueBackend.enqueue"
657 if held
is not None and held !=
"completed":
658 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: duplicate {item.item_id}", 3)
660 f
"{item.item_id!r} is already {held} in this queue"
663 payload: dict[str, object] = {
666 "instance": item.instance,
670 return self.
_record(OP_ENQUEUE, item.item_id, payload)
672 def move(self, item_id: str, position: int, *, reason: str) -> QueueState:
673 """Move a pending item to *position*, for a stated *reason*."""
674 func =
"FileQueueBackend.move"
678 _dbg(
"OC_CHAT.MAIN", func, f
"{item_id}: {current} -> {index}", 3)
682 {
"position": index,
"from_position": current,
"reason": reason},
685 def dequeue(self, item_id: str, *, instance: str, reason: str) -> QueueState:
686 """Take an item out of pending into progress, naming *instance*."""
690 OP_DEQUEUE, item_id, {
"instance": instance,
"reason": reason}
698 round_no: int |
None =
None,
699 merge_commit_sha: str |
None =
None,
701 """Finish an item, for a stated *reason*.
703 Idempotent: completing an already-completed item records
704 nothing and returns the state unchanged, which is what the
705 retired ``complete_task`` did when it refused to append a plan
706 to a completed list twice.
708 ``round_no`` and ``merge_commit_sha`` are the REGISTRY backend's
709 demands (``queue_complete`` requires both). They are accepted
710 here so one call site drives both backends; this backend
711 records them in the op payload, because the log is the truth,
712 and derives no state from them.
715 UnknownItemError: VALIDATION_ERROR (L3) -- the queue holds
718 func =
"FileQueueBackend.complete"
721 if held ==
"completed":
722 _dbg(
"OC_CHAT.MAIN", func, f
"{item_id} already completed; no-op", 2)
726 f
"{item_id!r} is not pending or in progress in this queue"
728 payload: dict[str, object] = {
"reason": reason}
729 if round_no
is not None:
730 payload[
"round_no"] = round_no
731 if merge_commit_sha
is not None:
732 payload[
"merge_commit_sha"] = merge_commit_sha
733 return self.
_record(OP_COMPLETE, item_id, payload)
738 """Render the fold into the projection. ``True`` when it wrote.
740 A re-render of unchanged state writes NOTHING: no temp file, no
741 replace, so the bytes and the mtime both stand still.
743 func =
"FileQueueBackend.write_projection"
744 path = self.
paths.projection_path
746 existing: str |
None = path.read_text(encoding=
"utf-8")
747 except FileNotFoundError:
750 if existing
is not None and text == existing:
751 _dbg(
"OC_CHAT.MAIN", func, f
"{path.name} unchanged; wrote nothing", 2)
754 path.parent.mkdir(parents=
True, exist_ok=
True)
755 with tempfile.NamedTemporaryFile(
760 prefix=
".work_queue_",
765 tmp_path = Path(tmp.name)
767 _dbg(
"OC_CHAT.MAIN", func, f
"wrote {path} ({len(text)} bytes)", 2)
770 def reconcile(self, *, apply: bool =
False) -> tuple[QueueDrift, ...]:
771 """Compare the fold with the parsed projection; report the difference.
773 Dry-run by default: it writes nothing at all, neither file. With
774 *apply* it records each fixable difference as an operation with
775 ``actor="reconcile"`` -- so a hand edit ends up in the log like
776 every other change -- and then re-renders the projection from
779 A hand DELETION is reported and never applied: the contract has
780 no remove operation, and inventing one to absorb a deletion
781 would make the log lossy in exactly the way it exists to avoid.
782 ``--apply`` still re-renders afterwards, so the view comes back
783 into agreement with the truth and the operator has been told
784 which edit did not survive.
786 Returns what the DRY RUN would have reported, whether or not
787 *apply* was set, so a caller can print one list either way.
789 func =
"FileQueueBackend.reconcile"
791 text = self.
paths.projection_path.read_text(encoding=
"utf-8")
792 except FileNotFoundError:
793 _dbg(
"OC_CHAT.MAIN", func,
"no projection; nothing to compare", 2)
797 _dbg(
"OC_CHAT.MAIN", func, f
"{len(drifts)} drift(s); apply={apply}", 2)
798 if not apply
or not drifts:
804 for _pass
in range(len(drifts) + 1):
805 remaining = [d
for d
in self.
_drift(self.
status(), parsed)
if d.fixable]
815 self, op_name: str, item_id: str, payload: dict[str, object]
817 """Append one operation, re-render the view, return the new state."""
834 def _bucket_of(state: QueueState, item_id: str) -> str |
None:
835 """Which bucket holds *item_id*, in words, or ``None``."""
836 for label, attribute
in (
837 (
"pending",
"pending"),
838 (
"in progress",
"in_progress"),
839 (
"completed",
"completed"),
841 if any(item.item_id == item_id
for item
in getattr(state, attribute)):
846 """The 1-based position of *item_id* in pending, or a refusal."""
847 for index, item
in enumerate(state.pending, start=1):
848 if item.item_id == item_id:
851 where = f
"it is {held}" if held
else "the queue does not hold it"
856 """*position* as a 1-based index in ``1..upper``, or the end."""
859 if not 1 <= position <= max(upper, 1):
861 f
"position {position} is outside 1..{max(upper, 1)}"
866 def _drift(folded: QueueState, parsed: QueueState) -> tuple[QueueDrift, ...]:
867 """Every difference between the fold and the parsed view."""
868 drifts: list[QueueDrift] = []
870 item.item_id: attribute
871 for _title, attribute
in SECTIONS
872 for item
in getattr(folded, attribute)
875 item.item_id: attribute
876 for _title, attribute
in SECTIONS
877 for item
in getattr(parsed, attribute)
879 for item_id, attribute
in parsed_where.items():
880 if item_id
not in folded_where:
885 detail=f
"hand-added to {attribute}",
888 elif folded_where[item_id] != attribute:
893 detail=f
"{folded_where[item_id]} -> {attribute} by hand",
896 for item_id
in folded_where:
897 if item_id
not in parsed_where:
902 detail=
"deleted from the projection by hand; the log has "
903 "no remove operation, so this is reported, not applied",
912 for item
in parsed.pending
913 if folded_where.get(item.item_id) ==
"pending"
916 item.item_id
for item
in folded.pending
if item.item_id
in set(shared)
918 for position, item_id
in enumerate(shared, start=1):
919 if folded_order.index(item_id) != position - 1:
924 detail=f
"reordered to position {position} by hand",
931 """Record one fixable drift as an operation attributed to reconcile."""
932 func =
"FileQueueBackend._apply_drift"
933 if not drift.fixable:
934 _dbg(
"OC_CHAT.MAIN", func, f
"{drift.item_id}: {drift.kind} not applied", 3)
937 item, bucket, position =
_locate(parsed, drift.item_id)
938 if drift.kind ==
"added":
940 item, position=position
if bucket ==
"pending" else None
942 if bucket ==
"in_progress":
945 instance=item.instance
or "unknown",
946 reason=
"reconciled from the projection",
948 elif bucket ==
"completed":
950 drift.item_id, reason=
"reconciled from the projection"
952 elif drift.kind ==
"advanced":
953 if bucket ==
"in_progress":
956 instance=item.instance
or "unknown",
957 reason=
"reconciled from the projection",
959 elif bucket ==
"completed":
961 drift.item_id, reason=
"reconciled from the projection"
963 elif drift.kind ==
"moved":
967 reason=
"reconciled from the projection",
971def _locate(state: QueueState, item_id: str) -> tuple[WorkItem, str, int]:
972 """*item_id*'s item, bucket name and 1-based position inside it."""
973 for _title, attribute
in SECTIONS:
974 for position, item
in enumerate(getattr(state, attribute), start=1):
975 if item.item_id == item_id:
976 return item, attribute, position
985 "PROJECTION_FILENAME",
QueueState _record(self, str op_name, str item_id, dict[str, object] payload)
tuple[QueueDrift,...] _drift(QueueState folded, QueueState parsed)
QueueState enqueue(self, WorkItem item, int|None position=None)
tuple[QueueOp,...] history(self, str item_id)
None _apply_drift(self, QueueDrift drift, QueueState parsed)
int _checked_index(int|None position, int upper)
tuple[QueueDrift,...] reconcile(self, *, bool apply=False)
QueueState complete(self, str item_id, *, str reason, int|None round_no=None, str|None merge_commit_sha=None)
int _pending_index(self, QueueState state, str item_id)
bool write_projection(self)
QueueState dequeue(self, str item_id, *, str instance, str reason)
str|None _bucket_of(QueueState state, str item_id)
None __init__(self, QueuePaths|None paths=None, *, str actor="operator")
QueueState move(self, str item_id, int position, *, str reason)
Path projection_path_override
Path projection_path(self)
None append_op(Path ops_path, QueueOp op)
str _canonical_text(QueueState state)
tuple[QueueOp,...] read_ops(Path ops_path)
QueueState parse_projection(str text)
QueueState fold(tuple[QueueOp,...] ops)
str _encode_op(QueueOp op)
str _annotation(WorkItem item)
WorkItem _item_from_annotations(str item_id, str|None annotations)
None _windows_safe_replace(Path src, Path dst)
WorkItem _item_from_payload(str item_id, Any payload)
str render_projection(QueueState state, str|None existing)
str _splice_into_existing(str text, QueueState state)
QueueOp _decode_op(str line, Path path, int number)
list[str] _bullets(tuple[WorkItem,...] items)
str|None _optional_str(object value)
str _attribute_for(str title)
tuple[WorkItem, str, int] _locate(QueueState state, str item_id)