From c3d8203e6c78156d28b0227cdbb59f6e58b62bec Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 20 Aug 2026 11:42:43 -0400 Subject: [PATCH] intake dashboard: Epics section under In flight + per-section markdown-version links (PyAutoMind#251) Co-Authored-By: Claude Fable 5 --- agents/conductors/intake/_intake.py | 104 +++++++++++++++++++++++++++- tests/test_intake_dashboard.py | 42 +++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index 9c07781..57ab230 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -389,6 +389,64 @@ def parse_registry(path: Path) -> list: return entries +# `epics.md` — the Mind's registry of long-running multi-phase programmes. +# Same H2-slug + `- key: value` shape as active.md, but the fields differ: +# `ledger:` names the epic's canonical state file (may live in another repo), +# `title:`/`status:`/`notes:` are display prose. The dashboard's job is only +# to hand a session enough to WORK OUT where the epic stands — the ledger +# stays the single source of truth. +_EPIC_FIELDS = ("title", "ledger", "status", "notes") + + +def parse_epics(path: Path) -> list: + """Parse `epics.md` into `[{slug, title, ledger, status, notes}]`. + + Tolerant like parse_registry: a slug alone still yields a record; absent + file -> empty list (a freshly-spawned Mind has no epics). + """ + if not path.is_file(): + return [] + entries, cur = [], None + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + head = _REG_HEAD.match(line) + if head: + cur = {"slug": head.group(1)} + cur.update({k: "" for k in _EPIC_FIELDS}) + entries.append(cur) + continue + if cur is None: + continue + field = _REG_FIELD.match(line) + if field and field.group(1) in _EPIC_FIELDS and not cur[field.group(1)]: + cur[field.group(1)] = field.group(2).strip() + return entries + + +def _epic_prompt(e: dict) -> str: + """The one-tap resume prompt: work out where the epic is, then continue. + + Deliberately a procedure, not a snapshot — any phase/issue state baked in + here would go stale the moment the epic advances, which is exactly the + problem the button exists to solve.""" + name = e.get("title") or e.get("slug", "this") + ledger = e.get("ledger", "") + parts = [f"Continue the '{name}' epic."] + if ledger: + parts.append( + f"Its canonical state lives in {ledger} — read that ledger (and " + "any DECISIONS/RESULTS files beside it) first.") + parts.append( + "Cross-check this epic's entry in PyAutoMind/epics.md, any related " + "rows in PyAutoMind/active.md, and the referenced repos' open issues " + "and PRs, to work out the last completed phase and what is currently " + "in flight. Then pick the next logical step and continue it through " + "the normal workflow (/start_dev — filing the phase's prompt first " + "if none exists), updating the ledger as the work advances.") + if e.get("notes"): + parts.append(f"Note: {e['notes']}") + return " ".join(parts) + + def _clip(text: str, limit: int = 130) -> str: """First line of a registry value, clipped at a word boundary.""" text = text.strip().splitlines()[0].strip() if text.strip() else "" @@ -492,6 +550,7 @@ def _count(key): "by_priority": _count("priority"), "records": records, "in_flight": in_flight, + "epics": parse_epics(mind / "epics.md"), "parked": parked, "planned": planned, "hygiene": hygiene, @@ -687,6 +746,23 @@ def render_dashboard(c: dict) -> str: L += _items(flight) or ["- _(nothing in flight)_"] L += [""] + if c.get("epics"): + L += ["## Epics", "", + "Long-running multi-phase programmes. Each 📋 prompt has Claude " + "read the epic's ledger, work out where it stands, and continue " + "from the next logical point — no hunting for the paired issue. " + "Full record in [`epics.md`](epics.md).", ""] + items = [] + for e in c["epics"]: + head = f"{_summary_label(e.get('title') or e['slug'])}" + if e.get("ledger"): + head += f" — ledger: `{e['ledger']}`" + if e.get("status"): + head += f" — {_summary_label(_clip(e['status']))}" + items.append(_task_row(head, _epic_prompt(e))) + L += _items(items) + L += [""] + for key, heading, verb, blurb in ( ("parked", "Parked", "resume", "Started or scoped, not currently in flight — " @@ -855,7 +931,14 @@ def record_row(r): H += [f'

{title} ({note}){more}

'] H += [record_row(r) for r in shown] or ['

(none right now)

'] - H += ["

In flight

", + def h2(title, src): + # Every section links the markdown file it is rendered from — the + # registry file is the full record, the page is the view. + a = (f' markdown ' + "version") if blob else "" + return f"

{title}{a}

" + + H += [h2("In flight", "active.md"), '

Issued — each has an open GitHub issue and ' "usually a branch.

"] for r in c["in_flight"]: @@ -869,10 +952,25 @@ def record_row(r): if not c["in_flight"]: H.append('

(nothing in flight)

') + if c.get("epics"): + H += [h2("Epics", "epics.md"), + '

Long-running multi-phase programmes — 📋 ' + "copies a prompt that works out where the epic stands from its " + "ledger and continues it from the next logical point.

"] + for e in c["epics"]: + text = f"{_summary_label(e.get('title') or e['slug'])}" + if e.get("ledger"): + text += (f' — ledger: ' + f"{_attr(e['ledger'])}") + if e.get("status"): + text += (f' — ' + f'{_summary_label(_clip(e["status"]))}') + H.append(_html_task(text, _epic_prompt(e))) + for key, heading, verb in (("parked", "Parked", "resume"), ("planned", "Planned", "start")): rows = c[key] - H += [f"

{heading}

", "
", + H += [h2(heading, f"{key}.md"), "
", f"{len(rows)} task(s)"] for e in rows: text = f"{_summary_label(e['slug'])}" @@ -887,7 +985,7 @@ def record_row(r): H.append('

(none)

') H.append("
") - H += ["

Backlog

", + H += [h2("Backlog", "draft").replace("/blob/main/draft", "/tree/main/draft"), f'

{c["total"]} filed prompts, not started — ' "sorted most-pickable first (priority, then size).

"] for wt, n in c["by_work_type"].items(): diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py index d9f6b5b..8dbb371 100644 --- a/tests/test_intake_dashboard.py +++ b/tests/test_intake_dashboard.py @@ -334,3 +334,45 @@ def test_check_ignores_the_generation_stamp_but_sees_content_drift(tmp_path, cap def test_check_on_a_missing_dashboard_is_drift(tmp_path): mind = _mind(tmp_path, drafts={"bug/widgets/one.md": _prompt("Bug one")}) assert _intake.main(["--mind", str(mind), "dashboard", "--check"]) == 1 + + +# --------------------------------------------------------------------------- # +# epics: long-running programmes resume from their ledger, not a paired issue +# --------------------------------------------------------------------------- # +_EPICS = """# Epics + +## jax-profiling +- title: JAX inference programme +- ledger: autolens_profiling/results/notes/inference/PROGRAMME.md +- notes: slices ship as autolens_profiling issues/PRs, not Mind prompts + +## bare-epic +""" + + +def test_epics_section_sits_under_in_flight_with_a_resume_prompt(tmp_path): + mind = _mind(tmp_path, registries={"epics.md": _EPICS}) + page = _page(mind) + assert page.index("## In flight") < page.index("## Epics") < page.index("## Parked") + epics = page.split("## Epics")[1].split("## Parked")[0] + assert "JAX inference programme" in epics + assert "PROGRAMME.md" in epics + # The copy payload is a procedure — work out the state, then continue. + assert "work out the last completed phase" in epics + assert "/start_dev" in epics + # A slug-only entry still lists (tolerant, like the other registries). + assert "bare-epic" in epics + + +def test_no_epics_file_means_no_epics_section(tmp_path): + page = _page(_mind(tmp_path, active={"one.md": _prompt("Solo task")})) + assert "## Epics" not in page, "a spawned Mind without epics.md stays clean" + + +def test_html_sections_link_their_markdown_source(tmp_path): + mind = _mind(tmp_path, registries={"epics.md": _EPICS, + "repos.yaml": REPOS_YAML}) + html = _html(mind) + for src in ("active.md", "epics.md", "parked.md", "planned.md"): + assert f'/blob/main/{src}">markdown version' in html, src + assert '/tree/main/draft">markdown version' in html