feat(RMGR-WP-0001): complete T05 end-to-end vertical slice

Implement observe/reconcile/update-task-status CLI path: workplan parse,
JSON projection index, git-backed task status writeback with correlation
events, and E2E pytest plus evidence artifacts. Finish foundation workplan.
This commit is contained in:
tegwick 2026-08-09 22:49:31 +02:00
parent 3cc67a9bd0
commit 8b5634ff2a
15 changed files with 873 additions and 20 deletions

View file

@ -0,0 +1,3 @@
from repo_manager.parse.workplan import ParsedTask, ParsedWorkplan, parse_workplan_file
__all__ = ["ParsedTask", "ParsedWorkplan", "parse_workplan_file"]

View file

@ -0,0 +1,114 @@
"""Workplan file parser (P0 extract — simplified from State Hub consistency_check)."""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
def _parse_yaml_block(raw: str) -> dict[str, Any]:
try:
data = yaml.safe_load(raw) or {}
except yaml.YAMLError:
return {}
return data if isinstance(data, dict) else {}
def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
if not text.startswith("---"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
return _parse_yaml_block(parts[1].strip()), parts[2]
@dataclass
class ParsedTask:
id: str | None
title: str | None
status: str | None
state_hub_task_id: str | None
raw: dict[str, Any] = field(default_factory=dict)
@dataclass
class ParsedWorkplan:
path: str
id: str | None
title: str | None
status: str | None
state_hub_workstream_id: str | None
frontmatter: dict[str, Any]
tasks: list[ParsedTask]
def parse_task_blocks(body: str) -> list[ParsedTask]:
headings = [
(m.start(), len(m.group(1)), m.group(2).strip())
for m in _HEADING_RE.finditer(body)
]
results: list[ParsedTask] = []
for m in _TASK_BLOCK_RE.finditer(body):
meta = _parse_yaml_block(m.group(1).strip())
prev = [(pos, level, text) for pos, level, text in headings if pos < m.start()]
title = meta.get("title")
if not title and prev:
title = prev[-1][2]
results.append(
ParsedTask(
id=str(meta["id"]) if meta.get("id") is not None else None,
title=str(title) if title else None,
status=str(meta["status"]) if meta.get("status") is not None else None,
state_hub_task_id=(
str(meta["state_hub_task_id"]).strip().strip('"')
if meta.get("state_hub_task_id") is not None
else None
),
raw=meta,
)
)
return results
def parse_workplan_text(text: str, *, relative_path: str) -> ParsedWorkplan:
fm, body = parse_frontmatter(text)
return ParsedWorkplan(
path=relative_path,
id=str(fm["id"]) if fm.get("id") is not None else None,
title=str(fm["title"]) if fm.get("title") is not None else None,
status=str(fm["status"]) if fm.get("status") is not None else None,
state_hub_workstream_id=(
str(fm["state_hub_workstream_id"]).strip().strip('"')
if fm.get("state_hub_workstream_id") is not None
else None
),
frontmatter=fm,
tasks=parse_task_blocks(body),
)
def parse_workplan_file(path: Path, *, repo_root: Path) -> ParsedWorkplan:
text = path.read_text(encoding="utf-8")
rel = str(path.relative_to(repo_root))
return parse_workplan_text(text, relative_path=rel)
def iter_workplan_files(repo_root: Path) -> list[Path]:
wp_dir = repo_root / "workplans"
if not wp_dir.is_dir():
return []
files: list[Path] = []
for p in sorted(wp_dir.rglob("*.md")):
if p.name.startswith("."):
continue
# skip archived copies optionally still include them
files.append(p)
return files