oc_chat
Loading...
Searching...
No Matches
plan_status.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/render/plan_status.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_render_plan_status.py"
6"""
7Renderer for per-worktree ``.PLAN_STATUS.md`` files.
8
9Purpose
10-------
11Render the registry's current plan assignment into the per-worktree status
12file used by workers and the legacy plan-status helper.
13
14Responsibilities
15----------------
16- Map registry lifecycle statuses to the legacy `.PLAN_STATUS.md` enum.
17- Render valid schema-v1.0 YAML frontmatter for a worktree.
18- Write status files through the shared atomic artifact helper.
19
20Diagnostics
21-----------
22Domain: OC_CHAT.MAIN
23Levels:
24 L2 - plan-status render lifecycle events
25 L3 - artifact drift details
26 L4 - per-field projection tracing
27
28Contracts
29---------
30- Output validates with `oc_chat.scripts.plan_status.read`.
31- No wall-clock values are invented by the renderer.
32
33Dependencies
34------------
35- oc_diagnostics (structured debug logging via _dbg)
36- yaml (frontmatter emission)
37- oc_chat.registry.render._common (projection and write helpers)
38- oc_chat.registry.api (Registry facade the plan row is read from)
39"""
40from __future__ import annotations
41
42from pathlib import Path
43
44from oc_diagnostics import _dbg
45import yaml
46
47from oc_chat.registry.api import Registry
49 RenderResult,
50 agent_letter,
51 plan_for_worktree,
52 write_text_artifact,
53)
54
55
56_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
57
58STATUS_FILENAME = ".PLAN_STATUS.md"
59
60_STATUS_TO_LEGACY = {
61 "draft": "Queued",
62 "pending": "Queued",
63 "implementing": "Implementing",
64 "ready_for_merge": "Completed",
65 "integrating": "Completed",
66 "completed": "Completed",
67 "partial": "Partially Completed",
68 "failed": "Failed",
69 "superseded": "Failed",
70}
71
72
73def render_plan_status(registry: Registry, *, worktree: Path) -> str:
74 """Render the status file for the plan assigned to ``worktree``."""
75
76 row = plan_for_worktree(registry, worktree)
77 if row is None:
78 return _render_empty(worktree)
79 status = _STATUS_TO_LEGACY.get(str(row["status"]), "Queued")
80 data: dict[str, object] = {
81 "schema_version": "1.0",
82 "plan_id": row["plan_id"],
83 "branch": row.get("branch") or "",
84 "worktree": row.get("worktree") or str(worktree),
85 "agent": agent_letter(row.get("assigned_slot")),
86 "model": row.get("model_override") or "gpt-5.5",
87 "thinking": _thinking(row.get("thinking_override")),
88 "session": _session(row.get("session_override")),
89 "status": status,
90 "progress": {
91 "steps_total": None,
92 "steps_done": None,
93 "current_step": _current_step(status),
94 },
95 }
96 if row.get("created_at"):
97 data["queued_at"] = str(row["created_at"])
98 if status == "Implementing" and row.get("updated_at"):
99 data["started_at"] = str(row["updated_at"])
100 if status in {"Completed", "Partially Completed", "Failed"}:
101 data["finished_at"] = row.get("body_observed_at") or row.get("updated_at")
102 body = f"# {row['plan_id']}\n\nRegistry-rendered status snapshot.\n"
103 return _frontmatter(data, body)
104
105
107 registry: Registry,
108 *,
109 worktree: Path,
110 prefer_local: bool = False,
111 dry_run: bool = False,
112) -> RenderResult:
113 """Write ``.PLAN_STATUS.md`` for ``worktree``."""
114
115 return write_text_artifact(
116 worktree / STATUS_FILENAME,
117 render_plan_status(registry, worktree=worktree),
118 artifact_kind="plan_status",
119 prefer_local=prefer_local,
120 dry_run=dry_run,
121 )
122
123
124def _render_empty(worktree: Path) -> str:
125 data: dict[str, object] = {
126 "schema_version": "1.0",
127 "plan_id": "(none)",
128 "branch": "",
129 "worktree": str(worktree),
130 "agent": "B",
131 "model": "gpt-5.5",
132 "thinking": "high",
133 "session": "either",
134 "status": "Queued",
135 "progress": {
136 "steps_total": None,
137 "steps_done": None,
138 "current_step": "No assigned registry plan",
139 },
140 }
141 return _frontmatter(data, "# No Assigned Plan\n\nRegistry has no plan for this worktree.\n")
142
143
144def _frontmatter(data: dict[str, object], body: str) -> str:
145 yaml_text = yaml.safe_dump(
146 data,
147 sort_keys=False,
148 allow_unicode=False,
149 default_flow_style=False,
150 ).rstrip()
151 return f"---\n{yaml_text}\n---\n\n{body.rstrip()}\n"
152
153
154def _thinking(value: object) -> str:
155 text = "high" if value is None else str(value)
156 return text if text in {"off", "low", "medium", "high", "max"} else "high"
157
158
159def _session(value: object) -> str:
160 text = "either" if value is None else str(value)
161 return text if text in {"fresh", "continue", "either"} else "either"
162
163
164def _current_step(status: str) -> str:
165 if status == "Implementing":
166 return "Implementing"
167 if status == "Queued":
168 return "Queued"
169 return "Completed - ready to merge"
RenderResult write_plan_status(Registry registry, *, Path worktree, bool prefer_local=False, bool dry_run=False)
str render_plan_status(Registry registry, *, Path worktree)
str _frontmatter(dict[str, object] data, str body)